Skip to content

Commit 51e4f29

Browse files
committed
cosmetic changes
1 parent 970114a commit 51e4f29

12 files changed

Lines changed: 344 additions & 33 deletions

File tree

judo/app/dora/controller_node.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from judo.app.structs import MujocoState
1313
from judo.controller import Controller, make_controller
14-
from judo.tasks import get_task_registration
1514

1615

1716
class ControllerNode(DoraNode):
@@ -50,13 +49,11 @@ def __init__(
5049

5150
def _build_controller(self, task_name: str, optimizer_name: str) -> Controller:
5251
"""Build controller using the task's registered rollout backend."""
53-
rollout_backend = get_task_registration(task_name).rollout_backend
5452
return self._make_controller_fn(
5553
init_task=task_name,
5654
init_optimizer=optimizer_name,
5755
task_registration_cfg=self._task_registration_cfg,
5856
optimizer_registration_cfg=self._optimizer_registration_cfg,
59-
rollout_backend=rollout_backend,
6057
)
6158

6259
def _current_optimizer_name(self) -> str:

judo/app/dora/simulation.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright (c) 2025 Robotics and AI Institute LLC. All rights reserved.
2+
3+
import time
4+
import warnings
5+
from typing import Callable
6+
7+
from dora_utils.dataclasses import from_arrow, to_arrow
8+
from dora_utils.node import DoraNode, on_event
9+
from omegaconf import DictConfig
10+
11+
from judo.app.structs import SplineData
12+
from judo.simulation.base import Simulation
13+
from judo.tasks import get_registered_tasks
14+
15+
16+
class SimulationNode(DoraNode):
17+
"""The simulation node."""
18+
19+
def __init__(
20+
self,
21+
node_id: str = "simulation",
22+
init_task: str = "cylinder_push",
23+
max_workers: int | None = None,
24+
task_registration_cfg: DictConfig | None = None,
25+
simulation_backend: str = "mujoco",
26+
custom_backends: dict[str, type[Simulation]] | None = None,
27+
) -> None:
28+
"""Initialize the simulation node."""
29+
super().__init__(node_id=node_id, max_workers=max_workers)
30+
self._simulation_backend = simulation_backend
31+
self._task_registration_cfg = task_registration_cfg
32+
self._custom_backends = custom_backends or {}
33+
self._init_sim(init_task)
34+
self.control_spline: Callable | None = None
35+
self.write_states()
36+
37+
def _resolve_backend(self, backend: str) -> type:
38+
"""Resolve a simulation backend class by name, checking custom backends first."""
39+
if backend in self._custom_backends:
40+
return self._custom_backends[backend]
41+
from judo.simulation import get_simulation_backend # noqa: PLC0415, I001
42+
43+
return get_simulation_backend(backend)
44+
45+
def _init_sim(self, task_name: str) -> None:
46+
"""Initialize simulation, auto-upgrading to policy backend if needed."""
47+
task_entry = get_registered_tasks().get(task_name)
48+
if task_entry is None:
49+
raise ValueError(f"Task {task_name} not found in task registry.")
50+
51+
task_cls, _ = task_entry
52+
backend = getattr(task_cls, "default_backend", None) or self._simulation_backend
53+
54+
sim_backend_cls = self._resolve_backend(backend)
55+
self.sim = sim_backend_cls(init_task=task_name, task_registration_cfg=self._task_registration_cfg)
56+
57+
# Auto-upgrade to policy backend if task requires locomotion policy
58+
if backend == "mujoco" and self.sim.task.uses_locomotion_policy:
59+
sim_backend_cls = self._resolve_backend("mujoco_policy")
60+
self.sim = sim_backend_cls(init_task=task_name, task_registration_cfg=self._task_registration_cfg)
61+
62+
@on_event("INPUT", "task")
63+
def update_task(self, event: dict) -> None:
64+
"""Event handler for processing task updates."""
65+
new_task = event["value"].to_numpy(zero_copy_only=False)[0]
66+
self._init_sim(new_task)
67+
self.control_spline = None # Clear stale spline
68+
69+
def spin(self) -> None:
70+
"""Spin logic for the simulation node."""
71+
try:
72+
while True:
73+
start_time = time.time()
74+
self.parse_messages()
75+
76+
if self.control_spline is not None:
77+
command = self.control_spline(self.sim.task.data.time)
78+
if command.shape[-1] == self.sim.task.nu:
79+
self.sim.step(command)
80+
else:
81+
warnings.warn(
82+
f"Control command has wrong number of dimensions! Expected {self.sim.task.nu}, got {command.shape[-1]}",
83+
stacklevel=2,
84+
)
85+
86+
self.write_states()
87+
88+
# Force simulation node to run at fixed rate specified by simulation timestep (specified in the model).
89+
dt_des = self.sim.timestep
90+
dt_elapsed = time.time() - start_time
91+
if dt_elapsed < dt_des:
92+
time.sleep(dt_des - dt_elapsed)
93+
else:
94+
warnings.warn(
95+
f"Sim step {dt_elapsed:.3f} longer than desired step {dt_des:.3f}!",
96+
stacklevel=2,
97+
)
98+
except KeyboardInterrupt:
99+
pass
100+
101+
def write_states(self) -> None:
102+
"""Reads data from simulation and writes to output topic."""
103+
arr, metadata = to_arrow(self.sim.sim_state)
104+
self.node.send_output("states", arr, metadata)
105+
arr, metadata = to_arrow(self.sim.render_pose)
106+
self.node.send_output("render_pose", arr, metadata)
107+
108+
@on_event("INPUT", "sim_pause")
109+
def set_paused_status(self, event: dict) -> None:
110+
"""Event handler for processing pause status updates."""
111+
self.sim.pause()
112+
113+
@on_event("INPUT", "task_reset")
114+
def reset_task(self, event: dict) -> None:
115+
"""Resets the task."""
116+
self.sim.task.reset()
117+
118+
@on_event("INPUT", "controls")
119+
def update_control(self, event: dict) -> None:
120+
"""Event handler for processing controls received from controller node."""
121+
spline_data = from_arrow(event["value"], event["metadata"], SplineData)
122+
self.control_spline = spline_data.spline()

judo/app/dora/simulation_node.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ def write_states(self) -> None:
101101
"""Reads data from simulation and writes to output topic."""
102102
arr, metadata = to_arrow(self.sim.sim_state)
103103
self.node.send_output("states", arr, metadata)
104-
arr, metadata = to_arrow(self.sim.world_state)
105-
self.node.send_output("world_states", arr, metadata)
104+
arr, metadata = to_arrow(self.sim.render_pose)
105+
self.node.send_output("render_pose", arr, metadata)
106106

107107
@on_event("INPUT", "sim_pause")
108108
def set_paused_status(self, event: dict) -> None:

judo/app/dora/visualization.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Copyright (c) 2025 Robotics and AI Institute LLC. All rights reserved.
2+
3+
import warnings
4+
5+
import pyarrow as pa
6+
from dora_utils.dataclasses import from_arrow, to_arrow
7+
from dora_utils.node import DoraNode, on_event
8+
from omegaconf import DictConfig
9+
from viser import GuiFolderHandle, GuiImageHandle, GuiInputHandle, IcosphereHandle, MeshHandle
10+
11+
from judo.app.structs import RenderPose
12+
from judo.tasks import Task, TaskConfig
13+
from judo.visualizers.visualizer import Visualizer
14+
15+
ElementType = GuiImageHandle | GuiInputHandle | GuiFolderHandle | MeshHandle | IcosphereHandle
16+
17+
18+
class VisualizationNode(DoraNode):
19+
"""The visualization node."""
20+
21+
def __init__(
22+
self,
23+
node_id: str = "visualization",
24+
max_workers: int | None = None,
25+
init_task: str = "cylinder_push",
26+
init_optimizer: str = "cem",
27+
task_registration_cfg: DictConfig | None = None,
28+
optimizer_registration_cfg: DictConfig | None = None,
29+
controller_override_cfg: DictConfig | None = None,
30+
optimizer_override_cfg: DictConfig | None = None,
31+
sim_pause_button: bool = True,
32+
geom_exclude_substring: str = "collision",
33+
available_tasks: dict[str, tuple[type[Task], type[TaskConfig]]] | None = None,
34+
) -> None:
35+
"""Initialize the visualization node."""
36+
super().__init__(node_id=node_id, max_workers=max_workers)
37+
self.visualizer = Visualizer(
38+
init_task=init_task,
39+
init_optimizer=init_optimizer,
40+
task_registration_cfg=task_registration_cfg,
41+
optimizer_registration_cfg=optimizer_registration_cfg,
42+
controller_override_cfg=controller_override_cfg,
43+
optimizer_override_cfg=optimizer_override_cfg,
44+
sim_pause_button=sim_pause_button,
45+
geom_exclude_substring=geom_exclude_substring,
46+
available_tasks=available_tasks,
47+
)
48+
49+
def write_sim_pause(self) -> None:
50+
"""Write the sim pause signal to the GUI."""
51+
with self.visualizer.sim_pause_lock:
52+
self.node.send_output("sim_pause", pa.array([1])) # dummy value
53+
self.visualizer.sim_pause_updated.clear()
54+
55+
def write_task(self) -> None:
56+
"""Write the task name to the GUI."""
57+
with self.visualizer.task_lock:
58+
self.node.send_output("task", pa.array([self.visualizer.task_name]))
59+
self.visualizer.task_updated.clear()
60+
61+
def write_task_reset(self) -> None:
62+
"""Write the task reset signal to the GUI."""
63+
with self.visualizer.task_lock:
64+
self.node.send_output("task_reset", pa.array([1])) # dummy value
65+
self.visualizer.task_reset_updated.clear()
66+
67+
def write_optimizer(self) -> None:
68+
"""Write the optimizer name to the GUI."""
69+
with self.visualizer.optimizer_lock:
70+
self.node.send_output("optimizer", pa.array([self.visualizer.optimizer_name]))
71+
self.visualizer.optimizer_updated.clear()
72+
73+
def write_controller_config(self) -> None:
74+
"""Write the controller config to the GUI."""
75+
with self.visualizer.controller_config_lock:
76+
self.node.send_output("controller_config", *to_arrow(self.visualizer.controller_config))
77+
self.visualizer.controller_config_updated.clear()
78+
79+
def write_optimizer_config(self) -> None:
80+
"""Write the optimizer config to the GUI."""
81+
with self.visualizer.optimizer_config_lock:
82+
self.node.send_output("optimizer_config", *to_arrow(self.visualizer.optimizer_config))
83+
self.visualizer.optimizer_config_updated.clear()
84+
85+
def write_task_config(self) -> None:
86+
"""Write the task config to the GUI."""
87+
with self.visualizer.task_config_lock:
88+
self.node.send_output("task_config", *to_arrow(self.visualizer.task_config))
89+
self.visualizer.task_config_updated.clear()
90+
91+
@on_event("INPUT", "render_pose")
92+
def update_states(self, event: dict) -> None:
93+
"""Callback to update states on receiving a new state measurement."""
94+
if self.visualizer.controller_config.spline_order == "cubic" and self.visualizer.optimizer_config.num_nodes < 4:
95+
warnings.warn("Cubic splines require at least 4 nodes. Setting num_nodes=4.", stacklevel=2)
96+
for e in self.visualizer.gui_elements["optimizer_params"]:
97+
if e.label == "num_nodes":
98+
e.value = 4
99+
break
100+
self.visualizer.optimizer_config_updated.set()
101+
102+
# TODO: change the mujoco state
103+
render_pose_msg = from_arrow(event["value"], event["metadata"], RenderPose)
104+
try:
105+
with self.visualizer.task_lock:
106+
self.visualizer.data.xpos[:] = render_pose_msg.xpos
107+
self.visualizer.data.xquat[:] = render_pose_msg.xquat
108+
self.visualizer.viser_model.set_data(self.visualizer.data)
109+
except ValueError:
110+
# we're switching tasks and the new task has a different number of xpos/xquat
111+
return
112+
113+
@on_event("INPUT", "traces")
114+
def update_traces(self, event: dict) -> None:
115+
"""Callback to update traces on receiving a new trace measurement."""
116+
traces_flat = event["value"].to_numpy()
117+
all_traces_rollout_size = int(event["metadata"]["all_traces_rollout_size"])
118+
shape = event["metadata"]["shape"]
119+
traces = traces_flat.reshape(*shape)
120+
with self.visualizer.task_lock:
121+
self.visualizer.viser_model.set_traces(traces, all_traces_rollout_size)
122+
123+
@on_event("INPUT", "plan_time")
124+
def update_plan_time(self, event: dict) -> None:
125+
"""Callback to update plan time on receiving a new plan time measurement."""
126+
plan_time_s = event["value"].to_numpy(zero_copy_only=False)[0]
127+
# GUI element might not exist during task switch
128+
if "plan_time_display" in self.visualizer.gui_elements:
129+
self.visualizer.gui_elements["plan_time_display"].value = plan_time_s * 1000 # ms
130+
131+
def spin(self) -> None:
132+
"""Spin logic for the visualization node."""
133+
try:
134+
for event in self.node:
135+
if self.visualizer.sim_pause_updated.is_set():
136+
self.write_sim_pause()
137+
if self.visualizer.task_updated.is_set():
138+
self.write_task()
139+
if self.visualizer.task_reset_updated.is_set():
140+
self.write_task_reset()
141+
if self.visualizer.optimizer_updated.is_set():
142+
self.write_optimizer()
143+
if self.visualizer.controller_config_updated.is_set():
144+
self.write_controller_config()
145+
if self.visualizer.optimizer_config_updated.is_set():
146+
self.write_optimizer_config()
147+
if self.visualizer.task_config_updated.is_set():
148+
self.write_task_config()
149+
150+
self.handle(event)
151+
except KeyboardInterrupt:
152+
pass
153+
154+
def cleanup(self) -> None:
155+
"""Cleanup the visualization node."""
156+
self.visualizer.cleanup()
157+
super().cleanup()

judo/app/dora/visualization_node.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from omegaconf import DictConfig
99
from viser import GuiFolderHandle, GuiImageHandle, GuiInputHandle, IcosphereHandle, MeshHandle
1010

11-
from judo.app.structs import WorldState
11+
from judo.app.structs import RenderPose
1212
from judo.tasks import TaskRegistration
1313
from judo.visualizers.visualizer import Visualizer
1414

@@ -103,7 +103,7 @@ def write_task_config(self) -> None:
103103
self.node.send_output("task_config", *to_arrow(self.visualizer.task_config))
104104
self.visualizer.task_config_updated.clear()
105105

106-
@on_event("INPUT", "world_states")
106+
@on_event("INPUT", "render_pose")
107107
def update_states(self, event: dict) -> None:
108108
"""Callback to update states on receiving a new state measurement."""
109109
if self.visualizer.controller_config.spline_order == "cubic" and self.visualizer.optimizer_config.num_nodes < 4:
@@ -115,11 +115,11 @@ def update_states(self, event: dict) -> None:
115115
self.visualizer.optimizer_config_updated.set()
116116

117117
# TODO: change the mujoco state
118-
world_state_msg = from_arrow(event["value"], event["metadata"], WorldState)
118+
render_pose_msg = from_arrow(event["value"], event["metadata"], RenderPose)
119119
try:
120120
with self.visualizer.task_lock:
121-
self.visualizer.data.xpos[:] = world_state_msg.xpos
122-
self.visualizer.data.xquat[:] = world_state_msg.xquat
121+
self.visualizer.data.xpos[:] = render_pose_msg.xpos
122+
self.visualizer.data.xquat[:] = render_pose_msg.xquat
123123
self.visualizer.viser_model.set_data(self.visualizer.data)
124124
except ValueError:
125125
# we're switching tasks and the new task has a different number of xpos/xquat

judo/app/structs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ def spline(self) -> interp1d:
8383

8484

8585
@dataclass
86-
class WorldState:
87-
"""Struct for world states used in visualization."""
86+
class RenderPose:
87+
"""Struct for visualization poses used by the renderer."""
8888

8989
xpos: np.ndarray
9090
xquat: np.ndarray

0 commit comments

Comments
 (0)