Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/sensors/tactile_franka.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def _add_tactile_sensor(
return scene.add_sensor(
gs.sensors.KinematicTaxel(
probe_local_pos=probe_local_pos,
probe_local_normal=probe_normal,
probe_radius=0.002,
normal_stiffness=5000.0,
normal_damping=1.0,
Expand Down
186 changes: 112 additions & 74 deletions examples/sensors/tactile_sandbox.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Interactive demo of tactile sensors on a fixed taxel pad (box or dome) with controllable objects.
Sensor types: ContactDepthProbe, ElastomerTaxel, KinematicTaxel, ProximityTaxel.
Sensor types: ContactDepthProbe, ContactProbe, ElastomerTaxel, KinematicTaxel, ProximityTaxel.

Note that the sensor readings here have not been calibrated to any units, and is purely for visualization purposes.
"""
Expand All @@ -24,9 +24,9 @@
from genesis.engine.entities.rigid_entity import RigidEntity
from genesis.engine.sensors.base_sensor import Sensor

KEY_DPOS = 0.005
KEY_DPOS = 0.001
FORCE_SCALE = 100.0
ROT_FORCE_SCALE = 200.0
ROT_FORCE_SCALE = 100.0

GRID_SIZE = 20 # 20x20 taxels for square
PROBE_RADIUS = 0.004
Expand All @@ -48,13 +48,26 @@ def _add_tactile_sensor(
probe_local_pos: np.ndarray,
probe_normal: tuple[float, float, float] | np.ndarray,
track_link_idx: tuple[int, ...],
contact_depth_query: str | None,
noise: bool,
) -> "Sensor":
common = dict(
entity_idx=entity.idx,
link_idx_local=link_idx_local,
draw_debug=True,
probe_radius=PROBE_RADIUS,
contact_depth_query=contact_depth_query,
)
if noise:
# Sensor imperfections shared by every tactile sensor type: viscoelastic hysteresis on the measured
# branch, a noised sensing radius, and a per-taxel measured-branch depth gain. Grid-capable taxel sensors
# additionally get spatial crosstalk (see ``grid_crosstalk_kwargs`` below).
common.update(
hysteresis_strength=0.5, # viscoelastic overshoot fraction
hysteresis_tau=0.1, # viscoelastic relaxation time constant (seconds)
probe_radius_noise=0.001, # additive sensing-radius noise (meters)
probe_gain=1.5, # per-taxel measured-branch depth gain
)
if sensor_type == "elastomer":
return scene.add_sensor(
gs.sensors.ElastomerTaxel(
Expand All @@ -63,45 +76,70 @@ def _add_tactile_sensor(
track_link_idx=track_link_idx,
n_sample_points=2000,
dilate_scale=1.0,
shear_scale=100.0,
shear_scale=2.0,
normal_exponent=1.0,
**common,
)
)

grid_local_pos = probe_local_pos # (ny, nx, 3) for the plane grid; flattened below for the non-grid sensors
is_grid = probe_local_pos.ndim == 3
probe_local_pos = probe_local_pos.reshape(-1, 3)
# Spatial crosstalk needs a regular grid layout, so enable it under --noise only for the grid-capable taxel
# sensors (and only the plane grid, not the dome). The conservative 3x3 kernel sums to 1 (center 0.6), so it
# bleeds ~40% of each taxel's measured force/torque onto its neighbors without changing the total.
grid_crosstalk_kwargs = (
dict(
probe_local_pos=grid_local_pos,
crosstalk_kernel=[[0.03, 0.07, 0.03], [0.07, 0.60, 0.07], [0.03, 0.07, 0.03]],
)
if noise and is_grid
else dict(probe_local_pos=probe_local_pos)
)
if sensor_type == "depth":
return scene.add_sensor(
gs.sensors.ContactDepthProbe(
probe_local_pos=probe_local_pos,
**common,
)
)
if sensor_type == "contact":
# Schmitt-trigger thresholds (contact depth in meters): a taxel latches on above contact_threshold and
# only releases once the depth drops back below the lower release_threshold.
return scene.add_sensor(
gs.sensors.ContactProbe(
probe_local_pos=probe_local_pos,
contact_threshold=0.004,
release_threshold=0.002,
**common,
)
)
if sensor_type == "kinematic":
return scene.add_sensor(
gs.sensors.KinematicTaxel(
probe_local_pos=probe_local_pos,
probe_local_normal=probe_normal,
normal_stiffness=500.0,
normal_damping=1.0,
shear_scalar=5.0,
twist_scalar=5.0,
shear_scalar=4.0,
twist_scalar=4.0,
normal_exponent=1.5,
**grid_crosstalk_kwargs,
**common,
)
)

common["probe_radius"] = PROBE_RADIUS * 2
common["debug_point_cloud_radius"] = 0.001
common["probe_radius"] = PROBE_RADIUS * 5
if sensor_type == "proximity":
return scene.add_sensor(
gs.sensors.ProximityTaxel(
probe_local_pos=probe_local_pos,
track_link_idx=track_link_idx,
n_sample_points=4000,
stiffness=200.0,
shear_coupling=100.0,
stiffness=40.0,
shear_coupling=10.0,
probe_local_normal=probe_normal,
probe_radius_noise=0.0001,
debug_point_cloud_radius=0.0005,
debug_probe_color=(0.2, 0.6, 1.0),
debug_contact_color=(1.0, 0.2, 0.2),
**grid_crosstalk_kwargs,
**common,
)
)
Expand All @@ -111,66 +149,48 @@ def _add_tactile_sensor(
def _plot_tactile_sensor(
scene: gs.Scene,
sensor_type: str,
labels: tuple[str, ...],
sensors: "tuple[Sensor, ...]",
sensor: "Sensor",
n_envs: int = 1,
plot_normal: tuple[float, float, float] = (0.0, 0.0, -1.0),
) -> None:
"""Set up a single live plot window: one vector-field subplot per environment for the per-taxel sensors, or one
line plot with a line per environment for the scalar (depth / contact-count) sensors."""
if not IS_MATPLOTLIB_AVAILABLE:
print("Matplotlib not available; skipping plot setup.")
return

if sensor_type == "elastomer":
for env_idx in range(n_envs):
for label, sensor in zip(labels, sensors):
scene.start_recording(
lambda s=sensor, i=env_idx: s.read()[i],
gs.recorders.MPLVectorFieldPlot(
title=f"({label} {OBJ_PER_ENV_LABELS[env_idx]}) ElastomerTaxel marker displacements",
positions=sensor.probe_local_pos.reshape(-1, 3),
normal=plot_normal,
scale_factor=1.0,
max_magnitude=0.01,
),
)
elif sensor_type == "kinematic":
for env_idx in range(n_envs):
for label, sensor in zip(labels, sensors):
scene.start_recording(
lambda s=sensor, i=env_idx: s.read().force[i],
gs.recorders.MPLVectorFieldPlot(
title=f"({label} {OBJ_PER_ENV_LABELS[env_idx]}) KinematicTaxel force",
positions=sensor.probe_local_pos.reshape(-1, 3),
normal=plot_normal,
scale_factor=0.01,
max_magnitude=1.0,
),
)
elif sensor_type == "proximity":
for env_idx in range(n_envs):
for label, sensor in zip(labels, sensors):
scene.start_recording(
lambda s=sensor, i=env_idx: s.read().force[i],
gs.recorders.MPLVectorFieldPlot(
title=f"({label} {OBJ_PER_ENV_LABELS[env_idx]}) ProximityTaxel force",
positions=sensor.probe_local_pos.reshape(-1, 3),
normal=plot_normal,
scale_factor=0.5,
max_magnitude=1.0,
),
)
elif sensor_type == "depth":
for env_idx in range(n_envs):
scene.start_recording(
lambda i=env_idx: tuple(sensor.read()[i].max() for sensor in sensors),
gs.recorders.MPLLinePlot(
title=f"ContactDepthProbe max depth ({OBJ_PER_ENV_LABELS[env_idx]})",
labels=labels,
x_label="step",
y_label="depth",
history_length=200,
),
)
env_titles = OBJ_PER_ENV_LABELS[:n_envs]

# Per-taxel vector-field sensors: one subplot per env, sharing the probe layout. data_func returns (n_envs, N, 3).
VECTOR_FIELD_SETUP = {
"elastomer": ("ElastomerTaxel marker displacements", 0.1, 0.1, lambda: sensor.read()),
"kinematic": ("KinematicTaxel force", 0.01, 1.0, lambda: sensor.read().force),
"proximity": ("ProximityTaxel force", 0.1, 1.0, lambda: sensor.read().force),
}
if sensor_type in VECTOR_FIELD_SETUP:
title, scale_factor, max_magnitude, read_field = VECTOR_FIELD_SETUP[sensor_type]
scene.start_recording(
lambda: tensor_to_array(read_field()).reshape(n_envs, -1, 3),
gs.recorders.MPLVectorFieldPlot(
title=title,
positions=sensor.probe_local_pos.reshape(-1, 3),
normal=plot_normal,
scale_factor=scale_factor,
max_magnitude=max_magnitude,
subplot_titles=env_titles,
),
)
return

# Scalar sensors: one line per env in a single plot. data_func returns one value per env.
title, y_label, reduce_fn = {
"depth": ("ContactDepthProbe max depth", "depth", lambda r: float(r.max())),
"contact": ("ContactProbe taxels in contact", "# taxels", lambda r: float(r.sum())),
}[sensor_type]
scene.start_recording(
lambda: tuple(reduce_fn(sensor.read()[i]) for i in range(n_envs)),
gs.recorders.MPLLinePlot(title=title, x_label="step", y_label=y_label, history_length=200, labels=env_titles),
)


def _print_sensor_reading(sensor_type: str, sensor: "Sensor", t: float) -> None:
Expand All @@ -183,6 +203,10 @@ def _print_sensor_reading(sensor_type: str, sensor: "Sensor", t: float) -> None:
max_depth = data.max()
if max_depth > gs.EPS:
print(f"t={t:.2f}s max depth={max_depth:.4f}")
elif sensor_type == "contact":
n_contact = int(data.sum())
if n_contact > 0:
print(f"t={t:.2f}s taxels in contact={n_contact}")
elif sensor_type == "kinematic":
magnitude = torch.linalg.norm(data.force, axis=-1).max()
if magnitude > gs.EPS:
Expand All @@ -204,10 +228,22 @@ def main() -> None:
parser.add_argument("--dome", action="store_true", help="Change the sensor object to a dome instead of a box")
parser.add_argument(
"--sensor",
choices=("elastomer", "depth", "kinematic", "proximity"),
choices=("elastomer", "depth", "contact", "kinematic", "proximity"),
default="elastomer",
help="Type of tactile sensor to use.",
)
parser.add_argument(
"--contact-depth-query",
choices=("sdf", "raycast"),
default=None,
help="Contact-depth backend for the tactile sensor (default: sensor's own default, currently sdf).",
)
parser.add_argument(
"--noise",
action="store_true",
help="Enable sensor imperfections (viscoelastic hysteresis, probe_radius_noise, probe_gain, and spatial "
"crosstalk on grid taxel sensors).",
)
args = parser.parse_args()

gs.init(
Expand Down Expand Up @@ -275,17 +311,17 @@ def main() -> None:
ny=GRID_SIZE,
)

# Procedural torus written to a temp .obj (avoids checking a 2k-line mesh into the repo).
torus_path = os.path.join(tempfile.gettempdir(), "tactile_sandbox_torus.obj")
if not os.path.exists(torus_path):
trimesh.creation.torus(major_radius=0.3, minor_radius=0.1).export(torus_path)
trimesh.creation.torus(major_radius=1.0, minor_radius=0.5).export(torus_path)

obj = scene.add_entity(
morph=[
gs.morphs.Mesh(
file=torus_path,
euler=(90.0, 0.0, 0.0),
scale=OBJECT_SIZE,
euler=(0.0, 0.0, 0.0),
scale=OBJECT_SIZE / 2,
convexify=False,
),
gs.morphs.Sphere(
radius=OBJECT_SIZE / 2,
Expand Down Expand Up @@ -313,9 +349,11 @@ def main() -> None:
probe_local_pos,
probe_normal,
track_link_idx=(obj.base_link_idx,),
contact_depth_query=args.contact_depth_query,
noise=args.noise,
)
if args.vis and "PYTEST_VERSION" not in os.environ:
_plot_tactile_sensor(scene, args.sensor, ("",), (sensor,), n_envs=4, plot_normal=probe_normal_axis)
_plot_tactile_sensor(scene, args.sensor, sensor, n_envs=4, plot_normal=probe_normal_axis)
scene.build(n_envs=4, env_spacing=(SENSOR_OBJ_SIZE * 1.2, SENSOR_OBJ_SIZE * 1.2))

obj_init_pos = tensor_to_array(obj.get_pos())
Expand Down Expand Up @@ -381,7 +419,7 @@ def rotate(axis_idx: int, is_negative: bool):
print("\n=== Tactile Sensor Sandbox ===")
n_taxels = probe_local_pos.reshape(-1, 3).shape[0]
layout = f"dome ({GRID_SIZE} latitude rings)" if args.dome else f"plane grid {probe_local_pos.shape[:-1]}"
print(f"sensor={args.sensor}; taxels={n_taxels}; {layout}")
print(f"sensor={args.sensor}; taxels={n_taxels}; {layout}; noise={'on' if args.noise else 'off'}")
if args.vis and IS_MATPLOTLIB_AVAILABLE:
print("Matplotlib live plot enabled when supported.")
if args.vis:
Expand Down
Loading