Temporal planning
ferroplan supports PDDL2.1 durative actions. Temporal problems are
auto-detected (any :durative-action in the domain) and routed to a
decision-epoch forward search; the CLI prints the IPC temporal plan format.
What's supported
:durative-actionwithat start,over all, andat endconditions and effects.- Durations that are constants or parameter-dependent, e.g.
:duration (= ?duration (/ (distance ?a ?b) (speed ?v)))— evaluated per grounded action against the initial state, or per expansion against the current state when the duration reads a fluent some action modifies (state-dependent durations, since 0.12).?durationis also accepted inside numeric effect expressions (duration-dependent effects like(increase (energy ?x) (* ?duration (recharge-rate ?x))), since 0.10). - Duration inequalities —
(>= ?duration L)/(<= ?duration U)andandranges; the search commits to the shortest feasible duration. - Timed initial literals —
(at <time> <literal>)in:init; each becomes a synthetic exogenous applier fired from a pre-seeded agenda at its time (so a goal reachable only via a TIL is not pruned as a dead end). - Required concurrency — actions whose intervals must overlap (the classic "match / mend-fuse": the fuse can only be mended while the match is lit).
How it works
Each durative action is compiled into two instantaneous snap-actions so the existing grounder and relaxed-plan heuristic can be reused:
A-STARTtakes theat startcondition (plus theover allinvariant) and applies theat starteffects plus a(RUNNING-A …)token;A-ENDrequires theat endcondition, the invariant, and that token; it applies theat endeffects and drops the token.
The duration and the over all invariant live in a side table the temporal
search consumes: a decision-epoch search advances time over an agenda of pending
end-events, only letting A-END fire duration after its matching A-START,
and checking the invariant at both happenings and on every happening in
between (0.14): a happening whose effects would delete a running interval's
invariant fact is refused, so a delete + re-add sneaking through the endpoint
checks — a bake spanning the gap between two kiln firings — can't happen, and
nodes whose agenda head can never legally fire are pruned at birth. Since 0.13
the pending-interval agenda is symmetry-reduced (canonical ordering +
redundant identical-interval elimination, FF_NO_TSYMM=1 reverts) — same-epoch
starts of interchangeable intervals no longer multiply the visited space. 0.14
adds object-symmetry orbits on top (FF_NO_ORBIT=1 reverts): when a
problem's objects or goal pairs are interchangeable — identical init profiles,
symmetric goals, a grounded task closed under relabeling — the visited key is
canonicalized under member permutation, collapsing machine-shop-style
"which identical piece is which" state blowups.
The compression rung (0.28)
Before any of that, a task that needs no concurrency is tried the cheap
way: every durative action is read as ONE instantaneous action (condition =
start ∧ over all ∧ end, effect = start then end), the classical ladder plans
that, and the plan is put back on the clock by a left-shift over each
step's read/write sets — independent work overlaps, interfering work does not.
The result is validated against the ORIGINAL durative task before it is
returned, so the rung can be wrong about a task without ever being wrong about
a plan. It banks: the decision-epoch search then runs as a bounded quality
chase and the smaller makespan wins, so a task that solved before returns the
plan it returned before. It declines — and says so under FF_WALL_DEBUG=1 —
on required concurrency, timed initial literals, trajectory constraints and
unconstrained durations, and it stands aside when FF_TCONC=1 asks for the
actor scheduler. FF_NO_TCOMPRESS=1 restores the 0.27 route.
One consequence to know about: the left-shift overlaps whatever the PDDL
allows. A domain that is lockless by design — the cabin crew, where "one job
per worker" is the scheduler's convention and not a precondition — now gets
every independent job at once by default (crew-solo: makespan 109 → 47, one
worker on four jobs). That is a valid PDDL2.1 plan. If a resource can only do
one thing at a time, say so in the domain with a busy token, or run the actor
scheduler (FF_TCONC=1), under which the results are exactly 0.27's.
A plan in hand comes back inside the budget (0.28)
Everything done to a plan that already solves the task — the preference
quality chase, the makespan chase, the scorer — is optional work, and it
stops a reserve short of FF_TIME_LIMIT (3 % of the wall, 0.5–3 s, plus a
size term) and at 75 % of FF_MEM_BUDGET_GB. A runner that kills at the wall
or at the memory cap no longer takes a valid plan down with the process. A
banked plan whose preference scorer cannot be built in what is left is
returned with a NOT scored note rather than not returned.
Output
Plans are rendered in the IPC temporal format, start: (action args) [duration],
with the overall makespan:
0.000: (fly plane1 city-a city-b) [3.000]
3.000: (fly plane1 city-b city-c) [4.000]
From the library, temporal solutions carry time on each Step and a
makespan on the Plan.
Usage
ff -o temporal-domain.pddl -f problem.pddl # auto-detected
ff -o temporal-domain.pddl -f problem.pddl --mode temporal --json
Resource scheduling (renewable + consumable)
Durative actions over numeric fluents give you resource allocation over time for free — the case that matters for scheduling crews, machines, tools, power, or mana. Model a renewable resource as a pool that is taken at start and returned at end, guarded by an at-start check:
(:functions (workers))
(:durative-action chop-tree
:duration (= ?duration 3)
:condition (at start (>= (workers) 1))
:effect (and (at start (decrease (workers) 1)) ; held over the interval…
(at end (increase (workers) 1)) ; …released at the end
(at end (increase (wood) 1))))
Because the decrement persists until the matching end fires, the decision-epoch
search holds the resource across the whole [start, end] interval: a pool of 1
forces tasks to serialize, a larger pool lets them overlap. Consumable
resources (materials) are the same idea without the release — produced and
consumed by a crafting chain (wood → planks, planks + stone → house).
See examples/rpg/
for a full gather → craft → build example; the same problem with (= (workers) 1)
vs 3 plans serially (makespan ~19) vs in parallel (~13). Plans are satisficing,
not makespan-optimal — a good plan fast, suited to an agent that plans, acts, and
replans as the world changes.
Validation against VAL
Plans are validated with VAL, the IPC plan
validator. On the full IPC-2008/2011 tempo-sat corpus (630 instances, 30 s each),
ferroplan solves 399 — and every one of the 399 plans is VAL-valid under
PDDL2.1 continuous-time semantics, confirming the snap-action compilation,
over all invariants, required concurrency, and ε-separation are correct.
(Testing against VAL is what surfaced the ε-separation requirement in the first
place; since 0.10 the pass totally ε-orders execution, so same-instant mutexes —
conditional-effect ones included — are impossible by construction.) Coverage on
the unsolved remainder is search-limited: the recorded walls are guidance
problems, not semantics. See
benchmarks/ipc67-temporal.md.
Not yet supported
Continuous (#t) effects are not handled (discrete duration-dependent
effects via ?duration are — see above). PDDL3 trajectory constraints
((:constraints …)) are enforced on the classical path (untimed operators,
since 0.7) but not on the temporal path — a durative-action domain that
declares them is rejected rather than silently ignored. Temporal search
guidance on the recorded wall domains (machine-shop, storage, model-train)
is the main open work item.