"""Closed-loop policies for the one-motor VLA: act on the argmax, hedge, or ask when the grounding is ambiguous."""
from __future__ import annotations
import numpy as np
from ..applications.calibration import SplitConformalClassifier
from .motor import OneMotor, expert_command
from .belief import K, _restrict, person_answers
from .scene import cell_centres, clarification
[docs]
def calibrate_gate(model, scenes, alpha=0.1):
"""Split-conformal gate fitted on held-out scenes: ask when the prediction set over cells has more than one cell."""
g = model.ground(np.stack([s.image for s in scenes]), np.stack([s.text for s in scenes]))
return SplitConformalClassifier(alpha=alpha).fit(g, np.array([s.target for s in scenes]))
[docs]
def fuse(g_first, g_answer, eps=1e-6):
"""Classical belief update after an answer: product of the two groundings, renormalised (Bayes with independent evidence)."""
post = g_first * g_answer
return post / post.sum() if post.sum() > eps else g_answer
[docs]
def run_episode(scene, rng, plan, motor=None, steps=60, tol=0.0625, n_cells=16):
"""Roll one episode out. ``plan(scene, rng)`` returns ``(controller, n_questions)``;
``controller(theta, omega)`` is called every control step with the noisy measurement."""
motor = motor or OneMotor()
xs = cell_centres(n_cells)
controller, asked = plan(scene, rng)
th, om = motor.reset(rng.uniform(-1, 1), rng)
for _ in range(steps):
th, om = motor.step(controller(th, om))
goal = xs[scene.target]
nearest = int(np.argmin([abs(motor.theta - xs[c]) for c in scene.cells]))
return {'error': abs(motor.theta - goal), 'success': abs(motor.theta - goal) < tol,
'right_object': scene.cells[nearest] == scene.target, 'asked': asked}
[docs]
def oracle_plan(scene, rng):
t = cell_centres(16)[scene.target]
return (lambda th, om: float(expert_command(t, th, om))), 0
[docs]
def vla_plan(model, gate=None, always_ask=False, slot=None, person=person_answers, n_questions=2):
"""Plan factory. No gate: act on the model's expected target. With a gate: ask when the set is not a single cell.
With a belief ``slot`` the robot asks two questions, the simulated ``person`` answers them, and the slot (Bayes, context-weighted or
quantum, see :mod:`quantum_mind.embodied.belief`) turns the instruction belief and the two answers into the posterior it acts on.
``n_questions=1`` uses only the first answer. The robot then acts on the most likely object. Without a slot it asks one
noise-free question (an idealised answer) and fuses it by the product rule.
"""
xs = cell_centres(model.n_cells)
def plan(scene, rng):
g = model.ground(scene.image, scene.text)
asked = 0
if slot is not None and (always_ask or (gate is not None and gate.predict_sets(g[None])[0].sum() > 1)):
cells = sorted(scene.cells)
m = len(cells)
g0, L, ctx, mask = np.zeros((1, K)), np.zeros((1, 2, K)), np.zeros((1, 2), int), np.zeros((1, K))
mask[0, :m], g0[0, :m] = 1, _restrict(g, cells)
for j, (text, c) in enumerate(person(scene, rng)):
L[0, j, :m], ctx[0, j] = _restrict(model.ground(scene.image, text), cells), c
if j >= n_questions:
L[0, j, :m] = 1.0 / m # unasked: uninformative
post = slot.posterior(g0, L, ctx, mask)[0, :m]
return (lambda th, om, t=float(xs[cells][int(np.argmax(post))]): float(model.act(t, th, om))), n_questions
if always_ask or (gate is not None and gate.predict_sets(g[None])[0].sum() > 1):
text2, _ = clarification(scene)
g = fuse(g, model.ground(scene.image, text2))
asked = 1
t = float(g @ xs)
return (lambda th, om: float(model.act(t, th, om))), asked
return plan
[docs]
def bc_plan(model):
def plan(scene, rng):
return (lambda th, om: model.command(scene.image, scene.text, th, om)), 0
return plan
[docs]
def run_sensed_episode(scene, rng, plan, mode, outage_at=1.0, disturbance=0.0, steps=60, tol=0.0625, n_cells=16):
"""Like :func:`run_episode`, but the controller sees a state *estimate* (see :mod:`quantum_mind.embodied.state`).
The encoder drops out at ``outage_at`` seconds and an unmodelled push of up to ``disturbance`` workspace units/s^2 acts on the joint."""
from .state import JointEstimator, SensedMotor
controller, asked = plan(scene, rng)
sm, est = SensedMotor(outage_at=outage_at, disturbance=disturbance), JointEstimator(mode)
z0 = sm.reset(rng.uniform(-1, 1), rng)
est.reset(z0)
th, om = z0, 0.0
for _ in range(steps):
u = controller(th, om)
th, om = est.step(sm.step(u), u)
goal = cell_centres(n_cells)[scene.target]
return {'error': abs(sm.m.theta - goal), 'success': abs(sm.m.theta - goal) < tol, 'asked': asked}