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
| Atom | Signature | True when … |
|---|---|---|
grasped | grasped(part) | The gripper has closed on part and force-closure is stable. |
joint_value | joint_value(part, op, value) | The named part's joint coordinate satisfies the comparator. op ∈ {">", ">=", "<", "<=", "≈"}. |
lifted | lifted(part, height_m=X) | The part's base z has risen by at least X metres relative to its initial pose. |
pose_near | pose_near(part, target_pose, tol) | The part's pose is within tol (xyz + axis-angle) of the target pose. |
placed_in | placed_in(a, b) | Object a is being supported by container b (contact + downward force). |
stacked_on | stacked_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.