Add a controller#
Prerequisites: the application dependencies, basic Python/NumPy, and controller priorities. Outcome: a positional controller tested with in-memory inputs.
1. Implement the error contract#
The example attracts a current point to a fixed target. It returns a positional error in metres and claims all three translation directions. The GTA execution client projects this packet with the other controller errors.
"""Educational position controller. Computes errors; never commands hardware."""
import numpy as np
from controllers.base_controller import BaseController
class PointAttractor(BaseController):
"""Track a sampled 3D target using a live point callback, in metres."""
def __init__(self, current_point, target_point):
super().__init__()
if not callable(current_point):
raise TypeError("current_point must be callable")
self.type = "positional"
self.current_point = current_point
self.target = self._point(target_point).copy()
@staticmethod
def _point(value):
point = np.asarray(value, dtype=float)
if point.shape != (3,) or not np.isfinite(point).all():
raise ValueError("Expected three finite coordinates")
return point
def compute_error(self, elapsed_time=0.0):
current = self._point(self.current_point())
return {
"type": self.type,
"error": self.target - current,
# This controller claims all three translation directions.
"axes": np.eye(3),
}
current_point is callable because the robot point changes during execution.
target_point is sampled at construction. Reject malformed or non-finite
inputs before producing a command packet. elapsed_time is normalized subtask
progress from 0 to 1, despite its name; this controller does not use it.
2. Test priority projection#
Run from the repository root in the application environment:
import numpy as np
from docs.examples.point_attractor import PointAttractor
from gta.error_projection import project_errors
controller = PointAttractor(lambda: np.zeros(3), [0.1, 0.2, 0.3])
packet = controller.compute_error(0.5)
position, rotation, force, gripper = project_errors([packet])
np.testing.assert_allclose(position, [0.1, 0.2, 0.3])
np.testing.assert_allclose(rotation, np.eye(3))
assert force is None and gripper is None
first = {"type": "positional", "error": [0.04, 0, 0], "axis": [1, 0, 0]}
position, *_ = project_errors([first, packet])
np.testing.assert_allclose(position, [0.04, 0.2, 0.3])
The earlier X constraint wins. The attractor contributes only Y and Z in the
second case. Reversing the order makes the full-position attractor claim every
translation direction first. Projection consumes packet order, not the
priority field itself. The generator preserves the recipe’s list order;
it does not sort that list by its numeric priority labels.
3. Register the controller#
To make the example in docs/examples/ available to the planner:
Put the implementation in
controllers/point_attractor.py.Export
PointAttractorfromcontrollers/__init__.py.Add it to the generated
from controllers import (...)block ingta/script_generator.py.Add
"PointAttractor": [0]toapply_dynamic_parameters()so the current point becomes a lambda in generated code. Otherwise it is evaluated once.Teach
agents/prompts/skill_generator.pyits exact signature, axis ownership, ordering, and appropriate use. Updateagents/prompts/parameter_setter.pyif it introduces physical parameters, units, or constraints. Update task-planning guidance only if its capabilities actually change. Existing checkpoint prompts are preserved snapshots.Test translation, priority interactions, invalid inputs, and the generated script’s syntax with the offline tests.
A recipe could then use
PointAttractor(g_eef_TCP, g_Ocube_Ptop_Lcenter).
The existing translator already recognizes those keypoints. New argument
syntax or helper functions need their own translator and import support.
4. Compare with the supplied controllers#
PosAlign constrains one axis instead of claiming a full position:
class PosAlign(BaseController):
"""
Move tool keypoint g1 to g2 along a1, applying offset o1 along the a1 axis.
a_along_keypoints(g1,g2) is a common axis choice for moving the points directly to each other.
"""
def __init__(self, g1, g2, a1, o1):
"""
Initialize PosAlign controller.
Args:
g1: Tool keypoint position function (lambda function)
g2: Target keypoint position (numpy array)
a1: Axis along which to align (numpy array)
o1: Offset value along the axis (float)
"""
self.type = "positional"
self.g1 = g1
self.g2 = np.asarray(g2)
self.a1 = np.asarray(a1) / np.linalg.norm(np.asarray(a1)) # Normalize
self.o1 = o1
self.target = self.g2 + self.a1 * self.o1
print(f"[PosAlign] Initialized: moving from {self.g1()} to {self.g2} along axis {self.a1} with offset {self.o1}")
def compute_error(self, elapsed_time=0.0):
"""Execute the position alignment controller."""
error = self.target - self.g1()
# Project the error on a1 axis to ensure movement is along the desired direction
error = np.dot(error, self.a1) * self.a1
return {'error': error, 'type': self.type, 'axis': self.a1}
def __repr__(self):
return f"PosAlign(g1={self.g1()}, g2={self.g2}, a1={self.a1}, o1={self.o1}, target={self.target})"
Distances are metres; AxisAlign constructor offsets are degrees, while its
error packet is a rotation vector in radians. Force controllers emit force
packets with magnitudes in newtons.
Verify: run the offline documentation example tests. Then inspect the controller catalog and runtime evaluation rules. For unresolved names or stale generated output, see execution troubleshooting.