Source code for gato.policy

"""MPCPolicy: an obs -> action adapter usable in any gymnasium env (or plain loop).

Pure numpy — no gymnasium import here. The policy holds its own clock and slides
a reference window over it; the controller does the solving.
"""
import numpy as np


[docs] class TrajectoryReference: """Sliding (6N,) window over a flat [x, y, z, 0, 0, 0]*T trajectory.""" def __init__(self, traj_flat, dt_knot, N): self.traj = np.asarray(traj_flat, dtype=np.float32).reshape(-1) if self.traj.size % 6: raise ValueError("trajectory must be flat [x,y,z,0,0,0]*T (multiple of 6)") self.dt_knot = float(dt_knot) self.N = int(N) if self.traj.size < 6 * self.N: raise ValueError(f"trajectory shorter than one horizon (need >= {6*self.N} values)")
[docs] def window(self, t): """The (6N,) goal window starting at time t (clamped to the trajectory end).""" last = self.traj.size // 6 - self.N off = min(int(t / self.dt_knot), last) return self.traj[6 * off: 6 * (off + self.N)]
[docs] def done(self, t): """True once the window start would run past the last full horizon. Units are KNOTS on both sides: the trajectory holds ``size // 6`` knots and the last valid window starts at knot ``size // 6 - N`` (the same clamp ``window()`` uses); ``done`` fires one knot after that. """ return int(t / self.dt_knot) > self.traj.size // 6 - self.N
[docs] class GoalReference: """Constant window: hold a single 3D EE goal over the whole horizon.""" def __init__(self, goal_xyz, N): goal_xyz = np.asarray(goal_xyz, dtype=np.float32).reshape(3) self._window = np.tile(np.concatenate([goal_xyz, np.zeros(3, dtype=np.float32)]), int(N))
[docs] def window(self, t): return self._window
[docs] def done(self, t): return False
[docs] class MPCPolicy: """Callable obs -> action wrapping an MPCController. Args: controller: gato.MPCController. reference: object with .window(t) -> (6N,) (TrajectoryReference/GoalReference). dt_step: the env's control period (how long each returned action is applied); also used as the hypothesis-scoring dt. obs_to_state: maps the env observation to the (nx,) solver state (default: identity/asarray — for envs whose obs IS [q, dq]). budget_s: per-call wall-clock budget handed to ``MPCController.step`` (iterations while the next one still fits; None = unbudgeted). """ def __init__(self, controller, reference, dt_step, obs_to_state=None, budget_s=None): self.controller = controller self.reference = reference self.dt_step = float(dt_step) self.budget_s = budget_s # per-call step budget (MPCController.step budget_s); None = unbudgeted self.obs_to_state = obs_to_state or (lambda o: np.asarray(o, dtype=np.float32)) self.t = 0.0 self.last = None # StepResult of the most recent __call__
[docs] def reset(self, obs): """Reset the controller and run a warm-up solve from the initial observation.""" x0 = self.obs_to_state(obs) self.controller.reset(x0) self.controller.warmup(x0, self.reference.window(0.0)) self.t = 0.0 self.last = None
def __call__(self, obs): x = self.obs_to_state(obs) r = self.controller.step(x, self.reference.window(self.t), dt_elapsed=self.dt_step, budget_s=self.budget_s) self.t += self.dt_step self.last = r return r.u