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 SurfaceGripperActuator — set_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 toopen_widthopen_threshold ≤ command ≤ close_threshold: idle — hold the current targetcommand > close_threshold: drive the fingers toclosed_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 yetCLOSED. -
CLOSED(1) — commanded closed and the fingers have stopped: every finger joint’s|velocity| ≤ velocity_toleranceand every finger joint has travelled more thanposition_toleranceaway fromopen_width, on two consecutiveupdatecalls. Stopping is the test rather than “reachedclosed_width” precisely because a successful grasp does not reachclosed_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]againstopen_width=0.04average to0.02and 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
CLOSEDon the crossing — before the grasp exists. - Once reported,
CLOSEDsticks 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).
- Every finger, not the average. Fingers at
-
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 orrobot.add_actuator(...)it. There is nothing to spawn; the engine will tell you so. - Construct it in
on_start(or later), not inbuild— 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_actionsuses 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 withrobot.find_joints. Assigning a new pattern re-resolves it on the next command orupdate.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 fromopen_widthbefore a settled gripper counts asCLOSED.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_commandon an unattached gripper returns quietly and nothing is replayed when the runtime attaches. Construct the gripper inon_startand command it from there. get_state()answers[OPEN]before attach rather than raising, whererobot.state’s members raiseRuntimeError. The gripper is deliberately inert pre-attach — but that means anOPENread 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 — liftsimulo.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,)set_command()
Section titled “set_command()”ParallelGripperActuator.set_command(command: Union[GripperCommand, float]) -> NoneSet one command, applied to every environment.
Args:
command— AGripperCommand(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 isset_commands.TypeError— If given aGripperState— see_as_command().
set_commands()
Section titled “set_commands()”ParallelGripperActuator.set_commands( commands: Union[Sequence[Union[GripperCommand, float]], torch.Tensor],) -> NoneSet commands for every environment.
Args:
commands— One command value per environment — a list ofGripperCommand/ 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 asIndexError: The shape of the mask [5]…instead of naming the input.TypeError— If any element is aGripperState.
get_state()
Section titled “get_state()”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.
is_open()
Section titled “is_open()”ParallelGripperActuator.is_open(instance: int = 0) -> boolCheck 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.
is_closed()
Section titled “is_closed()”ParallelGripperActuator.is_closed(instance: int = 0) -> boolCheck 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).
update()
Section titled “update()”ParallelGripperActuator.update(dt: float) -> NoneRefresh the gripper state from the finger joints.
Args:
dt— Time step in seconds. Accepted for parity withSurfaceGripperActuator.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.
reset()
Section titled “reset()”ParallelGripperActuator.reset(env_ids: Optional[Any] = None) -> NoneReset environments to open: clear the latches, command the fingers open.
Args:
env_ids— Environment indices to reset — the tensorTask.reset_idxreceives, a sequence of ints, orNonefor every environment. Pass the ids in an RL task. This actuator is never registered with the scene, soScene.reset()does not reach it and nothing resets it on your behalf;reset_idxis the only hook that runs. Callingreset()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.