Search options
This page is the reference for the --search option and for the
rest of the command line. It lists every search algorithm and every heuristic
the planner accepts, with each option, its type and its default value.
Contents
- The syntax of a search configuration
- Search algorithms
- Heuristics
- Options of a single CEGAR abstraction
- Options of a domain abstraction collection
- Abstraction sources
- How large an abstraction to build
- Optional build features
- The rest of the command line
- Output and exit codes
The syntax of a search configuration
One option selects the search algorithm and its heuristic. It is
--search, and its default value is astar(blind()).
Always quote the value. Parentheses mean something to the shell.
--search 'astar(blind())'
--search 'astar(lmcutnumeric())'
--search 'gbfs(ff())'
--search 'astar(domain_abstraction(max_abstraction_size=1000000))'
--search 'astar_fs(fast=lmcutnumeric(), slow=domain_abstraction())'
--search 'astar(canonical(domain(), cartesian()))'
A configuration is a call. A call is a name, optionally followed by a
parenthesised list of arguments separated by commas. An argument is either
positional or written as key=value. A value is either a scalar or
another call.
A call with no arguments may drop its parentheses. blind() and
blind are the same thing.
Names, option keys and values are all case-insensitive. The parser converts
them to lower case, so ASTAR(Blind()) works and
flaw_kind=Progression works.
Value types
The type column of every option table on this page uses these names.
| Type | Accepted values |
|---|---|
| bool | true or false. |
| int | A non-negative decimal integer. |
| float | A decimal number, or the word infinity. |
int or none | A non-negative decimal integer, or the word none. |
| string | Any bare scalar. It is converted to lower case. |
| {a, b, c} | One of the listed words. |
| heuristic | Another call, from the heuristic list. |
Positional arguments
An option may be given positionally instead of by name. The order is the
order of the rows in that name's option table on this page. So
domain_abstraction(1000000) sets
max_abstraction_size, because that is the first row.
Named arguments are clearer and they do not break when an option is added. Use them unless the call has only one argument.
What happens when a configuration is wrong
Nothing falls back to a default. An unknown name or an unknown option is an error, and the error quotes the word it did not recognise.
An unknown search algorithm is rejected while the command line is being parsed.
$ planforge --search 'nosuchengine(blind())' domain.pddl problem.pddl
error: invalid value 'nosuchengine(blind())' for '--search <SPEC>': unknown search engine `nosuchengine`
An unknown heuristic is rejected before the search starts.
$ planforge --search 'astar(nope())' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "unknown heuristic `nope`" }
An unknown option is rejected the same way.
$ planforge --search 'astar(domain_abstraction(debug=true))' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "unknown option `debug` for `domain_abstraction`" }
A value outside an enumeration is rejected with the value quoted.
$ planforge --search 'astar(domain_abstraction(flaw_kind=nope))' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "invalid FlawKind `nope`" }
All four of these exit with status 1.
An outer search(...) wrapper is accepted and ignored, so
search(astar(blind())) is read as astar(blind()). It
exists because some scripts write configurations that way.
Search algorithms
astar
astar(heuristic=blind(), mpd=false)
A* search. It orders the open list on f = g + h, where
g is the cost paid so far and h is the heuristic
estimate of the cost remaining.
With an admissible heuristic, A* returns a cheapest plan. A heuristic is admissible if it never estimates the remaining cost as higher than the true remaining cost. The heuristic list says which heuristics are admissible.
| Option | Type | Default | Meaning |
|---|---|---|---|
heuristic | heuristic | blind() | The heuristic to evaluate. May be given positionally, which is the usual way. |
mpd | bool | false | Re-evaluate a state when it is taken off the open list, instead of trusting the value it was inserted with. This helps heuristics whose estimates improve while the search runs, because a state inserted early was scored by a weaker heuristic. |
gbfs
gbfs(heuristic=blind())
Greedy best-first search. It orders the open list on h alone and
ignores the cost paid so far.
This is usually much faster than A* with the same heuristic. It has no optimality guarantee, not even with an admissible heuristic, because a cheap route to a state can be discarded in favour of a route that looks closer to the goal. The cost it returns is an upper bound on the cheapest cost.
This is the algorithm to use with ff(). The
home page shows it returning a cheapest
plan on one fixture and a plan of twice the cheapest cost on another.
| Option | Type | Default | Meaning |
|---|---|---|---|
heuristic | heuristic | blind() | The heuristic to evaluate. May be given positionally. |
astar_fs
astar_fs(fast=<heuristic>, slow=<heuristic>)
A* with two heuristics, one cheap and one expensive. The cheap one is
evaluated for every state and orders the open list. The expensive one is
evaluated only when a state is about to be expanded. The entry is then
reinserted with f = g + max(h_fast, h_slow).
Both arguments are required and both must be named. Positional arguments are rejected.
Because the key is a maximum, the search returns a cheapest plan when both heuristics are admissible. The maximum of two admissible estimates is admissible. The maximum of an admissible and an inadmissible estimate is not.
Nothing checks this for you. Passing ff() in either slot is
accepted, and it costs you the guarantee without a warning. Use the
admissibility column in the heuristic list, or wrap the
heuristic in check_admissible.
| Option | Type | Default | Meaning |
|---|---|---|---|
fast | heuristic | required | Evaluated for every generated state. Should be the cheaper of the two. |
slow | heuristic | required | Evaluated when a state is expanded. |
sgd
sgd(horizon=dovetail, particles=8, updates=20000, ...)
This is not a search algorithm. It builds a plan by gradient descent over a
transcription of the task up to a fixed horizon. There is no open list and no
state registry. It shares the --search option so that it can reuse
the translation, the resource limits and the plan output.
It is experimental. It reports no search statistics, and it needs the
sgd build feature. A build without that
feature parses the configuration and then refuses it.
$ planforge --search 'sgd()' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "`sgd(...)` requires the `sgd` cargo feature; rebuild with `cargo build --release -p planforge --features sgd`" }
All options must be named. These are the ones that decide the shape of a run.
| Option | Type | Default | Meaning |
|---|---|---|---|
horizon | int, dovetail, or dovetail(start, growth, max) | dovetail(8, 2.0, 512) | How many action slots the transcription has. An integer fixes the horizon. dovetail starts at start slots and multiplies by growth up to max. |
particles | int | 8 | How many independent candidate plans are optimised at once. |
updates | int | 20000 | Gradient updates per horizon. |
learning_rate, alias lr | float | 0.04 | Step size of the optimiser. |
cycles | int | 6 | Annealing cycles per horizon. |
seed | int | 1 | Seed for the random initialisation. |
verify_period | int | 10 | How often, in updates, the current candidate is checked against the real task. |
About seventy further options set penalty weights, temperatures and
annealing schedules. They are not documented here, because they are tuning
parameters for one experiment rather than a stable interface. They are listed
with their defaults in
planforge-searcher/src/sgd.rs.
Heuristics
These are the names the heuristic factory accepts. Where a heuristic has more than one spelling, the spellings are listed together and mean the same thing.
The admissibility column is the one that decides whether your plan is a
cheapest plan. astar needs an admissible heuristic for that, and
astar_fs needs both of its heuristics to be admissible.
"Yes" in this column means admissible by construction. If you want it checked
against a state rather than asserted, use
check_admissible.
| Name | Admissible | Needs | What it is |
|---|---|---|---|
blind | yes | The baseline. Zero on a goal state, the cheapest action cost elsewhere. | |
lmcutnumeric | yes | Numeric landmark cut. | |
ff | no | Relaxed plan length, with a monotonic numeric relaxation. | |
domain_abstraction | yes | One domain abstraction, refined by CEGAR. | |
cartesian_abstraction | yes | One numeric Cartesian abstraction, refined by CEGAR. | |
max_cartesian_abstraction, canonical_cartesian_abstraction | yes | The same abstraction, entered through a maximising or canonical wrapper. | |
canonical_domain_abstractions | yes | A collection of domain abstractions, combined canonically. | |
multi_domain_abstractions | yes | The same collection, combined by maximum. | |
greedy_numeric_pdb | yes | --restrict-task | One numeric pattern database over a greedily chosen pattern. |
canonical_numeric_pdb, max_numeric_pdb | yes | --restrict-task | Numeric pattern databases over systematically enumerated patterns. |
max, canonical, scp, cost_partitioning | yes | Combinators. They take abstraction sources and differ in how the parts are added up. | |
scp_online, scp_online_cartesian | yes | Saturated cost partitioning, with the option of computing it per state during the search. | |
fillscp, fill_scp, fillscp_cartesian, fill_scp_cartesian | yes | Saturated cost partitioning topped up with LM-cut over the leftover costs. | |
numeric_potential | yes | cplex | Potential functions found by linear programming. |
posthoc_optimization, pho | yes | cplex | Post-hoc optimisation over a collection of domain abstractions. |
pot_da_ocp | yes | cplex | An optimal cost partitioning over potential functions and one domain abstraction. |
check_admissible | same as its argument | A wrapper for debugging. It compares another heuristic against the true goal distance. |
The same instance under every heuristic
The table below is one run of each heuristic on the delivery fixture from the
home page. Every row is under
astar, so every row returns a plan of cost 22.
The point of the table is the spread in the expansion count. It is a factor of about a hundred between the weakest and the strongest heuristic here, and that is on one small instance. Do not read it as a ranking. A heuristic that wins on this fixture can lose badly on a task with a different structure.
| Heuristic | Expanded |
|---|---|
lmcutnumeric() | 73 |
fillscp() | 361 |
cartesian_abstraction() | 433 |
canonical(icaps26_cartesian(max_time=20)) * | 825 |
scp(domain(), online=false) | 1 060 |
scp_online(online=false) | 1 428 |
canonical_domain_abstractions() | 1 526 |
max(domain()) | 1 526 |
greedy_numeric_pdb() * | 6 603 |
domain_abstraction() | 6 657 |
canonical_numeric_pdb() * | 14 873 |
max_numeric_pdb() * | 35 031 |
blind() | 35 242 |
Expansions are not the same as time. lmcutnumeric expands 73
states here and fillscp expands 361, but the abstraction has to be
built before the search starts, and that construction is not in this column.
How large an abstraction to build shows what that costs.
blind
blind()
Zero on a goal state, and the cheapest action cost in the task on every other
state. This is the baseline. It is admissible and it tells the search almost
nothing, so A* with blind is close to uniform-cost search.
Admissible: yes. It takes no options. Passing one is an error.
lmcutnumeric
lmcutnumeric(ceiling_less_than_one=false, ignore_numeric=false, random_pcf=false,
irmax=false, disable_ma=false, use_second_order_simple=false,
use_constant_assignment=false, bound_iterations=0,
precision=0.000001, epsilon=0.0)
The numeric landmark-cut heuristic. It repeatedly finds a cut in the justification graph of a relaxed task, charges the cheapest operator in that cut, and subtracts that cost from the task before looking for the next cut. The sum of the charges is the estimate.
This is the strongest heuristic here on most of the fixtures in the
repository, and it is the one to try first. It can also be used as the component
that spends the leftover costs inside a cost partitioning. See
fillscp.
Admissible: yes.
| Option | Type | Default | Meaning |
|---|---|---|---|
ceiling_less_than_one | bool | false | Round a repetition count up to 1 when the computed count is below 1. |
ignore_numeric | bool | false | Drop the numeric part of the task and compute propositional LM-cut only. Useful for telling apart a propositional and a numeric regression. |
random_pcf | bool | false | Not implemented. Setting it to true is an error: lmcutnumeric random_pcf=true is not implemented yet. |
irmax | bool | false | A switch in the landmark cost computation. See the note below. |
disable_ma | bool | false | Turn off the multiplier-aware numeric repetition count. See the note below. |
use_second_order_simple | bool | false | A switch in the numeric repetition count. See the note below. |
use_constant_assignment | bool | false | Take constant assignment effects into account when building the relaxation. |
bound_iterations | int | 0 | Rounds of numeric bound propagation before the estimate is computed. 0 means no bounds are computed at all. |
precision | float | 0.000001 | Tolerance for comparing numeric quantities. Must be non-negative. |
epsilon | float | 0.0 | Slack added when a strict inequality has to be turned into a non-strict one. Must be non-negative. |
irmax, disable_ma and
use_second_order_simple select between alternative formulas in the
numeric part of the computation. They exist because numeric Fast Downward has
options of the same name, and the defaults above reproduce its behaviour. They
are for experiments. You do not need to set them to use the heuristic, and this
page does not describe the formulas, because the source is the only accurate
description of them.
ff
ff()
The FF heuristic. It solves a relaxed task in which delete effects are ignored, and returns the cost of that relaxed plan.
The numeric part uses a monotonic relaxation. Each numeric variable carries an envelope of reachable values, written as a smallest and a largest value, and that envelope only ever widens as the relaxed planning graph grows. A numeric comparison becomes available as soon as the envelope admits it.
Admissible: no. The estimate can be higher than the true
remaining cost. A plan found with ff is therefore not a cheapest
plan, and that holds under astar just as much as under
gbfs. Use it when you want a plan quickly and do not need a cheapest
one.
It refuses a task with scale-up or scale-down
effects, rather than returning an estimate it cannot justify. A relaxation that
widens an envelope in both directions cannot bound a multiplicative effect
soundly.
It takes no options. Passing one is an error.
There is no preferred-operator support. ff returns an estimate
and nothing else, so greedy search cannot use a preferred-operator queue.
domain_abstraction
domain_abstraction(max_abstraction_size=100000, ...)
One domain abstraction, built by counterexample-guided abstraction refinement. The construction starts from a very coarse abstraction, finds a plan in it, checks whether that plan works in the real task, and splits a variable's domain wherever it does not. It repeats until a limit is reached.
The estimate is the goal distance in the finished abstraction. Refinement also splits on comparison axioms, not only on facts, which is what makes it useful on a numeric task.
Admissible: yes.
Its options are the single CEGAR abstraction
options. max_abstraction_size is the one that decides how good
the heuristic gets. How large an abstraction to build
measures that.
cartesian_abstraction, max_cartesian_abstraction, canonical_cartesian_abstraction
cartesian_abstraction(max_abstraction_size=100000, ...)
One numeric Cartesian abstraction, built by the same refinement loop. An abstract state here is a Cartesian set, that is one subset of values per variable, so a split cuts such a set in two instead of merging domain values.
The three names build the same abstraction. max_cartesian_abstraction
and canonical_cartesian_abstraction wrap it in the maximising and the
canonical combination. With a single abstraction, all three give the same
estimate, and on the delivery fixture all three expand 433 states. The wrappers
are there so that the same configuration string keeps working when the source is
changed to produce several abstractions.
Admissible: yes.
These names accept all nine single CEGAR abstraction
options, and they use five of them:
max_abstraction_size, max_time,
combine_labels and random_seed. The other four,
max_iterations, use_wildcard_plans,
flaw_treatment and init_split_method, are accepted and
then dropped. That is worth knowing, because setting one of them here looks like
it worked and changes nothing.
To vary the split policy of a Cartesian abstraction, use the
cartesian(...) source under a
combinator instead. It has its own option set, and that set is the one that
reaches the split selector.
canonical_domain_abstractions, multi_domain_abstractions
canonical_domain_abstractions(max_abstraction_size=1000000, ...)
Several domain abstractions instead of one. The generator runs CEGAR repeatedly, each time from a different starting split, and keeps the abstractions it produces until it runs out of time or out of collection size.
canonical_domain_abstractions combines them canonically. It finds
the sets of abstractions whose costs do not overlap, adds the estimates within
each such set, and takes the maximum over the sets. That is admissible and it is
at least as strong as taking the plain maximum.
multi_domain_abstractions takes the maximum over the collection.
It is cheaper to evaluate and weaker.
Admissible: yes, both.
Their options are the domain abstraction
collection options. Note that total_max_time defaults to 10
seconds, so a bigger collection needs that option raised as well as
max_collection_size.
greedy_numeric_pdb
greedy_numeric_pdb(max_pdb_states=100000, numeric_first=true, random_seed=0, ...)
One numeric pattern database. A pattern is a subset of the variables. The
database is a table of goal distances in the task projected onto that pattern.
The pattern here is grown greedily, one variable at a time, until the table would
exceed max_pdb_states.
Admissible: yes.
Needs --restrict-task.
| Option | Type | Default | Meaning |
|---|---|---|---|
max_pdb_states | int | 100000 | Largest table the generator will build. This is the size bound on the pattern. |
numeric_first | bool | true | Put numeric variables into the pattern before propositional ones. |
random_seed | int | 0 | Seed for tie-breaking in the variable order. The word none is not accepted here. |
variable_order_type | {cg_goal_level, cg_goal_random, goal_cg_level} | goal_cg_level | How the candidate variables are ordered before the pattern is grown. |
exploration_heuristic | {zero, blind, lmcut} | blind | Heuristic used inside the projected search that fills the table. |
frontier_heuristic | {zero, blind, lmcut} | blind | Heuristic used for states on the frontier of the table. |
failed_lookup_heuristic | {zero, blind, lmcut} | blind | Estimate returned when a state is not in the table. |
canonical_numeric_pdb, max_numeric_pdb
canonical_numeric_pdb(max_pdb_states=50000, max_pattern_size=2, ...)
Several numeric pattern databases. Patterns are enumerated systematically up
to max_pattern_size variables, instead of grown greedily.
canonical_numeric_pdb combines the databases canonically.
max_numeric_pdb takes the maximum. Both names use the same options.
Admissible: yes, both.
Both need --restrict-task. The same
option set is also what the pdb(...) source
accepts.
| Option | Type | Default | Meaning |
|---|---|---|---|
max_pdb_states | int | 50000 | Largest table per pattern. |
max_pattern_size | int | 2 | Largest number of variables in a pattern. Raising this grows the number of patterns quickly. |
only_interesting_patterns | bool | true | Keep only patterns that can give a non-trivial estimate. Setting this to false is not implemented and aborts the run. |
exploration_heuristic | {zero, blind, lmcut} | blind | As above. |
frontier_heuristic | {zero, blind, lmcut} | blind | As above. |
failed_lookup_heuristic | {zero, blind, lmcut} | blind | As above. |
max, canonical, scp, cost_partitioning
max(<source>, ...)
canonical(<source>, ..., construction_max_time=<float>)
scp(<source>, ..., online=true, ...)
These three do not build abstractions. They combine whatever they are given. What they are given is one or more abstraction sources, passed positionally.
Splitting the two halves means the collection you build and the way you add it
up can be chosen independently. canonical(domain()) and
canonical_domain_abstractions() are the same heuristic, written two
ways, and the first form is the one that lets you swap in a different source.
A combinator with no source is an error.
$ planforge --search 'astar(max())' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "`max` requires at least one domain(...), cartesian(...), cartesian_collection(...), icaps26_cartesian(...), or pdb(...) source" }
Admissible: yes, all three.
max
The maximum over the components. It accepts sources and no options at all,
not even construction_max_time.
canonical
The canonical combination described under
canonical_domain_abstractions. It
accepts sources and exactly one option.
| Option | Type | Default | Meaning |
|---|---|---|---|
construction_max_time | float, seconds | no limit | One budget covering both the generation of the sources and the building of any lookup table. Must be finite and greater than zero. When it runs out, the run stops with shared abstraction construction deadline exceeded and exit code 7. |
scp, cost_partitioning
Saturated cost partitioning. Each component is given only the operator costs it actually needs to justify its own estimates, and the leftover costs are handed to the next component. The estimates can then be added instead of maximised, which is where the extra strength comes from.
Sources are positional and every option must be named. The options are
construction_max_time as above, plus this subset of the
scp_online options:
online, max_time,
table_construction_max_time, max_size,
diversify, samples, max_orders,
interval, combine_labels,
scoring_function, orders,
initial_order_generation_max_time,
order_optimization_max_time, saturator,
residual_sweeps, random_seed and
partitioning. They have the same types and defaults there.
Options that generate abstractions belong inside the source, not on the combinator. The error message says so.
planforge --search 'astar(scp(domain(total_max_time=60), online=false))' domain.pddl problem.pddl
scp_online, scp_online_cartesian
scp_online(online=true, max_time=200, table_construction_max_time=30, ...)
Saturated cost partitioning again, reached without naming a source.
scp_online builds a collection of domain abstractions.
scp_online_cartesian builds one Cartesian abstraction instead.
With online=true, the partitioning is computed for each state
during the search. With online=false, a table of orders is built
before the search starts and then looked up. The online form can be much
stronger per state and it pays for that on every evaluation.
These names have two option surfaces. Give a positional source and they behave
exactly like scp, with the restricted
option list. Give no positional source and the full option table below applies.
Admissible: yes.
| Option | Type | Default | Meaning |
|---|---|---|---|
online | bool | true | Compute the partitioning per state during the search, instead of from a table built up front. |
max_time | float, seconds | 200.0 | Total budget for building the abstractions and the orders. |
table_construction_max_time | float, seconds | 30.0 | Budget for the lookup table alone. |
max_size | int | unbounded | Largest lookup table, counted in entries. |
diversify | bool | false | Keep an order only when it improves the estimate on a sample state. Valid only with online=false. |
samples | int | 1000 | How many sample states are drawn for diversification. |
max_orders | int | unbounded | Largest number of orders kept. |
interval | int | unbounded | How often, in evaluations, a new order is computed in the online mode. |
combine_labels | bool | false | Merge operators with identical transitions into one label before partitioning. This also sets the same option on the collection. |
collection | call | see below | The collection options, as a nested call. The name of the call is ignored and only its arguments are used. |
use_numeric_pdbs | bool | false | Add numeric pattern databases to the collection. Setting this needs --restrict-task. |
max_pdb_states | int | 50000 | Size bound per pattern database, when they are used. |
max_pattern_size | int | 2 | Largest pattern, when pattern databases are used. |
only_interesting_patterns | bool | true | As for the pattern databases above. |
pdb_exploration_heuristic | {zero, blind, lmcut} | blind | Heuristic inside the projected search. |
pdb_frontier_heuristic | {zero, blind, lmcut} | zero | Heuristic on the frontier. |
pdb_failed_lookup_heuristic | {zero, blind, lmcut} | zero | Estimate for a state not in the table. |
scoring_function | {max_heuristic, min_stolen_costs, max_heuristic_per_stolen_costs} | max_heuristic_per_stolen_costs | How a candidate order is scored while orders are being built. |
orders | {greedy_orders, dynamic_greedy_orders, random_orders, diverse_orders} | greedy_orders | How the component orders are generated. |
initial_order_generation_max_time | float, seconds | 10.0 | Budget for producing the first order. |
order_optimization_max_time | float, seconds | 5.0 | Budget for improving an order after it has been produced. |
saturator | {all, perim, perimstar} | all | Which costs a component is allowed to keep when it saturates. |
residual_sweeps | int | 0 | Extra passes over the components to spend costs that were left over. |
random_seed | int or none | 2011 | Seed for order generation and sampling. This also sets the same option on the collection. |
partitioning | {label, region} | label | Whether costs are saturated per label or per state region. region is stronger and it makes the construction build operator footprints, which costs memory. |
Any option that is not in this table is passed on to the
collection options. So
scp_online(total_max_time=60) is legal and sets the collection's
budget. An unknown option is reported against the collection's type name.
combine_labels and random_seed write through to the
collection as well as to the partitioning. To set them differently for the two,
write the collection out with the explicit collection=... form.
fillscp, fill_scp, fillscp_cartesian, fill_scp_cartesian
fillscp(table_construction_max_time=30, combine_labels=false, ...)
Saturated cost partitioning per label, then LM-cut over whatever cost is left over. The abstractions rarely need every unit of cost, and the leftover would otherwise be wasted, so a second admissible heuristic is run on it and the two estimates are added.
The _cartesian spellings use a Cartesian abstraction in place of
the domain abstraction collection.
Admissible: yes.
| Option | Type | Default | Meaning |
|---|---|---|---|
table_construction_max_time | float, seconds | 30.0 | Budget for the lookup table. |
combine_labels | bool | false | As for scp_online. Also sets the collection's option. |
collection | call | see collection options | The collection, as a nested call. |
scoring_function | {max_heuristic, min_stolen_costs, max_heuristic_per_stolen_costs} | max_heuristic_per_stolen_costs | As for scp_online. |
orders | {greedy_orders, dynamic_greedy_orders, random_orders, diverse_orders} | greedy_orders | As for scp_online. |
order_optimization_max_time | float, seconds | 5.0 | As for scp_online. |
saturator | {all, perim, perimstar} | all | As for scp_online. |
random_seed | int or none | 2011 | As for scp_online. Also sets the collection's option. |
partitioning | {label, region} | label | As for scp_online. |
lmcut | call | see lmcutnumeric | Options for the LM-cut component that spends the leftover costs, as a nested call. The name of the call is ignored. |
Options not in this table go to the
collection options, as for
scp_online. One of them is overridden after parsing:
fillscp always sets collection_strategy=standard,
because it needs full-goal tasks. Passing a different value here has no
effect.
numeric_potential
numeric_potential(opt=initial_state, num_samples=1000, ...)
Potential functions. A potential function assigns a weight to every fact and every numeric variable, and the estimate for a state is the sum of the weights that hold in it. The weights come from a linear program whose constraints make the resulting estimate admissible.
Which linear program is solved depends on opt. You can optimise
the estimate for the initial state alone, for all states at once, for a set of
sampled states, or for a diverse portfolio of several functions.
Admissible: yes.
Needs the cplex build feature. Without it
the run stops before translation.
$ planforge --search 'astar(numeric_potential())' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "the requested LP-backed heuristic requires unrestricted CPLEX, which is not compiled into this build; rebuild with `--features cplex` and set CPLEX_ROOT" }
| Option | Type | Default | Meaning |
|---|---|---|---|
opt | {initial_state, all_states, samples, diverse_samples} | initial_state | Which objective the linear program maximises. |
num_samples | int | 1000 | Sample states drawn for the sampled objectives. Must be at least 1. |
num_heuristics | int | 4 | Functions in the diverse portfolio. Must be at least 1. |
max_diverse_generation_time | float, seconds | 30.0 | Budget for building the diverse portfolio. |
include_initial_state_potential | bool | true | Add the initial-state function to a portfolio. |
include_all_states_potential | bool | false | Add the all-states function to a portfolio. |
diverse_fallback | {largest_gap, random} | largest_gap | How the next sample is picked when diversification stalls. |
rays | int | 0 | Unbounded directions to look for. 0 disables the search for them. |
max_ray_generation_time | float, seconds | 30.0 | Budget for that search. |
ray_epsilon | float | 0.000001 | Tolerance used while looking for them. |
ray_certificate_file | string | numeric_potential_ray_certificate.json | Where a found direction is written. The parser lower-cases this value, so a path with capital letters will not survive. |
max_potential | float | 100000000 | Upper bound on any single weight. It keeps the linear program bounded. |
ignore_numeric_variables | bool | false | Give weights to facts only. |
bounds | {none, monotone, aibr, all} | none | Which analysis supplies bounds on the numeric variables. |
simple_action_bounds | bool | false | Use a cheaper per-action bound. |
goal_conditioned | bool | true | Let a function be specialised to the goal it is asked about. |
goal_cost_partitioning | bool | true | Partition costs between goals rather than solving one program for all of them. |
num_goal_cost_partitions | int | 4 | Partitions to build. Must be at least 1. |
num_goal_conditioned_heuristics | int | 1 | Goal-conditioned functions to build. Must be at least 1. |
num_goal_conditioned_samples | int | 100 | Samples used for them. |
max_conditioned_generation_time | float, seconds | 120.0 | Budget for building them. |
max_online_heuristics | int | 100 | Largest number of functions kept while the search runs. |
online_reoptimization_interval | int | 50 | Evaluations between two re-optimisations. Must be at least 1. |
max_consecutive_online_misses | int | 20 | Consecutive misses before re-optimising. |
max_online_misses | int | 12 | Total misses before re-optimising. |
max_online_lp_solves | int | 1000 | Cap on linear programs solved during the search. |
invalidate_online_cache_on_growth | bool | false | Drop cached estimates when a new function is added. |
online_reoptimization_on_new_states_only | bool | false | Re-optimise only for states never seen before. |
cache_estimates | bool | true | Remember the estimate for a state. |
precision | float | 0.000001 | Tolerance for comparing numeric quantities. |
epsilon | float | 0.0 | Slack for strict inequalities. |
dump_lp | bool | false | Write the linear program to a file for inspection. |
validate_duality | bool | false | Check the primal and dual solutions against each other. For debugging. |
posthoc_optimization, pho
posthoc_optimization(max_abstraction_size=1000000, ...)
Post-hoc optimisation over a collection of domain abstractions. One linear program per state decides how to distribute the operator costs among the abstractions so that their estimates add up to as much as possible. The model stays in memory between states, so only the objective changes from one evaluation to the next.
Admissible: yes.
Needs the cplex build feature. Its
options are the collection options.
pot_da_ocp
pot_da_ocp(abstraction=domain_abstraction_cegar(record_transition_system=true), nonnegative=false, ...)
An optimal cost partitioning between potential functions and one domain abstraction. Instead of running the two heuristics separately and taking a maximum, one linear program splits the operator costs between them so that the sum of the two estimates is as large as it can be.
Admissible: yes.
Needs the cplex build feature.
| Option | Type | Default | Meaning |
|---|---|---|---|
abstraction | call | required | Must be written as domain_abstraction_cegar(...) or domain_abstraction(...). Its arguments are the single CEGAR abstraction options, plus the two rows below. |
nonnegative | bool | false | Require every partitioned cost to be at least zero. |
Two options are read inside the abstraction= call before the
CEGAR options are applied.
| Option | Type | Default | Meaning |
|---|---|---|---|
record_transition_system | bool | false | Keep the abstract transition system after construction. The linear program needs it, so this must be set to true. Leaving it out is an error: pot_da_ocp requires record_transition_system=true in its abstraction generator. |
record_transition_system_max_transitions | int | 100000 | Cap on how many transitions are kept. |
Every other option of pot_da_ocp is passed to
numeric_potential, so that whole
table is available here too.
check_admissible
check_admissible(<heuristic>)
A wrapper for debugging. It forwards every query to the heuristic it wraps, and then computes the true goal distance of the same state by running blind A* over the real task. If the estimate is above the true distance, the heuristic is not admissible, and the wrapper reports an evaluation error instead of letting the search carry on with a broken bound.
It takes exactly one heuristic, positionally.
planforge --search 'astar(check_admissible(lmcutnumeric()))' \
tests/assets/numeric-pddl-files/delivery/domain.pddl \
tests/assets/numeric-pddl-files/delivery/pfile1.pddl
It is very expensive, because it solves the remaining task from scratch for every state it sees. On the delivery fixture it leaves the search unchanged, at 73 expansions and cost 22, and takes it from 0.03 seconds to about 58 seconds. Use it when you suspect a heuristic. Do not use it in an experiment.
Options of a single CEGAR abstraction
These nine options are shared by
domain_abstraction,
cartesian_abstraction,
max_cartesian_abstraction,
canonical_cartesian_abstraction and the
abstraction= call of
pot_da_ocp. They are listed in positional
order.
| Option | Type | Default | Meaning |
|---|---|---|---|
max_abstraction_size | int | 100000 | Largest number of abstract states. Refinement stops when the next split would exceed this. This is the option that decides how good the heuristic gets. See below. Must be greater than zero. |
max_iterations | int | unbounded | Largest number of refinement steps. Must be greater than zero. |
max_time | float, seconds; infinity means no limit | no limit | Wall-clock budget for building the abstraction. |
use_wildcard_plans | bool | false | Let a step of the abstract plan stand for any operator with the same label, instead of one fixed operator. This gives the flaw search more room and can find a working plan sooner. |
combine_labels | bool | true | Merge operators that induce the same abstract transitions into one label. This shrinks the transition system. |
random_seed | int or none | 2011 | Seed for the random choices in flaw selection. none leaves the generator unseeded, so the abstraction differs between runs. |
flaw_treatment | see below | random_single_atom | How a flaw is turned into one or more splits. |
flaw_kind | see below | progression | How flaws are looked for along the abstract plan. |
init_split_method | see below | init_value | How a variable's domain is split the first time it is refined. |
These are the only nine names accepted. Every other field of the
configuration is set by the planner and is not reachable from the command line,
which is why domain_abstraction(debug=true) is an error.
Values of the three enumerated options
flaw_treatment is one of
random_single_atom, one_split_per_atom,
one_split_per_variable, max_refined_single_atom,
min_growth_single_atom, max_refined_preferring_prop,
closest_to_goal,
balance_max_refined_and_closest_to_goal or
balance_max_refined_preferring_prop_and_closest_to_goal.
flaw_kind is one of
progression, regression,
execute_entire_plan, sequence_progression,
sequence_regression, sequence_bidirectional or
target_centered.
progression walks the abstract plan forward from the initial
state and stops at the first step that does not apply.
regression walks it backward from the goal.
execute_entire_plan walks the whole plan and collects every flaw it
finds, which gives more splits per refinement step and costs more time per
step.
init_split_method is one of
goal_value, goal_value_or_random_if_non_goal,
init_value, random_value,
random_partition,
random_binary_partition_separating_init_goal or
identity.
The Cartesian names accept all nine options and use only
max_abstraction_size, max_time,
combine_labels and random_seed. See
above.
Options of a domain abstraction collection
These options are shared by
canonical_domain_abstractions,
multi_domain_abstractions,
posthoc_optimization, the
domain(...) source, and the
collection= call of
scp_online and
fillscp.
Several names appear both here and in the
single-abstraction table with a different default.
max_abstraction_size is 1 000 000 here and 100 000 there.
flaw_kind is execute_entire_plan here and
progression there. Check which table applies to the name you are
configuring.
| Option | Type | Default | Meaning |
|---|---|---|---|
max_abstraction_size | int | 1000000 | Largest number of abstract states in one member of the collection. |
max_collection_size | int | 10000000 | Largest total number of abstract states over the whole collection. Generation stops when this is reached. |
abstraction_generation_max_time | float, seconds | infinity | Budget for one member. |
total_max_time | float, seconds | 10.0 | Budget for the whole collection. Ten seconds is short. Raise this before you raise max_collection_size, or the size bound will never be reached. |
stagnation_limit | float, seconds | 20.0 | How long generation may run without producing a useful new member before it reacts. |
blacklist_trigger_percentage | float | 0.75 | Fraction of total_max_time after which blacklisting may start. |
enable_blacklist_on_stagnation | bool | true | On stagnation, forbid some variables so that the next member is forced to differ from the ones already found. |
blacklist_option | {goals, non_goals, all} | all | Which variables may be blacklisted. |
init_split_candidates | {goals, non_goals, all} | all | Which variables may be used for the first split of a member. |
init_split_quantity | {none, single, all} | single | How many variables are split first. none forces init_split_method=identity. |
random_seed | int or none | 2011 | Seed for every random choice in generation. |
debug | bool | false | Print what the generator is doing. This option exists here and not on a single abstraction. |
use_wildcard_plans | bool | false | As in the single-abstraction table. |
combine_labels | bool | true | As in the single-abstraction table. |
flaw_kind | see the enumeration | execute_entire_plan | As in the single-abstraction table, with a different default. |
flaw_treatment | see the enumeration | random_single_atom | As in the single-abstraction table. |
init_split_method | see the enumeration | init_value | As in the single-abstraction table. |
numeric_split_strategy | {standard, exclusion} | standard | How a numeric split value is chosen. exclusion parses and is then refused, because it is not implemented in this port. |
collection_strategy | {standard, complementary} | standard | complementary builds members that are meant to cover different parts of the task. |
interleave_split_directions | bool | false | Alternate forward and backward split-value selection between members. Requires collection_strategy=standard and cannot be combined with a fixed split_direction. |
split_direction | {default, forward, forward_partition_deviation, backward} | default | Where a numeric split value is placed. default lets the flaw kind decide. |
Abstraction sources
A source is what max,
canonical and scp combine. Sources are passed
positionally. A source given with a key is an error.
planforge --search 'astar(canonical(domain()))' domain.pddl problem.pddl
planforge --search 'astar(canonical(domain(), cartesian()))' domain.pddl problem.pddl
planforge --search 'astar(max(domain(total_max_time=60)))' domain.pddl problem.pddl
There are five source names.
domain, domain_abstractions
A collection of domain abstractions built by CEGAR. Its options are the collection options.
When the combinator has a construction_max_time, that budget also
caps this source's total_max_time and
abstraction_generation_max_time.
cartesian, cartesian_abstraction
One numeric Cartesian abstraction. Unlike the
cartesian_abstraction
heuristic, this source has its own option set, and that set reaches the split
selector and the refinement direction.
Two option names are accepted as aliases for the first two rows, so that a
configuration written for a domain abstraction can be reused:
max_abstraction_size means max_states, and
abstraction_generation_max_time means max_time.
| Option | Type | Default | Meaning |
|---|---|---|---|
max_states | int | 10000 | Largest number of abstract states. |
max_time | float, seconds; infinity means no limit | no limit | Budget for the construction. |
combine_labels | bool | false | Merge operators with the same abstract transitions. |
debug | bool | false | Print what the refinement loop is doing. |
random_seed | int | unseeded | Seed for random split selection. The word none is not accepted here. |
flaw_kind | {progression, execute_entire_plan} | progression | Only these two values are accepted. Any other value of the wider flaw-kind enumeration is refused by name. |
refinement_direction | {progression, regression, target_centered} | progression | Which end of the plan refinement works from. target_centered is another spelling of regression. |
split_selection | {min_transition_growth, min_growth, max_additive_steps, random, least_refined} | min_transition_growth | Which split is chosen when several are possible. min_growth is another spelling of min_transition_growth. |
split_selection_rank | int | unset | Take the split at this rank instead of the best one. For experiments on split selection. |
abstract_plan | {backward_shortest_path, stable_astar} | backward_shortest_path | How the abstract plan is found in each iteration. |
flaw_candidates | {general, desired_region} | general | Which flaws are considered candidates for a split. |
cartesian_collection, cartesian_abstraction_collection
Several Cartesian abstractions. It accepts everything
cartesian accepts, plus these five.
Passing one of these five to cartesian is an unknown-option
error.
| Option | Type | Default | Meaning |
|---|---|---|---|
variants_per_goal | int | 1 | How many abstractions are built per goal fact. |
collection_strategy | {standard, complementary} | standard | As for the domain collection. |
progressive_goal_roots | bool | false | Build the goal-directed roots one after another instead of all at once. |
max_collection_size | int | 10000000 | Largest total number of abstract states over the collection. |
total_max_time | float, seconds; infinity means no limit | no limit | Budget for the whole collection. |
planforge --search 'astar(canonical(cartesian_collection(max_states=1000, max_collection_size=100000), construction_max_time=900))' \
domain.pddl problem.pddl
icaps26_cartesian
Cartesian abstractions under the split-selection policies of the ICAPS 2026 paper cited on the credits page. It exists so that those experiments can be reproduced from a configuration string.
It is a source only. It is not a heuristic name, so
astar(icaps26_cartesian()) reports an unknown heuristic. Write
astar(canonical(icaps26_cartesian())) instead.
All options must be named. It needs
--restrict-task. Its defaults are the
paper's settings rather than the cartesian source's defaults.
| Option | Type | Default | Meaning |
|---|---|---|---|
pick | {random, min_unwanted, max_unwanted} | max_unwanted | The paper's split selector. |
max_states | int | unbounded | Largest number of abstract states. |
max_time | float, seconds; infinity means no limit | 900 | Budget for the construction. |
random_seed | int | 2011 | Seed for pick=random. |
combine_labels | bool | false | Merge operators with the same abstract transitions. |
debug | bool | false | Print what the refinement loop is doing. |
pdb, numeric_pdb
Numeric pattern databases. Its options are those of
canonical_numeric_pdb. It needs
--restrict-task.
Errors from sources
A positional argument that is not one of the five source names is rejected, and the message lists the five.
$ planforge --search 'astar(max(nope(x=1)))' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "`max` accepts only domain(...), cartesian(...), cartesian_collection(...), icaps26_cartesian(...), and pdb(...) sources; got `nope(...)`" }
The wording depends on the combinator. canonical reports the same
mistake as an unknown option, because it accepts one option of its own and reads
anything that is not a source as that option. scp reports that its
options have to be named.
A source that produces no abstraction at all is an error rather than an empty combination, because an empty combination would quietly evaluate to zero.
Abstractions need a conjunctive goal
An abstract operator is derived from a task operator, and no task operator
writes a derived variable. Only the axioms do. So a goal on a
:derived predicate is a goal that an abstraction has nothing to
reach for.
Every abstraction family refuses such a task by name instead of inventing an interpretation of it. The message gives the variable and its axiom layer.
This also covers the goal-reachability predicate the translator introduces for a disjunctive, quantified or nested goal. Numeric comparisons are not affected. A comparison variable is derived too, but refining the operands of a comparison is exactly what the numeric refinement loop does.
blind, ff and lmcutnumeric solve those
tasks normally, because they test the goal in a state the axiom evaluator has
already closed. See the limits section.
How large an abstraction to build
max_abstraction_size is the option that decides how good an
abstraction heuristic is. Refinement stops when the next split would push the
abstraction over the bound, so the bound decides where the heuristic stops
improving.
The table below is three runs of astar(domain_abstraction()) on
the delivery fixture, changing only that option. The h column is the estimate for
the initial state, which the run prints as the first f value.
max_abstraction_size |
h for the initial state | Expanded | Total time |
|---|---|---|---|
| 100 000 (the default) | 13 | 6 657 | under 1 s |
| 1 000 000 | 16 | 2 803 | 3 s |
| 10 000 000 | 17 | 2 028 | 77 s |
The default is deliberately small so that construction finishes quickly. If an abstraction heuristic looks weak on your task, raise this bound before you conclude anything about the heuristic.
The cost of raising it is construction time and memory, and both grow faster than the estimate improves. From 100 000 to 10 000 000 is a hundredfold increase in the bound, and it buys four points of h and takes 77 seconds instead of under one.
For a collection, the bound to raise is
max_collection_size in the
collection options, and
total_max_time has to go up with it. It defaults to 10 seconds, and
generation stops on whichever limit comes first.
Optional build features
Both optional features are off by default, and a default build does not compile their dependencies at all. There is a continuous integration job whose only purpose is to prove that, by inspecting the build graph.
cplex
This feature adds the three LP-backed heuristics:
numeric_potential,
posthoc_optimization and
pot_da_ocp. It links against the native
IBM ILOG CPLEX 22.2 C interface.
CPLEX_ROOT=/path/to/CPLEX_Studio/cplex \
cargo build --release --features cplex
CPLEX_ROOT must contain
include/ilcplex/cplex.h and
lib/x86-64_linux/static_pic/libcplex.a. The planner links the
position-independent static library and uses one solver thread.
At heuristic start-up it checks that the licence in force will accept and solve a model with 1001 columns. A size-restricted licence, such as the Community Edition, is therefore rejected at the start of the run rather than in the middle of an experiment.
sgd
This feature adds the sgd engine and the
automatic differentiation backend it needs.
cargo build --release -p planforge --features sgd
The rest of the command line
planforge --help prints the authoritative list. This is what the
options mean.
| Option | Default | Meaning |
|---|---|---|
--search <SPEC> | astar(blind()) | The configuration described on this page. |
--max-time <DURATION> | no limit | Wall-clock budget. See the duration syntax below. |
--max-memory <SIZE> | no limit | Memory cap. See the size syntax below. |
--restrict-task | off | Convert the task to its restricted form. See below. |
--compact-numeric-states | off | Store numeric values in the state registry behind checked 32-bit interned identifiers. The values stay exact. States get smaller on a task with many numeric variables. |
--log-level <LEVEL> | info | How much the planner prints. One of off, error, warn, info, debug, trace. |
--portfolio | off | Run two searches in sequence instead of one. See below. Cannot be combined with --search, --max-time or --max-memory. |
<INPUT>... | required | One argument is a SAS+ file. Two arguments are a PDDL domain file and a PDDL problem file. |
Duration and size syntax
A duration is a number with an optional suffix. The suffixes are
ms, s, m and h. A number with
no suffix is read as seconds. So 45s, 30m,
1h and 45 are all valid.
A size is a number with an optional suffix. The suffixes are b,
k, m, g and t, and the
two-letter forms kb, mb, gb and
tb. They are powers of 1024. A number with no suffix is read as
bytes. So 4096M and 8G are both valid.
Both are enforced by the planner rather than by the operating system. On Linux the memory cap is applied to resident memory by a parent process, with a looser address-space limit behind it. The address-space limit alone is not enough, because the allocator reserves address space well ahead of the pages it actually uses.
Heuristic construction and search give back a fixed memory reserve as the cap approaches. That reserve is what buys enough room to print a report and exit, instead of being killed by an external Slurm or cgroup limit.
--restrict-task
Some heuristics are defined over a restricted task, in which the operands of a
comparison axiom are not themselves derived. --restrict-task
converts the task to that form. A task that is already restricted is left
alone.
The heuristics that need it are the pattern databases and
icaps26_cartesian. Asking for one
without the flag is an error that names the offending axiom, rather than a silent
conversion.
$ planforge --search 'astar(canonical(pdb()))' domain.pddl problem.pddl
Error: Custom { kind: Other, error: "task is not restricted: comparison axiom 0 has derived left operand 8; use `--restrict-task` or provide an already restricted task" }
--portfolio
Two searches in sequence rather than one. The first is
astar(lmcutnumeric()) under a tight budget. The second is
astar(canonical_domain_abstractions(...)) with a fixed set of
options.
Each stage is a separate child process. That is what makes the first stage's limits actually hold, and it means the second stage starts with a clean heap.
| Option | Default | Meaning |
|---|---|---|
--lmcut-time | 5m | Wall-clock budget for the first stage. |
--lmcut-memory | 7G | Memory cap for the first stage. |
--canonical-construction-time | 300 | Seconds given to building the abstraction collection in the second stage. |
--canonical-memory | 8G | Memory cap for the second stage. |
--canonical-time | no limit | Wall-clock cap for the second stage. When it is unset, that stage runs until it finds a plan, exceeds its memory cap, or is interrupted. |
All five require --portfolio.
Output and exit codes
A solved run prints the plan, then a block of statistics. It also writes the
plan to sas_plan in the working directory, one parenthesised
grounded action per line.
The statistics block has the same shape as Fast Downward's. It reports plan length and plan cost, then states expanded, reopened, evaluated and generated, then dead ends, then the number of registered states, then the search time.
Informational output goes to standard output. Warnings and errors go to standard error. So redirecting one of the two does not lose the other.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The planner ran to completion. This does not mean a plan was found. See the note below. |
| 1 | The run reported an error and stopped. An unknown heuristic, an unknown option, a bad option value, a missing input file, or a heuristic that needs a build feature this binary does not have. |
| 6 | The memory limit was reached. |
| 7 | The time limit was reached. This includes a limit reached during heuristic construction, before the search starts. |
| 101 | The process panicked. Usually a construct outside the supported PDDL fragment. Otherwise a bug. |
A run killed by a signal exits with 128 plus the signal number.
Do not test for exit code 0
Exit code 0 covers two different outcomes. One is that a plan was found. The
other is that the search finished, explored everything it could reach, and found
nothing. The second case prints No solution found and writes no
sas_plan, and it still exits with 0.
So a script must not read a zero status as success. Test for the plan file, or
test for Solution found! in the output.