mrlab-ai/PlanForge

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

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.

TypeAccepted values
booltrue or false.
intA non-negative decimal integer.
floatA decimal number, or the word infinity.
int or noneA non-negative decimal integer, or the word none.
stringAny bare scalar. It is converted to lower case.
{a, b, c}One of the listed words.
heuristicAnother 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.

OptionTypeDefaultMeaning
heuristicheuristicblind()The heuristic to evaluate. May be given positionally, which is the usual way.
mpdboolfalseRe-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.

OptionTypeDefaultMeaning
heuristicheuristicblind()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.

OptionTypeDefaultMeaning
fastheuristicrequiredEvaluated for every generated state. Should be the cheaper of the two.
slowheuristicrequiredEvaluated 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.

OptionTypeDefaultMeaning
horizonint, 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.
particlesint8How many independent candidate plans are optimised at once.
updatesint20000Gradient updates per horizon.
learning_rate, alias lrfloat0.04Step size of the optimiser.
cyclesint6Annealing cycles per horizon.
seedint1Seed for the random initialisation.
verify_periodint10How 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.

Every heuristic name, and where it is documented below.
NameAdmissibleNeedsWhat it is
blindyesThe baseline. Zero on a goal state, the cheapest action cost elsewhere.
lmcutnumericyesNumeric landmark cut.
ffnoRelaxed plan length, with a monotonic numeric relaxation.
domain_abstractionyesOne domain abstraction, refined by CEGAR.
cartesian_abstractionyesOne numeric Cartesian abstraction, refined by CEGAR.
max_cartesian_abstraction, canonical_cartesian_abstractionyesThe same abstraction, entered through a maximising or canonical wrapper.
canonical_domain_abstractionsyesA collection of domain abstractions, combined canonically.
multi_domain_abstractionsyesThe same collection, combined by maximum.
greedy_numeric_pdbyes--restrict-taskOne numeric pattern database over a greedily chosen pattern.
canonical_numeric_pdb, max_numeric_pdbyes--restrict-taskNumeric pattern databases over systematically enumerated patterns.
max, canonical, scp, cost_partitioningyesCombinators. They take abstraction sources and differ in how the parts are added up.
scp_online, scp_online_cartesianyesSaturated cost partitioning, with the option of computing it per state during the search.
fillscp, fill_scp, fillscp_cartesian, fill_scp_cartesianyesSaturated cost partitioning topped up with LM-cut over the leftover costs.
numeric_potentialyescplexPotential functions found by linear programming.
posthoc_optimization, phoyescplexPost-hoc optimisation over a collection of domain abstractions.
pot_da_ocpyescplexAn optimal cost partitioning over potential functions and one domain abstraction.
check_admissiblesame as its argumentA 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.

Default options unless the row says otherwise. Default build, so the three heuristics that need CPLEX are absent. Rows marked with an asterisk were run with --restrict-task.
HeuristicExpanded
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.

OptionTypeDefaultMeaning
ceiling_less_than_oneboolfalseRound a repetition count up to 1 when the computed count is below 1.
ignore_numericboolfalseDrop the numeric part of the task and compute propositional LM-cut only. Useful for telling apart a propositional and a numeric regression.
random_pcfboolfalseNot implemented. Setting it to true is an error: lmcutnumeric random_pcf=true is not implemented yet.
irmaxboolfalseA switch in the landmark cost computation. See the note below.
disable_maboolfalseTurn off the multiplier-aware numeric repetition count. See the note below.
use_second_order_simpleboolfalseA switch in the numeric repetition count. See the note below.
use_constant_assignmentboolfalseTake constant assignment effects into account when building the relaxation.
bound_iterationsint0Rounds of numeric bound propagation before the estimate is computed. 0 means no bounds are computed at all.
precisionfloat0.000001Tolerance for comparing numeric quantities. Must be non-negative.
epsilonfloat0.0Slack 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.

OptionTypeDefaultMeaning
max_pdb_statesint100000Largest table the generator will build. This is the size bound on the pattern.
numeric_firstbooltruePut numeric variables into the pattern before propositional ones.
random_seedint0Seed 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_levelHow the candidate variables are ordered before the pattern is grown.
exploration_heuristic{zero, blind, lmcut}blindHeuristic used inside the projected search that fills the table.
frontier_heuristic{zero, blind, lmcut}blindHeuristic used for states on the frontier of the table.
failed_lookup_heuristic{zero, blind, lmcut}blindEstimate 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.

OptionTypeDefaultMeaning
max_pdb_statesint50000Largest table per pattern.
max_pattern_sizeint2Largest number of variables in a pattern. Raising this grows the number of patterns quickly.
only_interesting_patternsbooltrueKeep 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}blindAs above.
frontier_heuristic{zero, blind, lmcut}blindAs above.
failed_lookup_heuristic{zero, blind, lmcut}blindAs 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.

OptionTypeDefaultMeaning
construction_max_timefloat, secondsno limitOne 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.

OptionTypeDefaultMeaning
onlinebooltrueCompute the partitioning per state during the search, instead of from a table built up front.
max_timefloat, seconds200.0Total budget for building the abstractions and the orders.
table_construction_max_timefloat, seconds30.0Budget for the lookup table alone.
max_sizeintunboundedLargest lookup table, counted in entries.
diversifyboolfalseKeep an order only when it improves the estimate on a sample state. Valid only with online=false.
samplesint1000How many sample states are drawn for diversification.
max_ordersintunboundedLargest number of orders kept.
intervalintunboundedHow often, in evaluations, a new order is computed in the online mode.
combine_labelsboolfalseMerge operators with identical transitions into one label before partitioning. This also sets the same option on the collection.
collectioncallsee belowThe collection options, as a nested call. The name of the call is ignored and only its arguments are used.
use_numeric_pdbsboolfalseAdd numeric pattern databases to the collection. Setting this needs --restrict-task.
max_pdb_statesint50000Size bound per pattern database, when they are used.
max_pattern_sizeint2Largest pattern, when pattern databases are used.
only_interesting_patternsbooltrueAs for the pattern databases above.
pdb_exploration_heuristic{zero, blind, lmcut}blindHeuristic inside the projected search.
pdb_frontier_heuristic{zero, blind, lmcut}zeroHeuristic on the frontier.
pdb_failed_lookup_heuristic{zero, blind, lmcut}zeroEstimate for a state not in the table.
scoring_function{max_heuristic, min_stolen_costs, max_heuristic_per_stolen_costs}max_heuristic_per_stolen_costsHow a candidate order is scored while orders are being built.
orders{greedy_orders, dynamic_greedy_orders, random_orders, diverse_orders}greedy_ordersHow the component orders are generated.
initial_order_generation_max_timefloat, seconds10.0Budget for producing the first order.
order_optimization_max_timefloat, seconds5.0Budget for improving an order after it has been produced.
saturator{all, perim, perimstar}allWhich costs a component is allowed to keep when it saturates.
residual_sweepsint0Extra passes over the components to spend costs that were left over.
random_seedint or none2011Seed for order generation and sampling. This also sets the same option on the collection.
partitioning{label, region}labelWhether 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.

OptionTypeDefaultMeaning
table_construction_max_timefloat, seconds30.0Budget for the lookup table.
combine_labelsboolfalseAs for scp_online. Also sets the collection's option.
collectioncallsee collection optionsThe collection, as a nested call.
scoring_function{max_heuristic, min_stolen_costs, max_heuristic_per_stolen_costs}max_heuristic_per_stolen_costsAs for scp_online.
orders{greedy_orders, dynamic_greedy_orders, random_orders, diverse_orders}greedy_ordersAs for scp_online.
order_optimization_max_timefloat, seconds5.0As for scp_online.
saturator{all, perim, perimstar}allAs for scp_online.
random_seedint or none2011As for scp_online. Also sets the collection's option.
partitioning{label, region}labelAs for scp_online.
lmcutcallsee lmcutnumericOptions 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" }
Options of numeric_potential, in positional order.
OptionTypeDefaultMeaning
opt{initial_state, all_states, samples, diverse_samples}initial_stateWhich objective the linear program maximises.
num_samplesint1000Sample states drawn for the sampled objectives. Must be at least 1.
num_heuristicsint4Functions in the diverse portfolio. Must be at least 1.
max_diverse_generation_timefloat, seconds30.0Budget for building the diverse portfolio.
include_initial_state_potentialbooltrueAdd the initial-state function to a portfolio.
include_all_states_potentialboolfalseAdd the all-states function to a portfolio.
diverse_fallback{largest_gap, random}largest_gapHow the next sample is picked when diversification stalls.
raysint0Unbounded directions to look for. 0 disables the search for them.
max_ray_generation_timefloat, seconds30.0Budget for that search.
ray_epsilonfloat0.000001Tolerance used while looking for them.
ray_certificate_filestringnumeric_potential_ray_certificate.jsonWhere a found direction is written. The parser lower-cases this value, so a path with capital letters will not survive.
max_potentialfloat100000000Upper bound on any single weight. It keeps the linear program bounded.
ignore_numeric_variablesboolfalseGive weights to facts only.
bounds{none, monotone, aibr, all}noneWhich analysis supplies bounds on the numeric variables.
simple_action_boundsboolfalseUse a cheaper per-action bound.
goal_conditionedbooltrueLet a function be specialised to the goal it is asked about.
goal_cost_partitioningbooltruePartition costs between goals rather than solving one program for all of them.
num_goal_cost_partitionsint4Partitions to build. Must be at least 1.
num_goal_conditioned_heuristicsint1Goal-conditioned functions to build. Must be at least 1.
num_goal_conditioned_samplesint100Samples used for them.
max_conditioned_generation_timefloat, seconds120.0Budget for building them.
max_online_heuristicsint100Largest number of functions kept while the search runs.
online_reoptimization_intervalint50Evaluations between two re-optimisations. Must be at least 1.
max_consecutive_online_missesint20Consecutive misses before re-optimising.
max_online_missesint12Total misses before re-optimising.
max_online_lp_solvesint1000Cap on linear programs solved during the search.
invalidate_online_cache_on_growthboolfalseDrop cached estimates when a new function is added.
online_reoptimization_on_new_states_onlyboolfalseRe-optimise only for states never seen before.
cache_estimatesbooltrueRemember the estimate for a state.
precisionfloat0.000001Tolerance for comparing numeric quantities.
epsilonfloat0.0Slack for strict inequalities.
dump_lpboolfalseWrite the linear program to a file for inspection.
validate_dualityboolfalseCheck 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.

OptionTypeDefaultMeaning
abstractioncallrequiredMust be written as domain_abstraction_cegar(...) or domain_abstraction(...). Its arguments are the single CEGAR abstraction options, plus the two rows below.
nonnegativeboolfalseRequire every partitioned cost to be at least zero.

Two options are read inside the abstraction= call before the CEGAR options are applied.

OptionTypeDefaultMeaning
record_transition_systemboolfalseKeep 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_transitionsint100000Cap 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.

OptionTypeDefaultMeaning
max_abstraction_sizeint100000Largest 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_iterationsintunboundedLargest number of refinement steps. Must be greater than zero.
max_timefloat, seconds; infinity means no limitno limitWall-clock budget for building the abstraction.
use_wildcard_plansboolfalseLet 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_labelsbooltrueMerge operators that induce the same abstract transitions into one label. This shrinks the transition system.
random_seedint or none2011Seed for the random choices in flaw selection. none leaves the generator unseeded, so the abstraction differs between runs.
flaw_treatmentsee belowrandom_single_atomHow a flaw is turned into one or more splits.
flaw_kindsee belowprogressionHow flaws are looked for along the abstract plan.
init_split_methodsee belowinit_valueHow 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.

In positional order.
OptionTypeDefaultMeaning
max_abstraction_sizeint1000000Largest number of abstract states in one member of the collection.
max_collection_sizeint10000000Largest total number of abstract states over the whole collection. Generation stops when this is reached.
abstraction_generation_max_timefloat, secondsinfinityBudget for one member.
total_max_timefloat, seconds10.0Budget 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_limitfloat, seconds20.0How long generation may run without producing a useful new member before it reacts.
blacklist_trigger_percentagefloat0.75Fraction of total_max_time after which blacklisting may start.
enable_blacklist_on_stagnationbooltrueOn stagnation, forbid some variables so that the next member is forced to differ from the ones already found.
blacklist_option{goals, non_goals, all}allWhich variables may be blacklisted.
init_split_candidates{goals, non_goals, all}allWhich variables may be used for the first split of a member.
init_split_quantity{none, single, all}singleHow many variables are split first. none forces init_split_method=identity.
random_seedint or none2011Seed for every random choice in generation.
debugboolfalsePrint what the generator is doing. This option exists here and not on a single abstraction.
use_wildcard_plansboolfalseAs in the single-abstraction table.
combine_labelsbooltrueAs in the single-abstraction table.
flaw_kindsee the enumerationexecute_entire_planAs in the single-abstraction table, with a different default.
flaw_treatmentsee the enumerationrandom_single_atomAs in the single-abstraction table.
init_split_methodsee the enumerationinit_valueAs in the single-abstraction table.
numeric_split_strategy{standard, exclusion}standardHow a numeric split value is chosen. exclusion parses and is then refused, because it is not implemented in this port.
collection_strategy{standard, complementary}standardcomplementary builds members that are meant to cover different parts of the task.
interleave_split_directionsboolfalseAlternate 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}defaultWhere 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.

The first four rows are the positional order.
OptionTypeDefaultMeaning
max_statesint10000Largest number of abstract states.
max_timefloat, seconds; infinity means no limitno limitBudget for the construction.
combine_labelsboolfalseMerge operators with the same abstract transitions.
debugboolfalsePrint what the refinement loop is doing.
random_seedintunseededSeed for random split selection. The word none is not accepted here.
flaw_kind{progression, execute_entire_plan}progressionOnly these two values are accepted. Any other value of the wider flaw-kind enumeration is refused by name.
refinement_direction{progression, regression, target_centered}progressionWhich 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_growthWhich split is chosen when several are possible. min_growth is another spelling of min_transition_growth.
split_selection_rankintunsetTake the split at this rank instead of the best one. For experiments on split selection.
abstract_plan{backward_shortest_path, stable_astar}backward_shortest_pathHow the abstract plan is found in each iteration.
flaw_candidates{general, desired_region}generalWhich 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.

OptionTypeDefaultMeaning
variants_per_goalint1How many abstractions are built per goal fact.
collection_strategy{standard, complementary}standardAs for the domain collection.
progressive_goal_rootsboolfalseBuild the goal-directed roots one after another instead of all at once.
max_collection_sizeint10000000Largest total number of abstract states over the collection.
total_max_timefloat, seconds; infinity means no limitno limitBudget 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.

OptionTypeDefaultMeaning
pick{random, min_unwanted, max_unwanted}max_unwantedThe paper's split selector.
max_statesintunboundedLargest number of abstract states.
max_timefloat, seconds; infinity means no limit900Budget for the construction.
random_seedint2011Seed for pick=random.
combine_labelsboolfalseMerge operators with the same abstract transitions.
debugboolfalsePrint 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.

Delivery fixture, cheapest cost 22, so a perfect heuristic would report h = 22. Total time is wall-clock time for the whole run, and almost all of it is construction.
max_abstraction_size h for the initial state Expanded Total time
100 000 (the default)136 657under 1 s
1 000 000162 8033 s
10 000 000172 02877 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.

OptionDefaultMeaning
--search <SPEC>astar(blind())The configuration described on this page.
--max-time <DURATION>no limitWall-clock budget. See the duration syntax below.
--max-memory <SIZE>no limitMemory cap. See the size syntax below.
--restrict-taskoffConvert the task to its restricted form. See below.
--compact-numeric-statesoffStore 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>infoHow much the planner prints. One of off, error, warn, info, debug, trace.
--portfoliooffRun two searches in sequence instead of one. See below. Cannot be combined with --search, --max-time or --max-memory.
<INPUT>...requiredOne 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.

OptionDefaultMeaning
--lmcut-time5mWall-clock budget for the first stage.
--lmcut-memory7GMemory cap for the first stage.
--canonical-construction-time300Seconds given to building the abstraction collection in the second stage.
--canonical-memory8GMemory cap for the second stage.
--canonical-timeno limitWall-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

CodeMeaning
0The planner ran to completion. This does not mean a plan was found. See the note below.
1The 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.
6The memory limit was reached.
7The time limit was reached. This includes a limit reached during heuristic construction, before the search starts.
101The 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.