@@ -7050,118 +7050,45 @@ def test_merge_entities(is_fixed, merge_fixed_links, show_viewer, tol, monkeypat
70507050@pytest .mark .slow # ~450s
70517051@pytest .mark .required
70527052def test_heterogeneous_physics_parity (show_viewer , tol ):
7053- """Test heterogeneous simulation by comparing against independent homogeneous simulations .
7053+ """Verify a single heterogeneous entity is bit-identical to separate entities of its variants .
70547054
7055- This test verifies that heterogeneous simulation produces identical physics results
7056- to running separate homogeneous simulations for each variant, including per-variant
7057- initial positions.
7055+ Uses the welded-child mesh objects from 'test_convexify' (each has an offset center of mass) so the parity check
7056+ exercises the inertia alignment, not just trivially-symmetric primitives.
70587057 """
7059- n_steps = 20
7060- box_drop_height = 0.05
7061- sphere_drop_height = 0.08
7062-
7063- # Run homogeneous simulation with box only
7064- scene_box = gs .Scene (
7065- show_viewer = False ,
7066- )
7067- scene_box .add_entity (gs .morphs .Plane ())
7068- box_obj = scene_box .add_entity (
7069- gs .morphs .Box (
7070- size = (0.04 , 0.04 , 0.04 ),
7071- pos = (0.0 , 0.0 , box_drop_height ),
7072- )
7073- )
7074- scene_box .build ()
7075- for _ in range (n_steps ):
7076- scene_box .step ()
7077- box_pos = tensor_to_array (box_obj .get_pos ())
7078- box_vel = tensor_to_array (box_obj .get_vel ())
7079-
7080- # Run homogeneous simulation with sphere only
7081- scene_sphere = gs .Scene (
7082- show_viewer = False ,
7083- )
7084- scene_sphere .add_entity (gs .morphs .Plane ())
7085- sphere_obj = scene_sphere .add_entity (
7086- gs .morphs .Sphere (
7087- radius = 0.02 ,
7088- pos = (0.1 , 0.0 , sphere_drop_height ),
7089- ),
7090- )
7091- scene_sphere .build ()
7092- for _ in range (n_steps ):
7093- scene_sphere .step ()
7094- sphere_pos = tensor_to_array (sphere_obj .get_pos ())
7095- sphere_vel = tensor_to_array (sphere_obj .get_vel ())
7058+ n_steps = 100
7059+ drop_height = 0.2
7060+ variants = (("mug_1" , "output.xml" ), ("donut_0" , "output.xml" ), ("cup_2" , "model.xml" ), ("apple_15" , "model.xml" ))
7061+
7062+ def make_morph (name , xml ):
7063+ asset_path = get_hf_dataset (pattern = f"{ name } /*" )
7064+ return gs .morphs .MJCF (file = f"{ asset_path } /{ name } /{ xml } " , pos = (0.0 , 0.0 , drop_height ))
7065+
7066+ # Independent homogeneous reference for each variant.
7067+ homog_pos , homog_vel = [], []
7068+ for name , xml in variants :
7069+ scene = gs .Scene (show_viewer = False )
7070+ scene .add_entity (gs .morphs .Plane ())
7071+ obj = scene .add_entity (make_morph (name , xml ))
7072+ scene .build ()
7073+ for _ in range (n_steps ):
7074+ scene .step ()
7075+ homog_pos .append (tensor_to_array (obj .get_pos ()))
7076+ homog_vel .append (tensor_to_array (obj .get_vel ()))
70967077
7097- # Run heterogeneous simulation with both variants (different sizes AND positions)
7098- # 4 envs with 2 variants: envs 0-1 get box, envs 2-3 get sphere
7099- scene_het = gs .Scene (
7100- show_viewer = show_viewer ,
7101- )
7078+ # Single heterogeneous entity with one variant per environment.
7079+ scene_het = gs .Scene (show_viewer = show_viewer )
71027080 scene_het .add_entity (gs .morphs .Plane ())
7103- # Divergent per-variant yaw offsets, irrelevant to the dynamics of these symmetric primitives dropped flat (so
7104- # the world references still match) but stripped per environment by the relative getters.
7105- box_offset_euler = (0.0 , 0.0 , 30.0 )
7106- sphere_offset_euler = (0.0 , 0.0 , - 45.0 )
7107- morphs_heterogeneous = (
7108- gs .morphs .Box (
7109- size = (0.04 , 0.04 , 0.04 ),
7110- pos = (0.0 , 0.0 , box_drop_height ),
7111- offset_euler = box_offset_euler ,
7112- ),
7113- gs .morphs .Sphere (
7114- radius = 0.02 ,
7115- pos = (0.1 , 0.0 , sphere_drop_height ),
7116- offset_euler = sphere_offset_euler ,
7117- ),
7118- )
7119- het_obj = scene_het .add_entity (morph = morphs_heterogeneous )
7120- scene_het .build (n_envs = 4 )
7121-
7122- # Verify initial positions match per-variant morph.pos
7123- het_pos_init = het_obj .get_pos ()
7124- assert_allclose (het_pos_init [0 , 2 ], box_drop_height , tol = tol )
7125- assert_allclose (het_pos_init [1 , 2 ], box_drop_height , tol = tol )
7126- assert_allclose (het_pos_init [2 , 0 ], 0.1 , tol = tol )
7127- assert_allclose (het_pos_init [2 , 2 ], sphere_drop_height , tol = tol )
7128- assert_allclose (het_pos_init [3 , 0 ], 0.1 , tol = tol )
7129- assert_allclose (het_pos_init [3 , 2 ], sphere_drop_height , tol = tol )
7130-
7131- # The relative getter strips each variant's own offset back to the user frame (identity), while the world frame
7132- # carries the per-environment offset.
7133- box_offset_quat = gu .xyz_to_quat (np .array (box_offset_euler ), rpy = True , degrees = True )
7134- sphere_offset_quat = gu .xyz_to_quat (np .array (sphere_offset_euler ), rpy = True , degrees = True )
7135- assert_allclose (het_obj .get_quat (relative = True ), gu .identity_quat (), tol = tol )
7136- het_quat_world = het_obj .get_quat (relative = False )
7137- assert_allclose (het_quat_world [:2 ], box_offset_quat , tol = tol )
7138- assert_allclose (het_quat_world [2 :], sphere_offset_quat , tol = tol )
7139-
7081+ het_obj = scene_het .add_entity (morph = tuple (make_morph (name , xml ) for name , xml in variants ))
7082+ scene_het .build (n_envs = len (variants ))
71407083 for _ in range (n_steps ):
71417084 scene_het .step ()
7142- het_pos = het_obj .get_pos ()
7143- het_vel = het_obj .get_vel ()
7144-
7145- # Verify heterogeneous results match homogeneous results
7146- # Envs 0-1 should match box simulation
7147- assert_allclose (het_pos [0 ], box_pos , tol = tol )
7148- assert_allclose (het_pos [1 ], box_pos , tol = tol )
7149- assert_allclose (het_vel [0 ], box_vel , tol = tol )
7150- assert_allclose (het_vel [1 ], box_vel , tol = tol )
7151-
7152- # Envs 2-3 should match sphere simulation
7153- assert_allclose (het_pos [2 ], sphere_pos , tol = tol )
7154- assert_allclose (het_pos [3 ], sphere_pos , tol = tol )
7155- assert_allclose (het_vel [2 ], sphere_vel , tol = tol )
7156- assert_allclose (het_vel [3 ], sphere_vel , tol = tol )
7157-
7158- # Box envs should have same mass, sphere envs should have same mass
7159- mass = het_obj .get_mass ()
7160- assert_allclose (mass [0 ], mass [1 ], tol = tol )
7161- assert_allclose (mass [2 ], mass [3 ], tol = tol )
7162- # Box and sphere should have different masses
7163- with pytest .raises (AssertionError ):
7164- assert_allclose (mass [0 ], mass [2 ], tol = tol )
7085+ het_pos = tensor_to_array (het_obj .get_pos ())
7086+ het_vel = tensor_to_array (het_obj .get_vel ())
7087+
7088+ # Each environment must match the standalone simulation of its variant.
7089+ for i in range (len (variants )):
7090+ assert_allclose (het_pos [i ], homog_pos [i ], tol = tol )
7091+ assert_allclose (het_vel [i ], homog_vel [i ], tol = tol )
71657092
71667093
71677094@pytest .mark .required
0 commit comments