chrisjcc commited on
Commit
5e06c65
·
verified ·
1 Parent(s): fca5fa8

Return a list of dictionaries (preferred for LLM-friendly JSON) python Copy Edit

Browse files
Files changed (1) hide show
  1. tray_sim.py +137 -4
tray_sim.py CHANGED
@@ -1,8 +1,13 @@
1
- import mujoco
2
- import numpy as np
3
- import imageio
4
  import os
 
5
  import tempfile
 
 
 
 
 
 
 
6
 
7
  MODEL_PATH = "assets/tray.xml"
8
  N_OBJECTS = 5 # number of dynamic blocks to randomize
@@ -10,6 +15,7 @@ PUSH_START_STEP = 50
10
  SIM_STEPS = 200
11
  IMPACT_STEP = 60 # is a good starting point, just after pusher activates.
12
 
 
13
  def classify_stability(data, model):
14
  """
15
  Classify object stability based on position data.
@@ -39,6 +45,95 @@ def classify_stability(data, model):
39
  stable_objects.append(False)
40
  return stable_objects
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def run_tray_simulation(seed=0, num_objects=N_OBJECTS, azimuth=45, elevation=-25, distance=0.6):
43
  np.random.seed(seed)
44
  model = mujoco.MjModel.from_xml_path(MODEL_PATH)
@@ -130,9 +225,47 @@ def run_tray_simulation(seed=0, num_objects=N_OBJECTS, azimuth=45, elevation=-25
130
 
131
  # Optional: print or return
132
  print("Stability:", stability_flags)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  # Save to GIF
135
  gif_path = os.path.join(tempfile.gettempdir(), f"tray_sim_{seed}.gif")
136
  imageio.mimsave(gif_path, frames, fps=20)
137
  #return gif_path
138
- return gif_path, stability_flags
 
 
 
 
 
 
1
  import os
2
+ import json
3
  import tempfile
4
+ import imageio
5
+
6
+ import numpy as np
7
+ import mujoco
8
+
9
+ # Extract final object rotations in Euler angles for interpretability (optional)
10
+ from scipy.spatial.transform import Rotation as R
11
 
12
  MODEL_PATH = "assets/tray.xml"
13
  N_OBJECTS = 5 # number of dynamic blocks to randomize
 
15
  SIM_STEPS = 200
16
  IMPACT_STEP = 60 # is a good starting point, just after pusher activates.
17
 
18
+
19
  def classify_stability(data, model):
20
  """
21
  Classify object stability based on position data.
 
45
  stable_objects.append(False)
46
  return stable_objects
47
 
48
+ # Extend the classifier to explain why something is unstable:
49
+ def classify_stability_verbose(data, model):
50
+ flags = []
51
+ tray_bounds = 0.3
52
+ for i in range(N_OBJECTS):
53
+ pos = data.qpos[i*7 : i*7 + 3]
54
+ reasons = []
55
+ if pos[2] < 0.01:
56
+ reasons.append("z too low (possibly toppled)")
57
+ if abs(pos[0]) > tray_bounds or abs(pos[1]) > tray_bounds:
58
+ reasons.append("outside tray bounds")
59
+ if not reasons:
60
+ flags.append({"stable": True, "reason": "object upright and within tray"})
61
+ else:
62
+ flags.append({"stable": False, "reason": ", ".join(reasons)})
63
+ return flags
64
+
65
+ # Create LLM-Friendly Prompt Template
66
+ def format_llm_prompt(physics_state):
67
+ """
68
+ Converts a physics state dictionary of a simulated scene into a JSON-serializable list of objects
69
+ (human-readable), where each object contains position, orientation, velocity, angular velocity,
70
+ and stability information suitable for prompting a large language model (LLM).
71
+
72
+ Each object is described by its:
73
+ - 3D position
74
+ - orientation in ZYX Euler angles (in degrees)
75
+ - linear velocity
76
+ - angular velocity
77
+ - stability status (Stable or Unstable)
78
+
79
+ Parameters:
80
+ physics_state (dict): A dictionary containing the physics properties of each object,
81
+ with the following keys:
82
+ - "positions_xyz": List of shape (N, 3), each entry a position vector [x, y, z]
83
+ - "orientations_euler": List of shape (N, 3), each entry an orientation vector [z, y, x] in degrees
84
+ - "velocities_linear": List of shape (N, 3), linear velocity vectors
85
+ - "velocities_angular": List of shape (N, 3), angular velocity vectors
86
+ - "stable_flags": List of bools indicating whether each object is stable
87
+
88
+ Returns:
89
+ List[dict]: Each dictionary represents an object with formatted fields
90
+ suitable for JSON serialization.
91
+ """
92
+ formatted = []
93
+ for i in range(len(physics_state["positions_xyz"])):
94
+ obj = {
95
+ "id": i,
96
+ "position": np.round(physics_state["positions_xyz"][i], 3).tolist(),
97
+ "orientation_euler_zyx_deg": np.round(physics_state["orientations_euler"][i], 1).tolist(),
98
+ "linear_velocity": np.round(physics_state["velocities_linear"][i], 3).tolist(),
99
+ "angular_velocity": np.round(physics_state["velocities_angular"][i], 3).tolist(),
100
+ "status": "Stable" if physics_state["stable_flags"][i] else "Unstable"
101
+ #"status": "Stable" if verbose["stable"] else f"Unstable ({verbose['reason']})"
102
+ }
103
+ formatted.append(obj)
104
+ return formatted
105
+
106
+ def format_as_natural_language_prompt(scene_description, task_description=None):
107
+ """
108
+ Convert structured physics state into a natural language prompt for LLMs.
109
+
110
+ Args:
111
+ scene_description (List[dict]): Output of format_llm_prompt().
112
+ task_description (str, optional): Additional instruction to prepend, such as
113
+ "Describe which blocks are unstable and why" or
114
+ "Predict which objects might move further if the tray tilts".
115
+
116
+ Returns:
117
+ str: Natural language prompt for an LLM.
118
+ """
119
+ lines = []
120
+ if task_description:
121
+ lines.append(task_description.strip())
122
+ lines.append("") # blank line for separation
123
+
124
+ lines.append("Here is the scene summary:")
125
+
126
+ for obj in scene_description:
127
+ line = (
128
+ f"Object {obj['id']} is at position {obj['position']} with orientation {obj['orientation_euler_zyx_deg']} degrees. "
129
+ f"It has linear velocity {obj['linear_velocity']} and angular velocity {obj['angular_velocity']}. "
130
+ f"Status: {obj['status']}."
131
+ )
132
+ lines.append(line)
133
+
134
+ return "\n".join(lines)
135
+
136
+
137
  def run_tray_simulation(seed=0, num_objects=N_OBJECTS, azimuth=45, elevation=-25, distance=0.6):
138
  np.random.seed(seed)
139
  model = mujoco.MjModel.from_xml_path(MODEL_PATH)
 
225
 
226
  # Optional: print or return
227
  print("Stability:", stability_flags)
228
+
229
+ euler_angles = []
230
+ for i in range(num_objects):
231
+ quat = data.qpos[i*7+3 : i*7+7] # qw, qx, qy, qz
232
+ # Convert to Euler (yaw-pitch-roll)
233
+ rot = R.from_quat([quat[1], quat[2], quat[3], quat[0]]) # MuJoCo uses [qw, qx, qy, qz]
234
+ euler = rot.as_euler('zyx', degrees=True)
235
+ euler_angles.append(euler.tolist())
236
+
237
+ # Fully structured snapshot of: Positions,
238
+ # Orientations (in both quaternion and Euler angle forms),
239
+ # Linear and angular velocities, and Stability flags
240
+ physics_state = {
241
+ "positions_xyz": data.qpos[:num_objects * 7].reshape(num_objects, 7)[:, :3].tolist(), # [x,y,z,qw,qx,qy,qz]
242
+ "orientations_quat": data.qpos[:num_objects * 7].reshape(num_objects, 7)[:, 3:].tolist(),
243
+ "orientations_euler": euler_angles,
244
+ "velocities_linear": data.qvel[:num_objects * 6].reshape(num_objects, 6)[:, :3].tolist(), # [vx,vy,vz,wx,wy,wz]
245
+ "velocities_angular": data.qvel[:num_objects * 6].reshape(num_objects, 6)[:, 3:].tolist(),
246
+ "stable_flags": stability_flags,
247
+ #"stable_verbose": classify_stability_verbose(data, model), more interpretable to LLMs
248
+ }
249
+
250
+ # Save JSON Snapshot (optional, for LLM input/debugging)
251
+ json_path = os.path.join(tempfile.gettempdir(), f"tray_sim_{seed}_state.json")
252
+ with open(json_path, "w") as f:
253
+ json.dump(physics_state, f, indent=2)
254
+
255
+ llm_friendly_output = format_llm_prompt(physics_state)
256
+
257
+ #formatted = format_llm_prompt(physics_state)
258
+ #prompt = format_as_natural_language_prompt(
259
+ # formatted,
260
+ # task_description="Explain which objects are likely to fall if the tray is tilted slightly to the right."
261
+ #)
262
+ print(prompt)
263
+
264
 
265
  # Save to GIF
266
  gif_path = os.path.join(tempfile.gettempdir(), f"tray_sim_{seed}.gif")
267
  imageio.mimsave(gif_path, frames, fps=20)
268
  #return gif_path
269
+ #return gif_path, stability_flags
270
+ #return gif_path, stability_flags, physics_state # optionally also return json_path
271
+ return gif_path, stability_flags, physics_state, llm_friendly_output, json_path # optionally also return json_path