Skip to content

Commit e5b71fc

Browse files
committed
improved bin packing example
1 parent 716149d commit e5b71fc

File tree

1 file changed

+141
-113
lines changed

1 file changed

+141
-113
lines changed

scripts/demos/bin_packing.py

Lines changed: 141 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
.. code-block:: bash
99
1010
# Usage
11-
./isaaclab.sh -p scripts/demos/bin_packing.py --num_envs 2048
11+
./isaaclab.sh -p scripts/demos/bin_packing.py --num_envs 32
1212
1313
"""
1414

@@ -22,9 +22,7 @@
2222
from isaaclab.app import AppLauncher
2323

2424
# add argparse arguments
25-
parser = argparse.ArgumentParser(
26-
description="Demo on spawning different number of objects in multiple bin packing environments."
27-
)
25+
parser = argparse.ArgumentParser(description="Demo usage of RigidObjectCollection through bin packing example")
2826
parser.add_argument("--num_envs", type=int, default=16, help="Number of environments to spawn.")
2927
# append AppLauncher cli args
3028
AppLauncher.add_app_launcher_args(parser)
@@ -37,11 +35,12 @@
3735

3836
"""Rest everything follows."""
3937

38+
import math
4039
import torch
4140

4241
import isaaclab.sim as sim_utils
4342
import isaaclab.utils.math as math_utils
44-
from isaaclab.assets import AssetBaseCfg, RigidObject, RigidObjectCfg, RigidObjectCollection, RigidObjectCollectionCfg
43+
from isaaclab.assets import AssetBaseCfg, RigidObjectCfg, RigidObjectCollection, RigidObjectCollectionCfg
4544
from isaaclab.scene import InteractiveScene, InteractiveSceneCfg
4645
from isaaclab.sim import SimulationContext
4746
from isaaclab.utils import Timer, configclass
@@ -51,42 +50,34 @@
5150
# Scene Configuration
5251
##
5352

53+
MAX_NUM_OBJECTS = 24
54+
MAX_OBJECTS_PER_BIN = 24
55+
MIN_OBJECTS_PER_BIN = 1
56+
NUM_OBJECTS_PER_LAYER = 4
57+
CACHE_HEIGHT = 2.5
5458
POSE_RANGE = {"roll": (-3.14, 3.14), "pitch": (-3.14, 3.14), "yaw": (-3.14, 3.14)}
55-
VELOCITY_RANGE = {"roll": (-0.76, 0.76), "pitch": (-0.76, 0.76), "yaw": (-0.76, 0.76)}
59+
VELOCITY_RANGE = {"roll": (-0.2, 1.0), "pitch": (-0.2, 1.0), "yaw": (-0.2, 1.0)}
5660

61+
# Object layout configuration
5762

58-
RANDOM_YCB_RIGID_OBJECT_CFG = RigidObjectCfg(
59-
prim_path="/World/envs/env_.*/Object",
60-
spawn=sim_utils.MultiAssetSpawnerCfg(
61-
assets_cfg=[
62-
sim_utils.UsdFileCfg(
63-
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/004_sugar_box.usd",
64-
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
65-
),
66-
sim_utils.UsdFileCfg(
67-
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/003_cracker_box.usd",
68-
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
69-
),
70-
sim_utils.UsdFileCfg(
71-
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/005_tomato_soup_can.usd",
72-
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
73-
),
74-
sim_utils.UsdFileCfg(
75-
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/006_mustard_bottle.usd",
76-
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
77-
),
78-
# note: the placeholder, this allows the effect of having less objects in some env ids
79-
sim_utils.SphereCfg(
80-
radius=0.1,
81-
rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=True),
82-
collision_props=sim_utils.CollisionPropertiesCfg(collision_enabled=False),
83-
visible=False
84-
),
85-
],
86-
random_choice=True,
63+
GROCERIES = {
64+
"OBJECT_A" : sim_utils.UsdFileCfg(
65+
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/004_sugar_box.usd",
66+
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
67+
),
68+
"OBJECT_B" : sim_utils.UsdFileCfg(
69+
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/003_cracker_box.usd",
70+
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
71+
),
72+
"OBJECT_C" : sim_utils.UsdFileCfg(
73+
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/005_tomato_soup_can.usd",
74+
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
8775
),
88-
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)),
89-
)
76+
"OBJECT_D" : sim_utils.UsdFileCfg(
77+
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/006_mustard_bottle.usd",
78+
rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4),
79+
)
80+
}
9081

9182

9283
@configclass
@@ -115,82 +106,103 @@ class MultiObjectSceneCfg(InteractiveSceneCfg):
115106
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.15)),
116107
)
117108

118-
# object collection
119-
object_collection: RigidObjectCollectionCfg = RigidObjectCollectionCfg(
109+
groceries: RigidObjectCollectionCfg = RigidObjectCollectionCfg(
120110
rigid_objects={
121-
"Object_A_Layer1": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
122-
prim_path="/World/envs/env_.*/Object_A_Layer1",
123-
init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.035, -0.06, 0.2)),
124-
),
125-
"Object_B_Layer1": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
126-
prim_path="/World/envs/env_.*/Object_B_Layer1",
127-
init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.035, 0.06, 0.2)),
128-
),
129-
"Object_C_Layer1": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
130-
prim_path="/World/envs/env_.*/Object_C_Layer1",
131-
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.035, 0.06, 0.2)),
132-
),
133-
"Object_D_Layer1": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
134-
prim_path="/World/envs/env_.*/Object_D_Layer1",
135-
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.035, -0.06, 0.2)),
136-
),
137-
"Object_A_Layer2": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
138-
prim_path="/World/envs/env_.*/Object_A_Layer2",
139-
init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.035, -0.06, 0.4)),
140-
),
141-
"Object_B_Layer2": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
142-
prim_path="/World/envs/env_.*/Object_B_Layer2",
143-
init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.035, 0.06, 0.4)),
144-
),
145-
"Object_C_Layer2": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
146-
prim_path="/World/envs/env_.*/Object_C_Layer2",
147-
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.035, 0.06, 0.4)),
148-
),
149-
"Object_D_Layer2": RANDOM_YCB_RIGID_OBJECT_CFG.replace(
150-
prim_path="/World/envs/env_.*/Object_D_Layer2",
151-
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.035, -0.06, 0.4)),
152-
),
111+
f"Object_{label}_Layer{layer}": RigidObjectCfg(
112+
prim_path=f"/World/envs/env_.*/Object_{label}_Layer{layer}",
113+
init_state=RigidObjectCfg.InitialStateCfg(
114+
pos=(x, y, 0.2 + (layer) * 0.2)
115+
),
116+
spawn=GROCERIES.get(f"OBJECT_{label}")
117+
)
118+
for layer in range(MAX_NUM_OBJECTS // NUM_OBJECTS_PER_LAYER)
119+
for label, (x, y) in zip(["A", "B", "C", "D"], [(-0.035, -0.1), (-0.035, 0.1), (0.035, 0.1), (0.035, -0.1)])
153120
}
154121
)
155122

156-
def reset_object_collections(scene: InteractiveScene, view_ids: torch.Tensor):
157-
if len(view_ids) == 0:
158-
return
159-
rigid_object_collection: RigidObjectCollection = scene["object_collection"]
160-
default_state_w = rigid_object_collection.data.default_object_state.clone()
161-
default_state_w[..., :3] = default_state_w[..., :3] + scene.env_origins.unsqueeze(1)
162-
default_state_w_view = rigid_object_collection.reshape_data_to_view(default_state_w)[view_ids]
163-
range_list = [POSE_RANGE.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]]
164-
ranges = torch.tensor(range_list, device=scene.device)
165-
samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(view_ids), 6), device=scene.device)
166123

124+
def reset_object_collections(
125+
scene: InteractiveScene, collection_name: str, view_states: torch.Tensor, view_ids: torch.Tensor, randomize: bool = False
126+
):
127+
rigid_object_collection: RigidObjectCollection = scene[collection_name]
128+
sel_view_states = view_states[view_ids]
129+
positions = sel_view_states[:, :3]
130+
orientations = sel_view_states[:, 3:7]
167131
# poses
168-
positions = default_state_w_view[:, :3] + samples[..., 0:3]
169-
orientations_delta = math_utils.quat_from_euler_xyz(samples[..., 3], samples[..., 4], samples[..., 5])
170-
orientations = math_utils.quat_mul(default_state_w_view[:, 3:7], orientations_delta)
171-
# velocities
172-
range_list = [VELOCITY_RANGE.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]]
173-
ranges = torch.tensor(range_list, device=scene.device)
174-
samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(view_ids), 6), device=scene.device)
175-
176-
new_velocities = default_state_w_view[:, 7:13] + samples
177-
new_poses = torch.concat((positions, orientations), dim=-1)
178-
179-
new_poses[..., 3:] = math_utils.convert_quat(new_poses[..., 3:], to="xyzw")
132+
if randomize:
133+
range_list = [POSE_RANGE.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]]
134+
ranges = torch.tensor(range_list, device=scene.device)
135+
samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(view_ids), 6), device=scene.device)
136+
positions += samples[..., 0:3]
180137

181-
num_objects = rigid_object_collection.num_instances * rigid_object_collection.num_objects
138+
orientations_delta = math_utils.quat_from_euler_xyz(samples[..., 3], samples[..., 4], samples[..., 5])
139+
orientations = math_utils.convert_quat(orientations, to="wxyz")
140+
orientations = math_utils.quat_mul(orientations, orientations_delta)
141+
orientations = math_utils.convert_quat(orientations, to="xyzw")
182142

183-
if len(view_ids) != num_objects:
184-
poses = torch.zeros((num_objects, 7), device=scene.device)
185-
poses[view_ids, :] = new_poses
186-
velocities = torch.zeros((num_objects, 6), device=scene.device)
187-
velocities[view_ids, :] = new_velocities
143+
# velocities
144+
new_velocities = sel_view_states[:, 7:13]
145+
if randomize:
146+
range_list = [VELOCITY_RANGE.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]]
147+
ranges = torch.tensor(range_list, device=scene.device)
148+
samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(view_ids), 6), device=scene.device)
149+
new_velocities += samples
188150
else:
189-
poses = new_poses
190-
velocities = new_velocities
191-
192-
rigid_object_collection.root_physx_view.set_transforms(poses, indices=view_ids.view(-1, 1))
193-
rigid_object_collection.root_physx_view.set_velocities(velocities, indices=view_ids.view(-1, 1))
151+
new_velocities[:] = 0.0
152+
153+
view_states[view_ids, :7] = torch.concat((positions, orientations), dim=-1)
154+
view_states[view_ids, 7:] = new_velocities
155+
156+
rigid_object_collection.root_physx_view.set_transforms(view_states[:, :7], indices=view_ids)
157+
rigid_object_collection.root_physx_view.set_velocities(view_states[:, 7:], indices=view_ids)
158+
159+
160+
def build_grocery_defaults(
161+
num_envs: int,
162+
device: str = "cpu",
163+
z_spacing: float = 0.1,
164+
cache_spacing: float = 0.25,
165+
num_objects_per_layer: int = NUM_OBJECTS_PER_LAYER,
166+
max_objects_per_bin: int = MAX_OBJECTS_PER_BIN,
167+
max_num_objects: int = MAX_NUM_OBJECTS,
168+
cache_height: float = CACHE_HEIGHT
169+
):
170+
171+
# The bin has a size of 0.2 x 0.3 x 0.15 m
172+
bin_x_dim, bin_y_dim, bin_z_dim = 0.2, 0.3, 0.15
173+
# First, we calculate the number of layers and objects per layer
174+
num_layers = math.ceil(max_objects_per_bin / num_objects_per_layer)
175+
num_x_objects = math.ceil(math.sqrt(num_objects_per_layer))
176+
num_y_objects = math.ceil(num_objects_per_layer / num_x_objects)
177+
total_objects = num_x_objects * num_y_objects * num_layers
178+
# Then, we create a 3D grid that allows for IxJxN objects to be placed ontop of the bin.
179+
x = torch.linspace(-bin_x_dim * (2 / 6), bin_x_dim * (2 / 6), num_x_objects, device=device)
180+
y = torch.linspace(-bin_y_dim * (2 / 6), bin_y_dim * (2 / 6), num_y_objects, device=device)
181+
z = torch.linspace(0, z_spacing * (num_layers - 1), num_layers, device=device) + bin_z_dim * 2
182+
grid_z, grid_y, grid_x = torch.meshgrid(z, y, x, indexing="ij") # Note Z first, allowing to naturally stack the layers.
183+
# Using this grid plus a reference quaternion, we can create the poses for the groceries to be spawned above the bin.
184+
ref_quat = torch.tensor([[0.0, 0.0, 0.0, 1.0]], device=device).repeat(total_objects, 1)
185+
positions = torch.stack((grid_x.flatten(), grid_y.flatten(), grid_z.flatten()), dim=-1)
186+
poses = torch.cat((positions, ref_quat), dim=-1)
187+
# Duplicate across environments, cap at max_num_objects
188+
active_spawn_poses = poses.unsqueeze(0).repeat(num_envs, 1, 1)[:, :max_num_objects, :]
189+
190+
# We'll also create a buffer for the cached groceries. They'll be spawned below the bin so they can't be seen.
191+
num_x_objects = math.ceil(math.sqrt(max_num_objects))
192+
num_y_objects = math.ceil(max_num_objects / num_x_objects)
193+
# We create a XY grid only and fix the Z height for the cache.
194+
x = cache_spacing * torch.arange(num_x_objects, device=device)
195+
y = cache_spacing * torch.arange(num_y_objects, device=device)
196+
grid_y, grid_x = torch.meshgrid(y, x, indexing="ij")
197+
grid_z = cache_height * torch.ones_like(grid_x)
198+
# We can then create the poses for the cached groceries.
199+
ref_quat = torch.tensor([[1.0, 0.0, 0.0, 0.0]], device=device).repeat(num_x_objects * num_y_objects, 1)
200+
positions = torch.stack((grid_x.flatten(), grid_y.flatten(), grid_z.flatten()), dim=-1)
201+
poses = torch.cat((positions, ref_quat), dim=-1)
202+
# Duplicate across environments, cap at max_num_objects
203+
cached_spawn_poses = poses.unsqueeze(0).repeat(num_envs, 1, 1)[:, :max_num_objects, :]
204+
205+
return active_spawn_poses, cached_spawn_poses
194206

195207

196208
##
@@ -202,24 +214,38 @@ def run_simulator(sim: SimulationContext, scene: InteractiveScene):
202214
"""Runs the simulation loop."""
203215
# Extract scene entities
204216
# note: we only do this here for readability.
205-
rigid_object: RigidObject = scene["object"]
206-
rigid_object_collection: RigidObjectCollection = scene["object_collection"]
217+
rigid_object_collection: RigidObjectCollection = scene["groceries"]
207218
view_indices = torch.arange(scene.num_envs * rigid_object_collection.num_objects, device=scene.device)
219+
default_state_w = rigid_object_collection.data.default_object_state.clone()
220+
default_state_w[..., :3] = default_state_w[..., :3] + scene.env_origins.unsqueeze(1)
208221
# Define simulation stepping
209222
sim_dt = sim.get_physics_dt()
210223
count = 0
224+
225+
active_spawn_poses, cached_spawn_poses = build_grocery_defaults(scene.num_envs, scene.device, max_num_objects=rigid_object_collection.num_objects)
226+
active_spawn_poses[..., :3] += scene.env_origins.view(-1, 1, 3)
227+
cached_spawn_poses[..., :3] += scene.env_origins.view(-1, 1, 3)
228+
active_spawn_poses = rigid_object_collection.reshape_data_to_view(active_spawn_poses)
229+
cached_spawn_poses = rigid_object_collection.reshape_data_to_view(cached_spawn_poses)
230+
spawning_state_w_view_xyzw = rigid_object_collection.reshape_data_to_view(default_state_w).clone()
231+
232+
groceries_mask_helper = torch.arange(rigid_object_collection.num_objects * scene.num_envs, device=scene.device) % rigid_object_collection.num_objects
211233
# Simulation loop
212234
while simulation_app.is_running():
213235
# Reset
214236
if count % 250 == 0:
215237
# reset counter
216238
count = 0
217-
# reset the scene entities
218-
# object
219-
root_state = rigid_object.data.default_root_state.clone()
220-
root_state[:, :3] += scene.env_origins
221-
# object collection
222-
reset_object_collections(scene, view_indices)
239+
num_active_groceries = torch.randint(MIN_OBJECTS_PER_BIN, rigid_object_collection.num_objects, (scene.num_envs, 1), device=scene.device)
240+
groceries_mask = (groceries_mask_helper.view(scene.num_envs, -1) < num_active_groceries).view(-1, 1)
241+
spawning_state_w_view_xyzw[:, :7] = cached_spawn_poses * (~groceries_mask) + active_spawn_poses * groceries_mask
242+
# Retrieve positions
243+
reset_object_collections(scene, "groceries", spawning_state_w_view_xyzw, view_indices[~groceries_mask.view(-1)])
244+
reset_object_collections(scene, "groceries", spawning_state_w_view_xyzw, view_indices[groceries_mask.view(-1)], randomize=True)
245+
# reset_object_collections(scene, "groceries", spawning_state_w_view_xyzw, view_indices)
246+
random_masses = torch.rand(rigid_object_collection.num_instances * rigid_object_collection.num_objects, device=scene.device) * 0.2 + 0.2
247+
rigid_object_collection.root_physx_view.set_masses(random_masses.cpu(), view_indices.cpu())
248+
rigid_object_collection.root_physx_view.set_disable_gravities((~groceries_mask).cpu(), indices=view_indices.cpu())
223249
scene.reset()
224250
print("[INFO]: Resetting scene state...")
225251

@@ -231,7 +257,9 @@ def run_simulator(sim: SimulationContext, scene: InteractiveScene):
231257
object_pos_b_view = rigid_object_collection.reshape_data_to_view(object_pos_b)
232258
inbound_mask = (-0.2 < object_pos_b_view[:, 0]) & (object_pos_b_view[:, 0] < 0.2)
233259
inbound_mask &= (-0.3 < object_pos_b_view[:, 1]) & (object_pos_b_view[:, 1] < 0.3)
234-
reset_object_collections(scene, view_indices[~inbound_mask])
260+
out_bound_indices = view_indices[~inbound_mask]
261+
if len(out_bound_indices) > 0:
262+
reset_object_collections(scene, "groceries", spawning_state_w_view_xyzw, view_indices[out_bound_indices])
235263
# Increment counter
236264
count += 1
237265
# Update buffers

0 commit comments

Comments
 (0)