dwehr commited on
Commit
b52300c
·
1 Parent(s): 9df6042

Add inverse dynamics support

Browse files
cosmos-framework/cosmos_framework/data/vfm/action/action_viz/app.py CHANGED
@@ -39,6 +39,7 @@ from cosmos_framework.data.vfm.action.action_viz.state import (
39
  GenerationResult,
40
  control_points_from_action,
41
  make_generation_id,
 
42
  read_generated_action,
43
  )
44
  from cosmos_framework.data.vfm.action.urdf_visualizer.unified_action import ActionFormat, get_video_from_sample
@@ -284,6 +285,7 @@ def launch_action_viz(config: AppConfig) -> None:
284
  num_steps_input = client.gui.add_number("Sampling steps", initial_value=DEFAULT_SAMPLING_STEPS, min=1, step=1)
285
  guidance_input = client.gui.add_number("Guidance", initial_value=DEFAULT_GUIDANCE, min=0.0, max=7.0, step=0.1)
286
  generate_button = client.gui.add_button("Run forward dynamics")
 
287
  policy_button = client.gui.add_button("Run policy")
288
  status_text = client.gui.add_markdown(f"*{worker_client.status_message}*")
289
  info_text = client.gui.add_markdown("")
@@ -300,6 +302,7 @@ def launch_action_viz(config: AppConfig) -> None:
300
  with client.gui.add_folder("Video"):
301
  gt_panel = client.gui.add_image(np.zeros((64, 64, 3), dtype=np.uint8), label="Ground truth")
302
  generated_panel = client.gui.add_image(np.zeros((64, 64, 3), dtype=np.uint8), label="Generated")
 
303
 
304
  with client.gui.add_folder("Trajectory editor"):
305
  target_dropdown = client.gui.add_dropdown("Trajectory", options=("right", "left"), initial_value="right")
@@ -696,8 +699,12 @@ def launch_action_viz(config: AppConfig) -> None:
696
  return
697
  if loaded.video is not None and t < len(loaded.video):
698
  gt_panel.image = loaded.video[t]
699
- if loaded.generated_video:
700
- generated_panel.image = loaded.generated_video[min(t, len(loaded.generated_video) - 1)]
 
 
 
 
701
 
702
  def _update_action_text(t: int) -> None:
703
  loaded = sample_state["loaded"]
@@ -750,6 +757,7 @@ def launch_action_viz(config: AppConfig) -> None:
750
  _flush_scene_update()
751
  loaded.generated_video = []
752
  generated_panel.image = np.zeros((64, 64, 3), dtype=np.uint8)
 
753
 
754
  generation_id = make_generation_id()
755
  client_generation_root = config.output_root / "active" / f"client_{client.client_id}"
@@ -802,13 +810,16 @@ def launch_action_viz(config: AppConfig) -> None:
802
  )
803
 
804
  try:
805
- if result.status == "success" and result.video_path:
806
- loaded.generated_video = _load_video_frames(Path(result.video_path))
807
- if model_mode == "policy" and result.generated_action_path:
 
 
 
808
  try:
809
- _replace_loaded_action(loaded, read_generated_action(Path(result.generated_action_path)))
810
- except Exception as exc:
811
- status_text.content = f"*Generation {generation_id} loaded video; action ignored: {exc}*"
812
  else:
813
  status_text.content = f"*Generation {generation_id} complete.*"
814
  else:
@@ -846,6 +857,10 @@ def launch_action_viz(config: AppConfig) -> None:
846
  def _(_) -> None:
847
  run_generation("forward_dynamics")
848
 
 
 
 
 
849
  @policy_button.on_click
850
  def _(_) -> None:
851
  run_generation("policy")
@@ -1021,6 +1036,22 @@ def _set_loaded_action(loaded: LoadedSample, raw_action: np.ndarray) -> None:
1021
  loaded.baked_action = raw_action.astype(np.float32).copy()
1022
 
1023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1024
  def _build_render_scene(loaded: LoadedSample, *, source_robot_animation: bool) -> Any:
1025
  """Build a render scene from the loaded sample's current editor state."""
1026
 
 
39
  GenerationResult,
40
  control_points_from_action,
41
  make_generation_id,
42
+ model_mode_generates_action,
43
  read_generated_action,
44
  )
45
  from cosmos_framework.data.vfm.action.urdf_visualizer.unified_action import ActionFormat, get_video_from_sample
 
285
  num_steps_input = client.gui.add_number("Sampling steps", initial_value=DEFAULT_SAMPLING_STEPS, min=1, step=1)
286
  guidance_input = client.gui.add_number("Guidance", initial_value=DEFAULT_GUIDANCE, min=0.0, max=7.0, step=0.1)
287
  generate_button = client.gui.add_button("Run forward dynamics")
288
+ inverse_dynamics_button = client.gui.add_button("Run inverse dynamics")
289
  policy_button = client.gui.add_button("Run policy")
290
  status_text = client.gui.add_markdown(f"*{worker_client.status_message}*")
291
  info_text = client.gui.add_markdown("")
 
302
  with client.gui.add_folder("Video"):
303
  gt_panel = client.gui.add_image(np.zeros((64, 64, 3), dtype=np.uint8), label="Ground truth")
304
  generated_panel = client.gui.add_image(np.zeros((64, 64, 3), dtype=np.uint8), label="Generated")
305
+ generated_panel.visible = False
306
 
307
  with client.gui.add_folder("Trajectory editor"):
308
  target_dropdown = client.gui.add_dropdown("Trajectory", options=("right", "left"), initial_value="right")
 
699
  return
700
  if loaded.video is not None and t < len(loaded.video):
701
  gt_panel.image = loaded.video[t]
702
+ generated_frame = _select_generated_video_frame(loaded.generated_video, t)
703
+ if generated_frame is None:
704
+ generated_panel.visible = False
705
+ else:
706
+ generated_panel.image = generated_frame
707
+ generated_panel.visible = True
708
 
709
  def _update_action_text(t: int) -> None:
710
  loaded = sample_state["loaded"]
 
757
  _flush_scene_update()
758
  loaded.generated_video = []
759
  generated_panel.image = np.zeros((64, 64, 3), dtype=np.uint8)
760
+ generated_panel.visible = False
761
 
762
  generation_id = make_generation_id()
763
  client_generation_root = config.output_root / "active" / f"client_{client.client_id}"
 
810
  )
811
 
812
  try:
813
+ if result.status == "success":
814
+ if result.video_path:
815
+ loaded.generated_video = _load_video_frames(Path(result.video_path))
816
+ else:
817
+ loaded.generated_video = []
818
+ if model_mode_generates_action(model_mode):
819
  try:
820
+ _replace_loaded_action(loaded, _read_generated_result_action(result))
821
+ except ValueError as exc:
822
+ status_text.content = f"*Generation {generation_id} failed: {exc}*"
823
  else:
824
  status_text.content = f"*Generation {generation_id} complete.*"
825
  else:
 
857
  def _(_) -> None:
858
  run_generation("forward_dynamics")
859
 
860
+ @inverse_dynamics_button.on_click
861
+ def _(_) -> None:
862
+ run_generation("inverse_dynamics")
863
+
864
  @policy_button.on_click
865
  def _(_) -> None:
866
  run_generation("policy")
 
1036
  loaded.baked_action = raw_action.astype(np.float32).copy()
1037
 
1038
 
1039
+ def _read_generated_result_action(result: GenerationResult) -> np.ndarray:
1040
+ """Read the raw action emitted by an action-generating model result."""
1041
+
1042
+ if result.generated_action_path is None:
1043
+ raise ValueError(f"Generation {result.generation_id} has no saved generated action")
1044
+ return read_generated_action(Path(result.generated_action_path))
1045
+
1046
+
1047
+ def _select_generated_video_frame(generated_video: list[np.ndarray], t: int) -> np.ndarray | None:
1048
+ """Return the displayed generated-video frame, or None when no video exists."""
1049
+
1050
+ if not generated_video:
1051
+ return None
1052
+ return generated_video[min(max(int(t), 0), len(generated_video) - 1)]
1053
+
1054
+
1055
  def _build_render_scene(loaded: LoadedSample, *, source_robot_animation: bool) -> Any:
1056
  """Build a render scene from the loaded sample's current editor state."""
1057
 
cosmos-framework/cosmos_framework/data/vfm/action/action_viz/app_test.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from cosmos_framework.data.vfm.action.action_viz.app import (
7
+ _read_generated_result_action,
8
+ _select_generated_video_frame,
9
+ )
10
+ from cosmos_framework.data.vfm.action.action_viz.state import (
11
+ BRIDGE_ACTION_DIM,
12
+ GenerationResult,
13
+ write_generated_action,
14
+ )
15
+
16
+
17
+ @pytest.mark.L0
18
+ def test_read_generated_result_action_supports_inverse_dynamics(tmp_path) -> None:
19
+ action = np.ones((2, BRIDGE_ACTION_DIM), dtype=np.float32)
20
+ action_path = write_generated_action(action, tmp_path)
21
+ result = GenerationResult(
22
+ generation_id="gen",
23
+ model_mode="inverse_dynamics",
24
+ status="success",
25
+ generated_action_path=str(action_path),
26
+ )
27
+
28
+ loaded = _read_generated_result_action(result)
29
+
30
+ np.testing.assert_allclose(loaded, action, atol=1e-6)
31
+
32
+
33
+ @pytest.mark.L0
34
+ def test_read_generated_result_action_requires_saved_action_path() -> None:
35
+ result = GenerationResult(generation_id="gen", model_mode="inverse_dynamics", status="success")
36
+
37
+ with pytest.raises(ValueError, match="Generation gen has no saved generated action"):
38
+ _read_generated_result_action(result)
39
+
40
+
41
+ @pytest.mark.L0
42
+ def test_select_generated_video_frame_returns_none_for_action_only_generations() -> None:
43
+ assert _select_generated_video_frame([], 0) is None
44
+
45
+
46
+ @pytest.mark.L0
47
+ def test_select_generated_video_frame_clamps_to_available_frames() -> None:
48
+ first = np.zeros((2, 2, 3), dtype=np.uint8)
49
+ last = np.ones((2, 2, 3), dtype=np.uint8)
50
+
51
+ np.testing.assert_array_equal(_select_generated_video_frame([first, last], -1), first)
52
+ np.testing.assert_array_equal(_select_generated_video_frame([first, last], 99), last)
cosmos-framework/cosmos_framework/data/vfm/action/action_viz/local_worker.py CHANGED
@@ -21,6 +21,7 @@ import numpy as np
21
  from cosmos_framework.data.vfm.action.action_viz.state import (
22
  GenerationRequest,
23
  GenerationResult,
 
24
  request_to_json_dict,
25
  write_generated_action,
26
  write_generation_request,
@@ -957,7 +958,7 @@ def _copy_generation_outputs(
957
 
958
  generated_video_path = None
959
  vision_path = sample_output_dir / "vision.mp4"
960
- if vision_path.is_file():
961
  generated_video_path = output_dir / "generated.mp4"
962
  shutil.copyfile(vision_path, generated_video_path)
963
 
@@ -975,6 +976,8 @@ def _copy_generation_outputs(
975
  to_model_space=False,
976
  )
977
  generated_action_path = write_generated_action(np.asarray(raw_action, dtype=np.float32), output_dir)
 
 
978
 
979
  result_payload: dict[str, Any] = {
980
  "ok": True,
 
21
  from cosmos_framework.data.vfm.action.action_viz.state import (
22
  GenerationRequest,
23
  GenerationResult,
24
+ model_mode_generates_action,
25
  request_to_json_dict,
26
  write_generated_action,
27
  write_generation_request,
 
958
 
959
  generated_video_path = None
960
  vision_path = sample_output_dir / "vision.mp4"
961
+ if request.model_mode != "inverse_dynamics" and vision_path.is_file():
962
  generated_video_path = output_dir / "generated.mp4"
963
  shutil.copyfile(vision_path, generated_video_path)
964
 
 
976
  to_model_space=False,
977
  )
978
  generated_action_path = write_generated_action(np.asarray(raw_action, dtype=np.float32), output_dir)
979
+ if model_mode_generates_action(request.model_mode) and generated_action_path is None:
980
+ raise ValueError(f"{request.model_mode} generation did not return an action")
981
 
982
  result_payload: dict[str, Any] = {
983
  "ok": True,
cosmos-framework/cosmos_framework/data/vfm/action/action_viz/local_worker_test.py CHANGED
@@ -1,10 +1,22 @@
1
  from __future__ import annotations
2
 
 
 
3
  from typing import Any
4
 
 
5
  import pytest
6
 
7
- from cosmos_framework.data.vfm.action.action_viz.local_worker import _patched_unipc_progress
 
 
 
 
 
 
 
 
 
8
 
9
 
10
  @pytest.mark.L0
@@ -24,3 +36,96 @@ def test_patched_unipc_progress_reports_sampler_percentages(monkeypatch: pytest.
24
 
25
  assert updates == [(37, "sampling"), (63, "sampling"), (90, "sampling")]
26
  assert unipc.progress_bar is passthrough_progress_bar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ import json
4
+ from pathlib import Path
5
  from typing import Any
6
 
7
+ import numpy as np
8
  import pytest
9
 
10
+ from cosmos_framework.data.vfm.action.action_viz.local_worker import (
11
+ _copy_generation_outputs,
12
+ _patched_unipc_progress,
13
+ )
14
+ from cosmos_framework.data.vfm.action.action_viz.state import (
15
+ BRIDGE_ACTION_DIM,
16
+ ControlPoint,
17
+ GenerationRequest,
18
+ read_generated_action,
19
+ )
20
 
21
 
22
  @pytest.mark.L0
 
36
 
37
  assert updates == [(37, "sampling"), (63, "sampling"), (90, "sampling")]
38
  assert unipc.progress_bar is passthrough_progress_bar
39
+
40
+
41
+ @pytest.mark.L0
42
+ def test_copy_generation_outputs_writes_inverse_dynamics_action_without_copying_auxiliary_video(tmp_path) -> None:
43
+ request = _request(tmp_path, "inverse_dynamics")
44
+ inference_dir = tmp_path / "framework_output"
45
+ sample_output_dir = inference_dir / request.generation_id
46
+ sample_output_dir.mkdir(parents=True)
47
+ sample_outputs = {
48
+ "outputs": [
49
+ {
50
+ "content": {
51
+ "action": [
52
+ [0.0] * BRIDGE_ACTION_DIM,
53
+ [0.0] * BRIDGE_ACTION_DIM,
54
+ ]
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ (sample_output_dir / "sample_outputs.json").write_text(json.dumps(sample_outputs), encoding="utf-8")
60
+ (sample_output_dir / "vision.mp4").write_bytes(b"auxiliary video")
61
+
62
+ payload, video_path, action_path = _copy_generation_outputs(request, inference_dir, Path(request.output_dir))
63
+
64
+ assert payload["artifacts"]["video_filename"] is None
65
+ assert payload["artifacts"]["action_filename"] == "generated_action.json"
66
+ assert video_path is None
67
+ assert action_path is not None
68
+ action = read_generated_action(Path(action_path))
69
+ assert action.shape == (2, BRIDGE_ACTION_DIM)
70
+
71
+
72
+ @pytest.mark.L0
73
+ def test_copy_generation_outputs_keeps_policy_video(tmp_path) -> None:
74
+ request = _request(tmp_path, "policy")
75
+ output_dir = Path(request.output_dir)
76
+ output_dir.mkdir(parents=True)
77
+ inference_dir = tmp_path / "framework_output"
78
+ sample_output_dir = inference_dir / request.generation_id
79
+ sample_output_dir.mkdir(parents=True)
80
+ sample_outputs = {
81
+ "outputs": [
82
+ {
83
+ "content": {
84
+ "action": [
85
+ [0.0] * BRIDGE_ACTION_DIM,
86
+ ]
87
+ }
88
+ }
89
+ ]
90
+ }
91
+ (sample_output_dir / "sample_outputs.json").write_text(json.dumps(sample_outputs), encoding="utf-8")
92
+ (sample_output_dir / "vision.mp4").write_bytes(b"generated video")
93
+
94
+ payload, video_path, action_path = _copy_generation_outputs(request, inference_dir, output_dir)
95
+
96
+ assert payload["artifacts"]["video_filename"] == "generated.mp4"
97
+ assert video_path == str(output_dir / "generated.mp4")
98
+ assert Path(video_path).read_bytes() == b"generated video"
99
+ assert action_path is not None
100
+
101
+
102
+ @pytest.mark.L0
103
+ def test_copy_generation_outputs_requires_action_for_inverse_dynamics(tmp_path) -> None:
104
+ request = _request(tmp_path, "inverse_dynamics")
105
+ inference_dir = tmp_path / "framework_output"
106
+ sample_output_dir = inference_dir / request.generation_id
107
+ sample_output_dir.mkdir(parents=True)
108
+ (sample_output_dir / "sample_outputs.json").write_text(
109
+ json.dumps({"outputs": [{"content": {}}]}),
110
+ encoding="utf-8",
111
+ )
112
+
113
+ with pytest.raises(ValueError, match="inverse_dynamics generation did not return an action"):
114
+ _copy_generation_outputs(request, inference_dir, Path(request.output_dir))
115
+
116
+
117
+ def _request(tmp_path, model_mode: str) -> GenerationRequest:
118
+ return GenerationRequest(
119
+ generation_id="gen",
120
+ model_mode=model_mode,
121
+ dataset="bridge",
122
+ sample_index=0,
123
+ experiment_name="",
124
+ s3_checkpoint_dir="nvidia/Cosmos3-Nano",
125
+ checkpoint_cache_dir=None,
126
+ output_dir=str(tmp_path / "output"),
127
+ seed=0,
128
+ num_steps=1,
129
+ control_points=[ControlPoint(frame=0, values=[0.0] * BRIDGE_ACTION_DIM)],
130
+ baked_action=np.zeros((1, BRIDGE_ACTION_DIM), dtype=np.float32).tolist(),
131
+ )
cosmos-framework/cosmos_framework/data/vfm/action/action_viz/state.py CHANGED
@@ -24,7 +24,8 @@ BRIDGE_ACTION_CHANNELS = (
24
  "gripper",
25
  )
26
  BRIDGE_DATASET_SELECTOR = "bridge_20260501"
27
- GENERATION_MODEL_MODES = ("forward_dynamics", "policy")
 
28
 
29
 
30
  @dataclass(frozen=True)
@@ -189,7 +190,7 @@ def read_generation_request(path: Path) -> GenerationRequest:
189
  return request_from_json_dict(json.loads(path.read_text()))
190
 
191
 
192
- def write_generated_action(action: np.ndarray, output_dir: Path, filename: str = "policy_action.json") -> Path:
193
  """Write a generated raw action array and return its path."""
194
 
195
  action = _validate_generated_action_array(action)
@@ -231,6 +232,12 @@ def read_generated_action(path: Path) -> np.ndarray:
231
  return _validate_generated_action_array(np.asarray(json.loads(path.read_text()), dtype=np.float32))
232
 
233
 
 
 
 
 
 
 
234
  def _validate_action_array(action: np.ndarray, action_dim: int | None = None) -> np.ndarray:
235
  action = np.asarray(action, dtype=np.float32)
236
  expected_dim = BRIDGE_ACTION_DIM if action_dim is None else int(action_dim)
 
24
  "gripper",
25
  )
26
  BRIDGE_DATASET_SELECTOR = "bridge_20260501"
27
+ GENERATION_MODEL_MODES = ("forward_dynamics", "inverse_dynamics", "policy")
28
+ ACTION_GENERATING_MODEL_MODES = ("inverse_dynamics", "policy")
29
 
30
 
31
  @dataclass(frozen=True)
 
190
  return request_from_json_dict(json.loads(path.read_text()))
191
 
192
 
193
+ def write_generated_action(action: np.ndarray, output_dir: Path, filename: str = "generated_action.json") -> Path:
194
  """Write a generated raw action array and return its path."""
195
 
196
  action = _validate_generated_action_array(action)
 
232
  return _validate_generated_action_array(np.asarray(json.loads(path.read_text()), dtype=np.float32))
233
 
234
 
235
+ def model_mode_generates_action(model_mode: str) -> bool:
236
+ """Return whether a supported generation mode emits a raw action trajectory."""
237
+
238
+ return _validate_model_mode(model_mode) in ACTION_GENERATING_MODEL_MODES
239
+
240
+
241
  def _validate_action_array(action: np.ndarray, action_dim: int | None = None) -> np.ndarray:
242
  action = np.asarray(action, dtype=np.float32)
243
  expected_dim = BRIDGE_ACTION_DIM if action_dim is None else int(action_dim)
cosmos-framework/cosmos_framework/data/vfm/action/action_viz/state_test.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from cosmos_framework.data.vfm.action.action_viz.state import (
7
+ BRIDGE_ACTION_DIM,
8
+ model_mode_generates_action,
9
+ read_generated_action,
10
+ write_generated_action,
11
+ )
12
+
13
+
14
+ @pytest.mark.L0
15
+ @pytest.mark.parametrize(
16
+ ("model_mode", "generates_action"),
17
+ [
18
+ ("forward_dynamics", False),
19
+ ("inverse_dynamics", True),
20
+ ("policy", True),
21
+ ],
22
+ )
23
+ def test_model_mode_generates_action(model_mode: str, generates_action: bool) -> None:
24
+ assert model_mode_generates_action(model_mode) is generates_action
25
+
26
+
27
+ @pytest.mark.L0
28
+ def test_generated_action_round_trip_uses_generic_filename(tmp_path) -> None:
29
+ action = np.arange(2 * BRIDGE_ACTION_DIM, dtype=np.float32).reshape(2, BRIDGE_ACTION_DIM)
30
+
31
+ path = write_generated_action(action, tmp_path)
32
+
33
+ assert path.name == "generated_action.json"
34
+ np.testing.assert_allclose(read_generated_action(path), action, atol=1e-6)