@@ -1662,5 +1662,126 @@ def test_from_zip(self):
16621662 string_spec .compile ()
16631663 self .assertEqual (spec .to_xml (), string_spec .to_xml ())
16641664
1665+ def test_rangefinder_sensor (self ):
1666+ """Test rangefinder sensor with mjSpec, iterative model building."""
1667+ # Raydata field enum values for dataspec bitfield
1668+ rd = mujoco .mjtRayDataField
1669+ dist_val = int (rd .mjRAYDATA_DIST )
1670+ dir_val = int (rd .mjRAYDATA_DIR )
1671+ origin_val = int (rd .mjRAYDATA_ORIGIN )
1672+ point_val = int (rd .mjRAYDATA_POINT )
1673+ normal_val = int (rd .mjRAYDATA_NORMAL )
1674+ depth_val = int (rd .mjRAYDATA_DEPTH )
1675+
1676+ # Step 1: Create a rangefinder sensor attached to a site, no dataspec set.
1677+ # Note: site goes on a child body because rangefinder excludes the site's
1678+ # parent body from ray casting.
1679+ spec = mujoco .MjSpec ()
1680+ sensor_body = spec .worldbody .add_body (name = 'sensor_body' , pos = [0 , 0 , 1 ])
1681+ sensor_body .add_site (name = 'rf_site' , zaxis = [0 , 0 , - 1 ])
1682+ rf_sensor = spec .add_sensor (
1683+ name = 'rf' ,
1684+ type = mujoco .mjtSensor .mjSENS_RANGEFINDER ,
1685+ objtype = mujoco .mjtObj .mjOBJ_SITE ,
1686+ objname = 'rf_site' ,
1687+ )
1688+
1689+ # This should fail: data spec (intprm[0]) must be positive
1690+ with self .assertRaisesWithPredicateMatch (
1691+ ValueError ,
1692+ lambda e : 'data spec (intprm[0]) must be positive' in str (e )
1693+ ):
1694+ spec .compile ()
1695+
1696+ # Step 2: Set dataspec to just mjRAYDATA_DIST
1697+ rf_sensor .intprm [0 ] = 1 << dist_val
1698+ model = spec .compile ()
1699+ data = mujoco .MjData (model )
1700+ mujoco .mj_forward (model , data )
1701+
1702+ # With no geometry, the ray should miss: dist = -1
1703+ self .assertEqual (model .nsensordata , 1 )
1704+ self .assertEqual (data .bind (rf_sensor ).data [0 ], - 1 )
1705+
1706+ # Step 3: Add all raydata fields and check no-hit values
1707+ all_fields = (
1708+ (1 << dist_val ) | (1 << dir_val ) | (1 << origin_val ) |
1709+ (1 << point_val ) | (1 << normal_val ) | (1 << depth_val )
1710+ )
1711+ rf_sensor .intprm [0 ] = all_fields
1712+ model = spec .compile ()
1713+ data = mujoco .MjData (model )
1714+ mujoco .mj_forward (model , data )
1715+
1716+ # Expected size: dist(1) + dir(3) + origin(3) + point(3) + normal(3) +
1717+ # depth(1) = 14
1718+ self .assertEqual (model .nsensordata , 14 )
1719+
1720+ # No-hit values
1721+ sd = data .bind (rf_sensor ).data
1722+ self .assertEqual (sd [0 ], - 1 ) # dist
1723+ np .testing .assert_allclose (sd [1 :4 ], [0 , 0 , 0 ]) # dir
1724+ np .testing .assert_allclose (sd [4 :7 ], [0 , 0 , 1 ]) # origin
1725+ np .testing .assert_allclose (sd [7 :10 ], [0 , 0 , 0 ]) # point
1726+ np .testing .assert_allclose (sd [10 :13 ], [0 , 0 , 0 ]) # normal
1727+ self .assertEqual (sd [13 ], - 1 ) # depth
1728+
1729+ # Step 4: Add a floor plane, now the ray should hit
1730+ spec .worldbody .add_geom (
1731+ name = 'floor' ,
1732+ type = mujoco .mjtGeom .mjGEOM_PLANE ,
1733+ size = [10 , 10 , 0.1 ],
1734+ )
1735+ model = spec .compile ()
1736+ data = mujoco .MjData (model )
1737+ mujoco .mj_forward (model , data )
1738+
1739+ # Ray starts at z=1 pointing down, hits floor at z=0
1740+ # For site sensor, depth = dist
1741+ sd = data .bind (rf_sensor ).data
1742+ self .assertAlmostEqual (sd [0 ], 1.0 , places = 6 ) # dist
1743+ np .testing .assert_allclose (sd [1 :4 ], [0 , 0 , - 1 ], atol = 1e-10 ) # dir
1744+ np .testing .assert_allclose (sd [4 :7 ], [0 , 0 , 1 ], atol = 1e-10 ) # origin
1745+ np .testing .assert_allclose (sd [7 :10 ], [0 , 0 , 0 ], atol = 1e-10 ) # point
1746+ np .testing .assert_allclose (sd [10 :13 ], [0 , 0 , 1 ], atol = 1e-10 ) # normal
1747+ self .assertAlmostEqual (sd [13 ], 1.0 , places = 6 ) # depth
1748+
1749+ # Step 5: Add a camera-based rangefinder sensor
1750+ # Camera also on child body so it doesn't exclude the floor
1751+ cam_body = spec .worldbody .add_body (name = 'cam_body' , pos = [0 , 0 , 2 ])
1752+ cam_body .add_camera (
1753+ name = 'rf_cam' ,
1754+ xyaxes = [1 , 0 , 0 , 0 , 1 , 0 ], # z=[0,0,1], looks along -z (down)
1755+ resolution = [3 , 3 ],
1756+ fovy = 90 ,
1757+ )
1758+ cam_sensor = spec .add_sensor (
1759+ name = 'rf_cam_sensor' ,
1760+ type = mujoco .mjtSensor .mjSENS_RANGEFINDER ,
1761+ objtype = mujoco .mjtObj .mjOBJ_CAMERA ,
1762+ objname = 'rf_cam' ,
1763+ intprm = [(1 << dist_val ) | (1 << depth_val ), 0 , 0 ],
1764+ )
1765+
1766+ model = spec .compile ()
1767+ data = mujoco .MjData (model )
1768+ mujoco .mj_forward (model , data )
1769+
1770+ # Site sensor: 14 values, Camera sensor: (1+1)*9 = 18 values
1771+ self .assertEqual (model .nsensordata , 14 + 18 )
1772+
1773+ # Check camera sensor data using bind
1774+ cam_sd = data .bind (cam_sensor ).data
1775+ stride = 2 # dist + depth per pixel
1776+ center_pixel = 4 # center of 3x3 = row 1, col 1
1777+
1778+ # Center pixel: ray straight down from z=2 to z=0
1779+ self .assertAlmostEqual (cam_sd [center_pixel * stride ], 2.0 , places = 6 )
1780+ self .assertAlmostEqual (cam_sd [center_pixel * stride + 1 ], 2.0 , places = 6 )
1781+
1782+ # Corner pixel: off-axis ray, dist > depth
1783+ self .assertGreater (cam_sd [0 ], cam_sd [1 ]) # dist > depth
1784+ self .assertAlmostEqual (cam_sd [1 ], 2.0 , places = 6 ) # depth is still 2.0
1785+
16651786if __name__ == '__main__' :
16661787 absltest .main ()
0 commit comments