@@ -1186,6 +1186,207 @@ class _ExposedDominoEnv(_ExposedEnvMixin, PyBulletDominoEnv):
11861186 f"is_held={ is_held } " )
11871187
11881188
1189+ def test_domino_pick_place_no_collisions ():
1190+ """Pick domino_1 and place it between others — no non-held domino moves.
1191+
1192+ Uses position mode with motion planning so BiRRT plans collision-free
1193+ paths. Verifies that non-held dominoes remain stationary throughout
1194+ the pick and place sequences (i.e., no arm–domino collisions).
1195+ """
1196+ try :
1197+ from predicators .envs .pybullet_domino import PyBulletDominoEnv
1198+ except ImportError :
1199+ pytest .skip ("pybullet_domino not available" )
1200+
1201+ utils .reset_config ({
1202+ "env" : "pybullet_domino" ,
1203+ "use_gui" : False ,
1204+ "pybullet_control_mode" : "position" ,
1205+ "pybullet_robot" : "fetch" ,
1206+ "domino_use_skill_factories" : True ,
1207+ "skill_phase_use_motion_planning" : True ,
1208+ "pybullet_ik_validate" : False ,
1209+ "domino_initialize_at_finished_state" : False ,
1210+ "domino_use_domino_blocks_as_target" : True ,
1211+ "domino_use_grid" : True ,
1212+ "domino_include_connected_predicate" : False ,
1213+ "domino_use_continuous_place" : True ,
1214+ "domino_restricted_push" : True ,
1215+ "domino_prune_actions" : False ,
1216+ "domino_has_glued_dominos" : False ,
1217+ "num_train_tasks" : 1 ,
1218+ "num_test_tasks" : 1 ,
1219+ })
1220+
1221+ class _ExposedDominoEnv (_ExposedEnvMixin , PyBulletDominoEnv ):
1222+ pass
1223+
1224+ env = _ExposedDominoEnv (use_gui = False )
1225+ Pick = env ._options ["Pick" ]
1226+ Place = env ._options ["Place" ]
1227+
1228+ domino_type = env ._domino_component .domino_type
1229+ robot_type = next (t for t in env .types if t .name == "robot" )
1230+
1231+ # Use test task 0 (matches debug_motion_planning.py setup)
1232+ obs = env .reset ("test" , 0 )
1233+ state = obs
1234+
1235+ robot = state .get_objects (robot_type )[0 ]
1236+ dominos = state .get_objects (domino_type )
1237+ pick_target = next (d for d in dominos if d .name == "domino_1" )
1238+
1239+ pos_tol = 1e-3
1240+
1241+ def _get_positions (st ):
1242+ return {o .name : (st .get (o , "x" ), st .get (o , "y" ), st .get (o , "z" ))
1243+ for o in st .get_objects (domino_type )}
1244+
1245+ def _check_moved (before , st , skip_names = ()):
1246+ moved = []
1247+ cur = _get_positions (st )
1248+ for name , (bx , by , bz ) in before .items ():
1249+ if name in skip_names :
1250+ continue
1251+ cx , cy , cz = cur [name ]
1252+ disp = np .sqrt ((cx - bx )** 2 + (cy - by )** 2 + (cz - bz )** 2 )
1253+ if disp > pos_tol :
1254+ moved .append ((name , disp ))
1255+ return moved
1256+
1257+ # ---- Pick domino_1 ----
1258+ pos_before_pick = _get_positions (state )
1259+ option = Pick .ground ([robot , pick_target ],
1260+ np .array ([0.01 ], dtype = np .float32 ))
1261+ assert option .initiable (state )
1262+
1263+ pick_collisions = []
1264+ for _ in range (300 ):
1265+ if option .terminal (state ):
1266+ break
1267+ action = option .policy (state )
1268+ state = env .simulate (state , action )
1269+ moved = _check_moved (pos_before_pick , state ,
1270+ skip_names = {pick_target .name })
1271+ if moved :
1272+ pick_collisions = moved
1273+
1274+ assert state .get (pick_target , "is_held" ) > 0.5 , \
1275+ "domino_1 should be held after pick"
1276+ assert not pick_collisions , \
1277+ f"Non-held dominoes moved during Pick: { pick_collisions } "
1278+
1279+ # ---- Place at (0.75, 1.26) between existing dominoes ----
1280+ pos_before_place = _get_positions (state )
1281+ target_x , target_y , target_yaw = 0.75 , 1.26 , 0.0
1282+ release_z = env .table_height + env .domino_height * 1.13
1283+
1284+ option = Place .ground (
1285+ [robot ],
1286+ np .array ([target_x , target_y , target_yaw , release_z ],
1287+ dtype = np .float32 ))
1288+ assert option .initiable (state )
1289+
1290+ place_collisions = []
1291+ for _ in range (300 ):
1292+ if option .terminal (state ):
1293+ break
1294+ action = option .policy (state )
1295+ state = env .simulate (state , action )
1296+ moved = _check_moved (pos_before_place , state )
1297+ if moved :
1298+ place_collisions = moved
1299+
1300+ assert not place_collisions , \
1301+ f"Non-held dominoes moved during Place: { place_collisions } "
1302+
1303+
1304+ @pytest .mark .xfail (reason = "Button detection zone overlaps dispense area "
1305+ "approach path — robot arm triggers button during place" )
1306+ def test_coffee_place_no_button_press ():
1307+ """PickJug then PlaceJugInMachine without turning machine on.
1308+
1309+ The jug should be placed on the dispense area without hitting the
1310+ machine's top overhang or accidentally pressing the button.
1311+ """
1312+ utils .reset_config ({
1313+ "env" : "pybullet_coffee" ,
1314+ "use_gui" : False ,
1315+ "pybullet_control_mode" : "position" ,
1316+ "pybullet_robot" : "fetch" ,
1317+ "pybullet_ik_validate" : False ,
1318+ "coffee_use_skill_factories" : True ,
1319+ "coffee_rotated_jug_ratio" : 0 ,
1320+ "coffee_num_cups_train" : [1 ],
1321+ "coffee_num_cups_test" : [1 ],
1322+ "coffee_machine_have_light_bar" : False ,
1323+ "coffee_move_back_after_place_and_push" : True ,
1324+ "coffee_machine_has_plug" : False ,
1325+ "coffee_combined_move_and_twist_policy" : True ,
1326+ "coffee_use_pixelated_jug" : True ,
1327+ "coffee_fill_jug_gradually" : True ,
1328+ "skill_phase_use_motion_planning" : True ,
1329+ "max_num_steps_option_rollout" : 100 ,
1330+ "num_train_tasks" : 1 ,
1331+ "num_test_tasks" : 1 ,
1332+ })
1333+
1334+ env = _ExposedCoffeeEnv (use_gui = False )
1335+
1336+ robot_type = next (t for t in env .types if t .name == "robot" )
1337+ jug_type = next (t for t in env .types if t .name == "jug" )
1338+ machine_type = next (t for t in env .types if t .name == "coffee_machine" )
1339+
1340+ obs = env .reset ("test" , 0 )
1341+ state = obs
1342+
1343+ robot = state .get_objects (robot_type )[0 ]
1344+ jug = state .get_objects (jug_type )[0 ]
1345+ machine = state .get_objects (machine_type )[0 ]
1346+
1347+ assert state .get (machine , "is_on" ) < 0.5 , "Machine should start OFF"
1348+
1349+ # ---- Pick the jug ----
1350+ pick_option = env .PickJug .ground (
1351+ [robot , jug ], np .array ([0.01 ], dtype = np .float32 ))
1352+ assert pick_option .initiable (state )
1353+
1354+ for _ in range (300 ):
1355+ if pick_option .terminal (state ):
1356+ break
1357+ action = pick_option .policy (state )
1358+ state = env .simulate (state , action )
1359+
1360+ assert state .get (jug , "is_held" ) > 0.5 , "Jug should be held after pick"
1361+ assert state .get (machine , "is_on" ) < 0.5 , "Machine turned on during pick!"
1362+
1363+ # ---- Place jug in machine ----
1364+ target_x = PyBulletCoffeeEnv .dispense_area_x
1365+ target_y = PyBulletCoffeeEnv .dispense_area_y
1366+ target_yaw = PyBulletCoffeeEnv .robot_init_wrist
1367+ release_z = PyBulletCoffeeEnv .z_lb + env .jug_handle_height ()
1368+
1369+ place_option = env .PlaceJugInMachine .ground (
1370+ [robot , jug , machine ],
1371+ np .array ([target_x , target_y , target_yaw , release_z ],
1372+ dtype = np .float32 ))
1373+ assert place_option .initiable (state )
1374+
1375+ machine_turned_on_step = None
1376+ for step in range (300 ):
1377+ if place_option .terminal (state ):
1378+ break
1379+ action = place_option .policy (state )
1380+ state = env .simulate (state , action )
1381+
1382+ if state .get (machine , "is_on" ) > 0.5 and machine_turned_on_step is None :
1383+ machine_turned_on_step = step
1384+
1385+ assert machine_turned_on_step is None , (
1386+ f"Machine was turned on at step { machine_turned_on_step } during "
1387+ f"PlaceJugInMachine — robot arm likely triggered the button." )
1388+
1389+
11891390def test_human_interaction_scripted_domino_solves_task ():
11901391 """Full pipeline: human_interaction approach with scripted option plan
11911392 (domino2.txt) solves the 1st test task in pybullet_domino."""
0 commit comments