Hierarchical, Symbolic, Language, and Multi-Agent Planning › Worked example: symbolic planning bound to options

Michael BrenndoerferJuly 12, 20263 min read
World Models HandbookAll chapters
Part 7 of 12Planning and Agency
Chapter 42Hierarchical, Symbolic, Language, and Multi-Agent Planning › Worked example: symbolic planning bound to options
Reading section 7 of 13 · Worked example: symbolic planning bound to options · 3 minSection 7 of 13 · 3 min

Book contents

Section 7 of 13 · 3 min read

Hierarchical, Symbolic, Language, and Multi-Agent Planning › Worked example: symbolic planning bound to optionsOne section · Sections in this chapter7 / 13 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Now the symbolic layer will choose the sequence. A single abstraction function maps an environment state into mutually exclusive location predicates plus key and door facts. Every symbolic operator has an option with matching initiation and effects.

In[14]:
Code
def abstract_state(state):
    if state.cell == START:
        location = "at_start"
    elif state.cell == KEY:
        location = "at_key"
    elif state.cell == DOOR_APPROACH:
        location = "at_door_approach"
    elif state.cell == GOAL:
        location = "at_goal"
    else:
        raise ValueError(f"No symbolic location for {state.cell}")
    return frozenset(
        {
            location,
            "has_key" if state.has_key else "no_key",
            "door_open" if state.door_open else "door_closed",
        }
    )


OPERATORS = [
    (
        "navigate_to_key",
        frozenset({"at_start", "no_key", "door_closed"}),
        frozenset({"at_key", "has_key"}),
        frozenset({"at_start", "no_key"}),
    ),
    (
        "navigate_to_door",
        frozenset({"at_key", "has_key", "door_closed"}),
        frozenset({"at_door_approach"}),
        frozenset({"at_key"}),
    ),
    (
        "unlock_door",
        frozenset({"at_door_approach", "has_key", "door_closed"}),
        frozenset({"door_open"}),
        frozenset({"door_closed"}),
    ),
    (
        "navigate_to_goal",
        frozenset({"at_door_approach", "has_key", "door_open"}),
        frozenset({"at_goal"}),
        frozenset({"at_door_approach"}),
    ),
]
OP_BY_NAME = {op[0]: op for op in OPERATORS}
OPERATOR_TO_OPTION = {
    "navigate_to_key": "go_to_key",
    "navigate_to_door": "go_to_door",
    "unlock_door": "unlock_door",
    "navigate_to_goal": "go_to_goal",
}
GOAL_PREDS = frozenset({"at_goal", "has_key", "door_open"})


def applicable(state, op):
    return op[1].issubset(state)


def apply(state, op):
    return frozenset((state - op[3]) | op[2])


def plan(initial, goal, operators, max_depth=8):
    queue = deque([(initial, [])])
    seen = {initial}
    while queue:
        state, sequence = queue.popleft()
        if goal.issubset(state):
            return sequence
        if len(sequence) >= max_depth:
            continue
        for op in operators:
            if applicable(state, op):
                next_state = apply(state, op)
                if next_state not in seen:
                    seen.add(next_state)
                    queue.append((next_state, sequence + [op[0]]))
    return None


def execute_symbolic_plan(operator_names, initial):
    env_state = initial
    symbolic = abstract_state(env_state)
    trajectory = [env_state.cell]
    execution_log = []
    for op_name in operator_names:
        op = OP_BY_NAME[op_name]
        if not applicable(symbolic, op):
            raise RuntimeError(f"Symbolic precondition failed for {op_name}")
        result = run_option(OPERATOR_TO_OPTION[op_name], env_state)
        if not result.succeeded:
            raise RuntimeError(f"Option failed for {op_name}: {result.reason}")
        predicted_symbolic = apply(symbolic, op)
        actual_symbolic = abstract_state(result.state)
        if predicted_symbolic != actual_symbolic:
            raise RuntimeError(f"Effect mismatch for {op_name}")
        env_state, symbolic = result.state, actual_symbolic
        trajectory.extend(result.cells[1:])
        execution_log.append((op_name, result.steps))
    return env_state, symbolic, trajectory, execution_log
Out[15]:
Console
Initial symbolic state: ['at_start', 'door_closed', 'no_key']
Plan selected by symbolic BFS: ['navigate_to_key', 'navigate_to_door', 'unlock_door', 'navigate_to_goal']
Operator-to-option execution: [('navigate_to_key', 4), ('navigate_to_door', 2), ('unlock_door', 1), ('navigate_to_goal', 3)]
Physical execution succeeded: True

The planner sees predicates and operators, not coordinates. The executor then checks each precondition, dispatches the bound option, and compares predicted effects with the abstraction of the resulting environment state. Symbolic search remains independent of grid geometry only while the grounded operator set is fixed and valid; a new map can change option feasibility or grounding cost even if the abstract sequence remains reusable.