User Guide · Core Concepts · Predicate DSL

Predicate DSL

Per-stage success is written in a small predicate language: three combinators, six atoms, no operator overloading. Each predicate compiles to a callable that is evaluated each step.

Grammar

The predicate language is intentionally minimal — it is YAML-friendly, parseable without a library, and trivially serialisable. The grammar in EBNF:

predicate  ::=  atom | combinator
combinator ::=  "and" "(" predicate {"," predicate} ")"
             |  "or"  "(" predicate {"," predicate} ")"
             |  "not" "(" predicate ")"
atom       ::=  name "(" args ")"
name       ::=  one of: grasped, joint_value, lifted, pose_near, placed_in, stacked_on
args       ::=  positional & keyword arguments per atom

The six atoms

AtomSignatureTrue when …
graspedgrasped(part)The gripper has closed on part and force-closure is stable.
joint_valuejoint_value(part, op, value)The named part's joint coordinate satisfies the comparator. op{">", ">=", "<", "<=", "≈"}.
liftedlifted(part, height_m=X)The part's base z has risen by at least X metres relative to its initial pose.
pose_nearpose_near(part, target_pose, tol)The part's pose is within tol (xyz + axis-angle) of the target pose.
placed_inplaced_in(a, b)Object a is being supported by container b (contact + downward force).
stacked_onstacked_on(a, b)Object a is resting on object b's top surface.

Tolerances default to physically reasonable values; override per-call if a task is unusually tight or loose.

Combinators

  • and(p1, p2, …) — all subpredicates must hold this step.
  • or(p1, p2, …) — at least one subpredicate holds this step.
  • not(p) — the subpredicate does not hold this step.

Combinators arbitrarily nest. Short-circuit evaluation means an and stops at the first false atom.

Worked examples

Engaged grasp:

grasped("cap")

Grasped and lifted:

and(grasped("cap"), lifted("cap", height_m: 0.05))

Door open beyond 60°:

joint_value("door", ">", 1.047)   # radians

Block placed in container OR on the table:

or(placed_in("block_a", "container"), stacked_on("block_a", "table"))

Released (post-engagement):

not(grasped("cap"))

How it compiles

compile_predicate(expr, env) returns a callable fn(state) -> bool. It walks the AST once at graph-load time, binds each atom to its evaluator (which knows about the asset and the env), and short-circuits combinators. The result is cheap to evaluate every step of the rollout.

from core.predicates import compile_predicate

fn = compile_predicate('and(grasped("cap"), lifted("cap", height_m: 0.05))', env)
while not fn(env.state):
    env.step(policy(env.state))

Extending the DSL

Adding a new atom is a single registration in core.predicates. Keep them small, observable, and asset-agnostic — the closed-set ethos applies here too. Compositional predicates that combine existing atoms should live in user YAML, not in the language.