Skip to content

simulo.ParallelGripperActuator

Parallel-jaw (two-finger) gripper, driven by finger-joint position targets.

The second concrete actuator, and the one that makes Actuator a pattern rather than a one-off. It honours exactly the same command and state contract as SurfaceGripperActuatorset_command / set_commands / get_state / is_open / is_closed / update / reset — so a task written against one gripper reads the same against the other, and swapping them changes the construction line only.

Without it, closing a Franka’s fingers means writing the widths by hand:

robot.set_joint_position_target(
[0.04, 0.04], joint_ids=robot.find_joints("panda_finger_joint.*")
)

— the same magic-number problem GripperCommand fixes one level up.

Command Interface: Identical to SurfaceGripperActuator (that is the point):

  • command < open_threshold: drive the fingers to open_width
  • open_threshold ≤ command ≤ close_threshold: idle — hold the current target
  • command > close_threshold: drive the fingers to closed_width

Accepts a GripperCommand (scripted intent) or a raw float (a policy’s continuous action). set_command applies one value to every environment; set_commands takes one value per environment, and accepts a tensor so a policy’s action column can be passed straight through.

State Interface: get_state() returns one GripperState per environment. A parallel gripper has no physics-constraint “attached” flag to read, so the state is inferred from the finger joints themselves, and the inference is honest about what it can and cannot see:

  • CLOSING (0) — commanded closed, fingers still moving: the last non-idle command was a close AND the gripper is not yet CLOSED.

  • CLOSED (1) — commanded closed and the fingers have stopped: every finger joint’s |velocity| ≤ velocity_tolerance and every finger joint has travelled more than position_tolerance away from open_width, on two consecutive update calls. Stopping is the test rather than “reached closed_width” precisely because a successful grasp does not reach closed_width — the fingers stall on the object at its own width.

    Three details of that sentence are load-bearing, each because the obvious cheaper version is wrong:

    • Every finger, not the average. Fingers at [0.04, 0.00] against open_width=0.04 average to 0.02 and would report a settled grasp with one jaw jammed fully open.
    • Two consecutive updates, not one. A damped oscillation crosses zero velocity every half-period, and a single-sample test reports CLOSED on the crossing — before the grasp exists.
    • Once reported, CLOSED sticks until the gripper is commanded open or reset. A held object that shifts does not un-grasp itself, and without the latch a settling wobble flips the state repeatedly (measured: 13 transitions in one simulated second).
  • OPEN (-1) — anything else.

What CLOSED does NOT mean. It does not mean an object is held. A closed empty gripper, a gripper stalled on an ungrippable obstacle, and a gripper holding a block are all CLOSED — the fingers stopped in every case. This actuator observes the fingers, not the world, and does not pretend otherwise; if your task needs to know whether something is actually held, check the object.

There is no OPENING state, because GripperState has none. A gripper commanded open reports OPEN immediately, before the fingers have finished travelling. Only the closing direction is resolved against measured motion.

Example:

import simulo
class LiftTask(simulo.Task):
def build(self, scene: simulo.Scene) -> None:
self.robot = simulo.Robot(asset=franka, initial_pose=simulo.Pose.identity())
scene.add(self.robot, at="/World/Robot")
def on_start(self, env: simulo.LearningEnv) -> None:
# Constructed against an already-built Robot, like an IK controller.
self.gripper = simulo.ParallelGripperActuator(
robot=self.robot,
finger_joints="panda_finger_joint.*",
open_width=0.04,
closed_width=0.0,
)
def apply_actions(self, actions: torch.Tensor) -> None:
# Continuous policy output, one value per environment.
self.gripper.set_commands(actions[:, -1])
def get_rewards(self) -> torch.Tensor:
self.gripper.update(self.env.dt)
grasped = self.gripper.get_state()
...

Backend Mapping: None of its own — and that is the design. A surface gripper is a physics constraint that must be spawned at a prim path, so it is added to the scene (scene.add(gripper, at=...)) and the engine creates an object for it. A parallel gripper is not a new object at all: it IS two joints the robot’s articulation already has. So this actuator is constructed against an already-built Robot — exactly like DifferentialIKController, the SDK’s other “commands a robot you already have” type — and writes through the robot’s own set_joint_position_target command path.

Consequences worth knowing:

  • Do not scene.add(...) it or robot.add_actuator(...) it. There is nothing to spawn; the engine will tell you so.
  • Construct it in on_start (or later), not in build — it reads the robot’s joints, which do not exist until the scene is built. It is inert until then.
  • Commands are still buffered and flushed automatically before the next physics step, one level down: they land in the robot’s own command buffer, which the scene flushes — the same path apply_actions uses for every other joint.
  • Nothing else should drive the finger joints while this is active, the same rule the controllers carry.

Attributes:

  • robot — The robot whose finger joints this gripper drives.
  • finger_joints — Joint-name pattern selecting the finger joints (regex or exact name), resolved with robot.find_joints. Assigning a new pattern re-resolves it on the next command or update.
  • open_width — Finger joint position, in metres, that counts as open.
  • closed_width — Finger joint position, in metres, that counts as closed.
  • open_threshold — Command threshold for opening (< this value).
  • close_threshold — Command threshold for closing (> this value).
  • position_tolerance — How far, in metres, the fingers must travel from open_width before a settled gripper counts as CLOSED.
  • velocity_tolerance — Finger joint speed, in m/s, at or below which the fingers count as stopped.

Note: What is live after construction, and what is not. open_width, closed_width, the thresholds, the tolerances and finger_joints are all re-read on each use, so assigning any of them takes effect on the next command — but they are re-read, not re-validated: the constructor’s guards do not run again. Configure through the constructor and treat post-construction assignment as a debugging tool.

Two behaviours differ from the sibling robot.state, deliberately and worth stating plainly rather than discovering:

  • Commands issued before the robot is attached are dropped, not buffered. set_command on an unattached gripper returns quietly and nothing is replayed when the runtime attaches. Construct the gripper in on_start and command it from there.
  • get_state() answers [OPEN] before attach rather than raising, where robot.state’s members raise RuntimeError. The gripper is deliberately inert pre-attach — but that means an OPEN read early in a lifecycle may mean “nothing is connected yet” rather than “the fingers are open”.

Close on a block and wait for the grasp to land, then lift:

def on_start(self) -> None:
self.gripper = simulo.ParallelGripperActuator(
robot=self.robot,
finger_joints="panda_finger_joint.*",
open_width=0.04,
closed_width=0.0,
)
self.gripper.set_command(simulo.GripperCommand.OPEN)
def on_step(self) -> None:
self.gripper.set_command(simulo.GripperCommand.CLOSE)
self.gripper.update(self.simulation.dt)
if self.gripper.is_closed():
... # the fingers have stopped — lift
simulo.ParallelGripperActuator(
robot: Robot,
finger_joints: str,
open_width: float = 0.04,
closed_width: float = 0.0,
open_threshold: float = -0.3,
close_threshold: float = 0.3,
position_tolerance: float = 0.002,
velocity_tolerance: float = 0.01,
path: Optional[str] = None,
enabled: bool = True,
)
ParallelGripperActuator.set_command(command: Union[GripperCommand, float]) -> None

Set one command, applied to every environment.

Args:

  • command — A GripperCommand (OPEN / IDLE / CLOSE) for scripted intent, or a raw float in [-1, 1].

Raises:

  • ValueError — If given a tensor or sequence holding more than one value — that is per-environment control, which is set_commands.
  • TypeError — If given a GripperState — see _as_command().
ParallelGripperActuator.set_commands(
commands: Union[Sequence[Union[GripperCommand, float]], torch.Tensor],
) -> None

Set commands for every environment.

Args:

  • commands — One command value per environment — a list of GripperCommand / floats (the two may be mixed), or a tensor, so a policy’s action column passes straight through: gripper.set_commands(actions[:, -1]).

Raises:

  • ValueError — If the count does not match the environment count. The check is unconditional: it used to be skipped whenever the count was one — which is also what “not attached yet” reports — so a 5-command batch for 1 environment reached the tensor layer and failed there as IndexError: The shape of the mask [5]… instead of naming the input.
  • TypeError — If any element is a GripperState.
ParallelGripperActuator.get_state() -> List[GripperState]

Get current gripper state for all environments.

Returns:

List of GripperState values, one per environment: - OPEN (-1): Not closing - CLOSING (0): Commanded closed, fingers still moving - CLOSED (1): Commanded closed, fingers stopped away from open Before the first update() the list is a single OPEN placeholder regardless of the environment count — the per- environment answer does not exist until the finger joints have been read. update(dt) sizes it; read it after, not before.

ParallelGripperActuator.is_open(instance: int = 0) -> bool

Check if a gripper instance is open.

Args:

  • instance — Environment index

Returns:

True if the gripper is not under a close command. Note this reads True as soon as an open is commanded, before the fingers finish travelling — GripperState has no OPENING member.

ParallelGripperActuator.is_closed(instance: int = 0) -> bool

Check if a gripper instance is closed.

Args:

  • instance — Environment index

Returns:

True if the fingers have stopped away from the open position under a close command (an empty closed gripper and one holding an object both report True — see the class docstring).

ParallelGripperActuator.update(dt: float) -> None

Refresh the gripper state from the finger joints.

Args:

  • dt — Time step in seconds. Accepted for parity with SurfaceGripperActuator.update() and unused: the state is inferred from the finger joints’ reported positions and velocities, which the engine already advances, so no time integration happens here.
ParallelGripperActuator.reset(env_ids: Optional[Any] = None) -> None

Reset environments to open: clear the latches, command the fingers open.

Args:

  • env_ids — Environment indices to reset — the tensor Task.reset_idx receives, a sequence of ints, or None for every environment. Pass the ids in an RL task. This actuator is never registered with the scene, so Scene.reset() does not reach it and nothing resets it on your behalf; reset_idx is the only hook that runs. Calling reset() with no arguments from there means one environment terminating physically opens the grippers of every other environment in the batch, mid-grasp — at 2048 environments, one termination drops 2047 held objects.