Grasping world
Warning
qrobot_simulator is experimental and its interfaces may change between minor releases.
A stationary gripper combines proximity with contact feedback while a ball moves
stochastically along its sensor axis. The same world accepts a ReactiveGripper,
ClassicalGripper, or QuantumGripper.
The simulation isolates a single perception-to-action problem. A blue ball moves toward and away from the stationary gripper. The gripper must close while the ball is between its jaws, use the internal touch sensor to recognize a successful catch, and reopen in time for the next approach. The world records catches, missed visits, empty closures, and response times. It supplies all three grippers with the same sensor interface and applies their commands to the same physical interaction.
Setup
The experiment contains three components: a stationary gripper, a ball moving along the gripper’s sensor axis, and an arena that computes sensing and contact. The orange region in the figure is the complete distance-sensor range. Its inner green portion is the space enclosed by the jaws. The touch pads lie inside the jaws, so they report contact only after a closure reaches the ball.
The sensor limits below define the distance normalization, while the grippable distance marks how far the jaws can reach. The prey consumption time is how long a successful catch must remain between the closed jaws before the prey disappears. Ball speed determines how quickly this geometry changes.
import pandas as pd
from qrobot_simulator.grasping_world.robots.config import (
BALL_PREY_CONFIG,
CLASSICAL_GRIPPER_CONFIG,
QUANTUM_GRIPPER_CONFIG,
REACTIVE_GRIPPER_CONFIG,
)
from qrobot_simulator.grasping_world.world.config import WORLD_CONFIG
setup = {
"sensor near / far (cm)": f"{WORLD_CONFIG.near_distance:g} / {WORLD_CONFIG.far_distance:g}",
"grippable distance (cm)": WORLD_CONFIG.grippable_distance,
"prey consumption time (s)": WORLD_CONFIG.consumption_time,
"ball maximum speed (cm/s)": BALL_PREY_CONFIG.max_speed,
}
pd.DataFrame.from_dict(setup, orient="index", columns=["value"])
| value | |
|---|---|
| sensor near / far (cm) | 5 / 20 |
| grippable distance (cm) | 15.0 |
| prey consumption time (s) | 2.5 |
| ball maximum speed (cm/s) | 7.0 |
The left plot shows how physical distance becomes a qUnit input. Objects inside the
near limit produce 1, objects beyond the far limit produce 0, and intermediate
distances are interpolated. This direction matches the proximity query: a larger
input means a closer object. The right plot shows the two touch states. An empty
gripper produces 1; contact with a ball produces 0.
import matplotlib.pyplot as plt
import numpy as np
from qrobot_simulator.grasping_world.utils.sensors import proximity_reading, touch_reading
distances = np.linspace(0, BALL_PREY_CONFIG.max_distance, 200)
values = [
proximity_reading(d, near=WORLD_CONFIG.near_distance, far=WORLD_CONFIG.far_distance)
for d in distances
]
fig, (distance_axis, touch_axis) = plt.subplots(1, 2, figsize=(9, 3))
distance_axis.plot(distances, values)
distance_axis.set(xlabel="Distance (cm)", ylabel="Proximity", ylim=(-0.05, 1.05))
touch_axis.bar(["empty", "contact"], [touch_reading(False), touch_reading(True)])
touch_axis.set(ylabel="Touch input", ylim=(0, 1.05))
fig.tight_layout()
plt.show()
Reactive gripper
All grippers see only normalized proximity and touch; none reads the ball’s position directly. They return one normalized command, where values above the gripper threshold close the jaws.
ReactiveGripperBrain is the no-memory reference. It closes as soon as the current
proximity reading crosses its threshold. While closed, current touch contact keeps
the jaws closed; an empty touch reading opens them. A single brief or corrupted
sample can therefore cause it to react.
pd.DataFrame.from_dict(
{
"proximity threshold": REACTIVE_GRIPPER_CONFIG.proximity_threshold,
"touch-contact threshold": REACTIVE_GRIPPER_CONFIG.contact_threshold,
"jaw activation threshold": REACTIVE_GRIPPER_CONFIG.gripper_threshold,
},
orient="index",
columns=["value"],
)
| value | |
|---|---|
| proximity threshold | 0.5 |
| touch-contact threshold | 0.5 |
| jaw activation threshold | 0.5 |
Classical gripper
ClassicalGripperBrain is the deterministic temporal comparison. It divides each
sensor stream into fixed, non-overlapping windows and computes the arithmetic mean
of every completed window. The proximity and empty-gripper means then enter the
same strict mean threshold used by the quantum actuator. Its sampling period and
window lengths equal those of the two qUnits, separating temporal memory from the
AngularModel and its stochastic measurement.
Its timing and decision thresholds are printed from ClassicalGripperConfig:
pd.DataFrame.from_dict(
{
"sampling period (s)": CLASSICAL_GRIPPER_CONFIG.sampling_period,
"proximity window (s)": CLASSICAL_GRIPPER_CONFIG.sampling_period
* CLASSICAL_GRIPPER_CONFIG.proximity_tau,
"empty-gripper window (s)": CLASSICAL_GRIPPER_CONFIG.sampling_period
* CLASSICAL_GRIPPER_CONFIG.empty_gripper_tau,
"jaw activation threshold": CLASSICAL_GRIPPER_CONFIG.gripper_threshold,
},
orient="index",
columns=["value"],
)
| value | |
|---|---|
| sampling period (s) | 0.1 |
| proximity window (s) | 1.0 |
| empty-gripper window (s) | 5.0 |
| jaw activation threshold | 0.5 |
The source below is the actual brain class imported from the package:
from inspect import getsource
from IPython.display import Code
from qrobot_simulator.grasping_world.robots.classical_gripper import ClassicalGripperBrain
Code(getsource(ClassicalGripperBrain), language="python")
class ClassicalGripperBrain(BaseGripperBrain):
"""Close and open the jaws from recent proximity and touch readings.
The brain averages proximity over a short period, so one isolated near
reading is insufficient to close the jaws. It averages touch over a longer
period. An empty gripper produces a high touch value and contact produces a
low one. The averages are combined into one command: strong recent proximity
while the gripper is available favors closing; losing proximity or accumulating
contact evidence favors opening.
The two averaging periods equal the periods observed by the quantum brain.
This makes it possible to compare ordinary averaging with the AngularModel
without giving either robot a longer sensor history.
"""
def __init__(self, config: ClassicalGripperConfig) -> None:
"""Create separate averages for ball proximity and jaw contact."""
self.config = config
self.proximity = FixedWindowIntegrator(
config.sampling_period,
config.proximity_tau,
initial_value=0.0,
)
self.empty_gripper = FixedWindowIntegrator(
config.sampling_period,
config.empty_gripper_tau,
initial_value=1.0,
)
self._activation = 0.0
def start(self, readings: Readings) -> None:
"""Initialize both outputs from the sensor state before simulated time."""
self.proximity.reset(readings["proximity"])
self.empty_gripper.reset(readings["touch"])
self._activation = self._actuator_output()
@property
def ready(self) -> bool:
"""Return true because both initial outputs are immediately available."""
return True
def command(self, readings: Readings, dt: float) -> float:
"""Update both sensor averages and return an open or closed command."""
self.proximity.update(readings["proximity"], dt)
self.empty_gripper.update(readings["touch"], dt)
self._activation = self._actuator_output()
return self._activation
def stop(self) -> None:
"""Accept the common lifecycle call; no resources require cleanup."""
def diagnostics(self) -> Diagnostics:
"""Return the sensor averages and current jaw command for display."""
return {
"proximity_mean": self.proximity.output,
"empty_gripper_mean": self.empty_gripper.output,
"gripper_activation": self._activation,
}
def _actuator_output(self) -> float:
"""Use the same strict normalized-mean threshold as ActuatorUnit."""
normalized_sum = (self.proximity.output + self.empty_gripper.output) / 2
return float(normalized_sum > self.config.gripper_threshold)
Quantum gripper
QuantumGripperBrain queries a short proximity history qUnit and a longer empty-gripper
history qUnit. Both measured bursts feed one actuator, which closes when their mean
exceeds its threshold. The shorter proximity window lets the robot respond to an
approaching ball sooner than the longer touch window responds to contact.
Its sampling, temporal windows, queries, and actuator threshold are taken from
QuantumGripperConfig:
pd.DataFrame.from_dict(
{
"sampling period (s)": QUANTUM_GRIPPER_CONFIG.sampling_period,
"proximity window (s)": QUANTUM_GRIPPER_CONFIG.sampling_period
* QUANTUM_GRIPPER_CONFIG.proximity_tau,
"proximity query": QUANTUM_GRIPPER_CONFIG.proximity_query,
"empty-gripper window (s)": QUANTUM_GRIPPER_CONFIG.sampling_period
* QUANTUM_GRIPPER_CONFIG.empty_gripper_tau,
"empty-gripper query": QUANTUM_GRIPPER_CONFIG.empty_gripper_query,
"jaw activation threshold": QUANTUM_GRIPPER_CONFIG.gripper_threshold,
},
orient="index",
columns=["value"],
)
| value | |
|---|---|
| sampling period (s) | 0.1 |
| proximity window (s) | 1.0 |
| proximity query | (1.0,) |
| empty-gripper window (s) | 5.0 |
| empty-gripper query | (1.0,) |
| jaw activation threshold | 0.5 |
from IPython.display import HTML
from qrobot_qunits import RedisConfig
from qrobot_simulator.grasping_world.robots.quantum_gripper import QuantumGripperBrain
from qrobot_visualization import build_network, draw
quantum_brain = QuantumGripperBrain(RedisConfig())
architecture = draw(build_network(quantum_brain.units))
HTML(architecture.to_html(include_plotlyjs="cdn", full_html=False, config={"responsive": True}))
Blue nodes form the sensor and perceptual path; the green node is the actuator. The arrows identify which outputs become inputs to the next unit. The graph itself reports the configured models, queries, periods, and wiring. The proximity burst rises when recent readings resemble a nearby ball. The empty-gripper burst falls as contact readings accumulate. Their combination supplies the jaw command.
Simulated interaction
The ball receives random changes in velocity, creating approaches, reversals, and near misses rather than a scripted passage. A residence limit prevents it from remaining indefinitely under the gripper. Arena boundaries and closed jaws constrain its motion.
When the jaws close around the ball, the ball is held and the touch input changes. The prey consumption time is the simulated chewing interval for which a caught ball remains between the closed jaws. At its end the consumed ball disappears, making the touch sensor report an empty gripper. The brain must then open the jaws; only that opening makes a new ball appear farther away. Opening before consumption releases the same ball and lets it escape. Closing without the ball counts as an empty grip. A visit ending inside the jaws counts as a correct grip, while an uncaught visit counts as a missed grip. Response time is measured from entry into the grippable region until capture. The renderer shows this world state and the current brain signals without affecting either one.
The random motion is useful for seeing the complete interaction. Controlled
measurements use the separate qrobot_simulator.grasping_world.analysis package,
which can present one predefined ball visit to each robot and record its readings,
commands, physical events, and response times. The experiment repository uses those
records for repeated trials and statistics; they are not part of this demo tutorial.
Run the example
With Redis listening on localhost:6379:
python examples/grasping_world.py --gripper quantum
Use --gripper reactive, --gripper classical, or --gripper quantum to select
the brain. A fixed --seed repeats the random ball motion, while --speed
accelerates the scheduled qBrain up to the limit reported by --help. A bounded
headless run can save its final frame:
python examples/grasping_world.py --gripper quantum --duration 10 --seed 7 --no-show \
--save-world grasping_live_world.png
Fixed headless comparison
Ten paired trials expose the classical and quantum grippers to the same initial conditions and random motion. This is a descriptive implementation check rather than a parameter search or a claim of statistical superiority. The cell prints the experimental setup.
| value | |
|---|---|
| paired trials | 5.00 |
| duration (simulated s) | 60.00 |
| physics step (s) | 0.05 |
| quantum time ratio | 10.00 |
Capture rate is the fraction of completed visits ending in a catch. Response time runs from entry into the grippable region to capture. The median describes a typical trial and the sample standard deviation shows variation among trajectories.
trial_results.groupby("controller", sort=False)[
["correct", "missed", "empty", "capture_rate", "mean_response_s"]
].agg(["median", "std"]).round(3)
| correct | missed | empty | capture_rate | mean_response_s | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| median | std | median | std | median | std | median | std | median | std | |
| controller | ||||||||||
| classical | 0.0 | 1.304 | 3.0 | 1.871 | 4.0 | 1.673 | 0.0 | 0.331 | 0.333 | 0.259 |
| quantum | 3.0 | 0.837 | 0.0 | 0.548 | 1.0 | 0.447 | 1.0 | 0.153 | 1.150 | 0.622 |