Download rtc/masking.py from Daniel-F/rtc-pi05: direct link, hf CLI and curl.
- Browser
- Download file 5.27 kB
-
https://huggingface.co/Daniel-F/rtc-pi05/resolve/main/rtc/masking.py
- Command line
-
hf download hf://Daniel-F/rtc-pi05/rtc/masking.py
-
curl -L -o masking.py https://huggingface.co/Daniel-F/rtc-pi05/resolve/main/rtc/masking.py
5.27 kB
| """Soft prefix-attention masks for RTC guided inpainting (paper Sec. 3.2 / App. A). | |
| Black, Galliker, Levine. "Real-Time Execution of Action Chunking Flow | |
| Policies." NeurIPS 2025. arXiv:2506.07339. | |
| RECONSTRUCTED MODULE. The original was lost; this restores it from the paper and | |
| from how :mod:`rtc.sampler` calls it (``start=inference_delay``, | |
| ``end=horizon-execution_horizon``, ``total=horizon``). The three properties that | |
| make RTC *correct* are asserted by ``test_masking.py``: | |
| 1. ``w[i] == 1`` for ``i < start`` -- the first ``d`` actions are already | |
| committed to the robot before the new chunk lands, so they are a HARD | |
| constraint, not a preference. Getting this wrong breaks the guarantee. | |
| 2. ``w`` is monotonically non-increasing -- confidence in the old chunk decays | |
| with distance into the future. | |
| 3. ``w[i] == 0`` for ``i >= end`` -- past the overlap there is no previous | |
| chunk to agree with, so the new one is unconstrained. | |
| The exact interior curve between ``start`` and ``end`` is a smoothness | |
| hyperparameter, not a correctness property. ``linear`` reproduces the reference | |
| implementation's documented example exactly:: | |
| get_prefix_weights(start=2, end=6, total=10, schedule="linear") | |
| -> [1, 1, 4/5, 3/5, 2/5, 1/5, 0, 0, 0, 0] | |
| ``exp`` is a reconstruction: same endpoints and monotonicity, but a convex decay | |
| that holds the old chunk more tightly just after the frozen prefix and releases | |
| it faster near ``end``. If you need bit-exact parity with the authors' ``exp``, | |
| compare against Physical-Intelligence/real-time-chunking-kinetix ``src/model.py`` | |
| and adjust :func:`_exp_curve` only -- nothing else depends on its shape. | |
| ``zeros`` is the naive hard-masking baseline the paper compares against: freeze | |
| the prefix, ignore the overlap entirely. | |
| """ | |
| from __future__ import annotations | |
| from typing import Literal | |
| import numpy as np | |
| PrefixAttentionSchedule = Literal["ones", "zeros", "linear", "exp"] | |
| _SCHEDULES = ("ones", "zeros", "linear", "exp") | |
| def _exp_curve(u: np.ndarray) -> np.ndarray: | |
| """Convex remap of the linear ramp ``u`` in [0, 1], fixing u=0 -> 0, u=1 -> 1. | |
| ``u * expm1(u) / (e - 1)``: monotone increasing, so weights stay ordered. | |
| """ | |
| return u * np.expm1(u) / (np.e - 1.0) | |
| def get_prefix_weights_np( | |
| start: int, | |
| end: int, | |
| total: int, | |
| schedule: PrefixAttentionSchedule = "exp", | |
| ) -> np.ndarray: | |
| """Soft mask ``W`` of shape ``(total,)``, float64 numpy. | |
| Args: | |
| start: ``d``, the inference delay. Indices ``< start`` are frozen (w = 1). | |
| end: ``H - s``, the prefix attention horizon. Indices ``>= end`` get w = 0. | |
| total: ``H``, the chunk length. | |
| schedule: see the module docstring. | |
| """ | |
| if schedule not in _SCHEDULES: | |
| raise ValueError(f"unknown schedule {schedule!r}; pick one of {_SCHEDULES}") | |
| if total <= 0: | |
| raise ValueError(f"total must be positive, got {total}") | |
| # A delay longer than the overlap would ask for a frozen prefix the mask | |
| # cannot express; the reference clamps rather than failing. | |
| start = int(min(start, end)) | |
| end = int(min(max(end, 0), total)) | |
| i = np.arange(total) | |
| if schedule == "ones": | |
| w = np.ones(total, dtype=np.float64) | |
| elif schedule == "zeros": | |
| w = (i < start).astype(np.float64) # hard mask: prefix only | |
| else: | |
| span = end - start + 1 # matches the reference example | |
| w = np.clip((end - i) / max(span, 1), 0.0, 1.0) | |
| if schedule == "exp": | |
| w = _exp_curve(w) | |
| w = np.where(i < start, 1.0, w) # property 1: hard prefix | |
| return np.where(i >= end, 0.0, w) # property 3 | |
| def get_prefix_weights( | |
| start: int, | |
| end: int, | |
| total: int, | |
| schedule: PrefixAttentionSchedule = "exp", | |
| device=None, | |
| dtype=None, | |
| ): | |
| """Torch flavour of :func:`get_prefix_weights_np` (what ``rtc.sampler`` uses).""" | |
| import torch | |
| w = get_prefix_weights_np(start, end, total, schedule) | |
| return torch.as_tensor(w, device=device, dtype=dtype or torch.float32) | |
| def get_prefix_weights_jax( | |
| start: int, | |
| end: int, | |
| total: int, | |
| schedule: PrefixAttentionSchedule = "exp", | |
| dtype=None, | |
| ): | |
| """JAX flavour, for the JAX/orbax dual-expert backend (``rtc.sampler_jax``).""" | |
| import jax.numpy as jnp | |
| w = get_prefix_weights_np(start, end, total, schedule) | |
| return jnp.asarray(w, dtype=dtype or jnp.float32) | |
| def guidance_weight(tau: float, beta: float) -> float: | |
| """PiGDM guidance weight ``min(beta, (1 - tau) / (tau * r2_tau))`` (paper Eqs. 2/4). | |
| Lives here rather than in a backend sampler so the torch and JAX paths cannot | |
| drift apart:: | |
| inv_r2 = (tau**2 + (1 - tau)**2) / (1 - tau)**2 # 1 / r2_tau (Eq. 4) | |
| c = (1 - tau) / tau # -> inf at tau == 0 | |
| weight = min(c * inv_r2, beta) | |
| At ``tau == 0`` the unclipped weight is infinite (the score is undefined at pure | |
| noise), so ``beta`` is what makes it finite -- exactly its role in App. A.2. | |
| """ | |
| if tau <= 0.0: | |
| return beta | |
| inv_r2 = (tau**2 + (1.0 - tau) ** 2) / ((1.0 - tau) ** 2) | |
| c = (1.0 - tau) / tau | |
| return min(c * inv_r2, beta) | |