Skip to content

Simulation and Visualization

Two runnable lanes with visible artifacts: a closed-loop simulator where the environment and policy are both Flows, and a graph renderer that validates the runtime IR and writes an inspectable HTML view. Both run without a real simulator or GPU.

The Lift task is a feedback loop: the environment produces a LiftState, the policy consumes it and returns a LiftAction, which feeds back into the environment. LiftEnvFlow runs a deterministic mock so the smoke test needs no robosuite install.

from retriever.flow import Flow, Latest, Pipeline, Rate, Trigger, io

@io
class LiftAction:
    dz: float | None = None
    grip: float | None = None

@io
class LiftState:
    step: int | None = None
    source: str | None = None
    object_height: float | None = None
    gripper_z: float | None = None
    reward: float | None = None
    done: bool | None = None

class LiftEnvFlow(Flow[LiftAction, LiftState]):
    """robosuite Lift, or a deterministic mock when robosuite is absent."""
    def step(self, action: LiftAction | None) -> LiftState:
        self.step_idx += 1
        dz   = 0.0 if action is None or action.dz   is None else float(action.dz)
        grip = 1.0 if action is None or action.grip is None else float(action.grip)
        return self._step_mock(dz=dz, grip=grip)   # or _step_robosuite in real mode

class HeuristicLiftPolicy(Flow[LiftState, LiftAction]):
    """Approach, close the gripper, then lift."""
    def step(self, state: LiftState | None) -> LiftAction:
        if state is None or state.object_height is None:
            return LiftAction(dz=-0.4, grip=1.0)
        if state.done or state.object_height >= self.target_height:
            return LiftAction(dz=0.0, grip=-1.0)
        if state.gripper_z > state.object_height + 0.08:
            return LiftAction(dz=-0.5, grip=1.0)
        return LiftAction(dz=0.6, grip=-1.0)

The feedback edge is explicit: env → policy and policy → env. The environment ticks at env_hz, the policy at policy_hz, and the printer wakes on each new step.

pipe = Pipeline("robosuite_lift_demo")
with pipe:
    env     = LiftEnvFlow(mode="mock", ...)          @ Rate(hz=20)
    policy  = HeuristicLiftPolicy(target_height=1.05) @ Rate(hz=5)
    printer = LiftPrinter(print_every=2)              @ Trigger("step")
    pipe.connect(env, policy, sync=Latest())
    pipe.connect(policy, env, sync=Latest())          # feedback loop
    pipe.connect(env, printer, sync=Latest())
pixi run demo-robosuite-mock

Real output — the mock is deterministic, so object_z rises once the gripper closes near the block and reward tracks the lift:

[mock step=002] object_z=0.820 gripper_z=0.940 reward=0.000 done=False
[mock step=004] object_z=0.820 gripper_z=0.900 reward=0.000 done=False
[mock step=006] object_z=0.841 gripper_z=0.904 reward=0.021 done=False
[mock step=008] object_z=0.883 gripper_z=0.952 reward=0.063 done=False
[mock step=010] object_z=0.925 gripper_z=1.000 reward=0.105 done=False
[mock step=012] object_z=0.967 gripper_z=1.048 reward=0.147 done=False

Swap mode="robosuite" (after installing the optional dependency) and the same graph drives the real simulator; LiftState.source reports which backend ran.

pipe.validate() returns the runtime IR; generate_ascii_graph and save_interactive_html turn it into an inspectable graph — including feedback edges, clocks, and sync policies.

from retriever.flow import Latest, Pipeline, Rate, Trigger, io
from retriever.ir.viz import generate_ascii_graph, save_interactive_html

pipe = Pipeline("visualization_demo")
env       = DummyNode() @ Rate(hz=10.0)
perception = DummyNode() @ Trigger("obs")
planner    = DummyNode() @ Trigger("state")
executor   = DummyNode() @ Trigger("plan", "state")
pipe.connect(env, perception, map={"data": "data"}, sync=Latest())
pipe.connect(perception, planner, map={"data": "data"}, sync=Latest())
pipe.connect(planner, executor, map={"data": "data"}, sync=Latest())
pipe.connect(perception, executor, map={"data": "data"}, sync=Latest())
pipe.connect(executor, env, map={"data": "data"}, sync=Latest())   # closes the loop

ir = pipe.validate()
print(generate_ascii_graph(ir))
save_interactive_html(ir, "out/golden_retriever_closed_loop_viz.html")
pixi run demo-pipeline-html-viz

Real output — the ASCII graph shows each node’s clock and the detected cycle, and the command writes a self-contained HTML artifact:

Pipeline: visualization_demo
============================
NOTE: Cycle detected in graph topology (expected for closed-loop systems).
[DummyNode] <Rate(10.0 Hz)>
   --(data -> data: Latest)--> [DummyNode]

[DummyNode] <Trigger(on ['obs'])>
   --(data -> data: Latest)--> [DummyNode]
   --(data -> _fanin/DummyNode__2/data: Latest)--> [DummyNode]

[DummyNode] <Trigger(on ['state'])>
   --(data -> _fanin/DummyNode__3/data: Latest)--> [DummyNode]

[DummyNode] <Trigger(on ['plan', 'state'])>
   --(data -> data: Latest)--> [DummyNode] (feedback/cycle)

HTML visualization written to out/golden_retriever_closed_loop_viz.html

The generated graph, embedded from the artifact:

Run the visual lane from lightest to heaviest so each layer is verified before the next:

  1. HTML graph proofpixi run demo-pipeline-html-viz renders the graph structure with no simulator or model.
  2. Mock robosuitepixi run demo-robosuite-mock runs the env↔policy loop deterministically, without a robosuite install.
  3. Real robosuite / Rerun — swap mode="robosuite" (optional dependency) only after the mock trace looks right.

Making the environment and policy ordinary Flows keeps timing and handoffs visible in the graph instead of hidden in callbacks: the feedback edge, the two clock rates, and the sync policies are all inspectable IR. That is why the same graph renders to ASCII/HTML and runs against either a mock or a real backend.

Heavier visual lanes reuse the same runtime once their dependencies are present:

pixi run -e twist2 demo-twist2-rerun   # MuJoCo + Rerun viewer (optional, heavy)

Source: examples/advanced/robosuite_lift/app.py and examples/experimental/visualization/visualize_pipeline.py.