Buckets:
| """Geometry and shared train/deploy plumbing for the demonstration graph. | |
| TRAIN/DEPLOY PARITY CONTRACT: velocity (finite_diff_vel), history padding | |
| (edge_pad_hist) and off-manifold distance (off_manifold_distance) each have EXACTLY | |
| ONE implementation, here. Independent reimplementations previously diverged (zero-pad vs | |
| edge-extrapolate on row 0), producing different abstain logits for an identical window. Callers | |
| route through these; never add a second path. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import os | |
| import warnings | |
| from dataclasses import dataclass | |
| from typing import Any, ClassVar, Optional, Protocol, runtime_checkable | |
| import numpy as np | |
| import torch | |
| from scipy.special import expit | |
| from onf.device import resolve_device | |
| from onf.graph.core import schema | |
| from onf.graph.core.edges import EdgeSet | |
| from onf.graph.core.nodes import NodeTable | |
| __all__ = [ | |
| "NeighborIndex", | |
| "PaddedHistory", | |
| "GeometricField", | |
| "CleanlinessField", | |
| "CleanlinessConfig", | |
| "CleanlinessScorer", | |
| "finite_diff_vel", | |
| "edge_pad_hist", | |
| "off_manifold_distance", | |
| "off_manifold_batch", | |
| "leave_one_demo_out_floors", | |
| "graph_hash", | |
| "load_cleanliness_field", | |
| "CONSTANTS_JSON", | |
| ] | |
| # C0's output, written beside g_nodes.npz. Hashed into graph_hash: a corpus recalibrated under a | |
| # different recipe is a different graph even when the node table is byte-identical. | |
| CONSTANTS_JSON = "constants.json" | |
| # Fields hashed into graph_hash. w is in the list: under C2 it is a LEARNED column, not a pure | |
| # function of the geometry, so a geometry-only hash would call two different graphs the same one. | |
| _HASH_NODE_FIELDS = ("q", "qdot", "owner", "task_id", "stage", "t_idx", "phase") | |
| _HASH_EDGE_FIELDS = ("src", "dst", "rel", "indptr", "w") | |
| _HASH_VERSION = b"graph_hash_v2" | |
| _HASH_ABSENT = b"<absent>" | |
| # Floor under every user-supplied divisor, so a degenerate 0 cannot turn a weight into inf/nan. | |
| _EPS = 1e-8 | |
| class NeighborIndex: | |
| """Blocked nearest-neighbour queries against a fixed corpus of joint configs. | |
| Uploads the corpus once and reuses it, so a caller that asks many times pays the transfer once. | |
| Optional per-row owner ids turn a plain NN query into the leave-one-demo-out floor: corpus rows | |
| sharing the query row's own demo are masked to inf. | |
| device is resolved once here and deliberately NOT plumbed through the module-level helpers: | |
| CPU and CUDA disagree by up to ~1e-4 rad elementwise, enough to move a training label. | |
| """ | |
| def __init__( | |
| self, | |
| corpus: np.ndarray, | |
| corpus_owner: Optional[np.ndarray] = None, | |
| block_size: Optional[int] = None, | |
| ) -> None: | |
| """ | |
| Args: | |
| corpus: [M, D] reference configs; cast to float32. | |
| corpus_owner: Optional [M] demo ids, required for owner-excluded queries. | |
| block_size: Query rows per cdist call; defaults to schema.CDIST_BS. | |
| """ | |
| self._device = resolve_device(None) | |
| self._block_size = int(block_size if block_size is not None else schema.CDIST_BS) | |
| self._corpus = torch.as_tensor( | |
| np.asarray(corpus, dtype=np.float32), device=self._device | |
| ) | |
| self._owner: Optional[torch.Tensor] = None | |
| if corpus_owner is not None: | |
| self._owner = torch.as_tensor( | |
| np.asarray(corpus_owner, dtype=np.int64), device=self._device | |
| ) | |
| def from_nodes(cls, nodes: NodeTable, with_owner: bool = False) -> "NeighborIndex": | |
| """Build an index over a graph's own node configs. | |
| Args: | |
| nodes: Source node table. | |
| with_owner: Attach nodes.owner to enable owner-excluded queries. | |
| Returns: | |
| A ready-to-query index. | |
| """ | |
| owner = np.asarray(nodes.owner, dtype=np.int64) if with_owner else None | |
| return cls(np.asarray(nodes.q, dtype=np.float32), owner) | |
| def __len__(self) -> int: | |
| return int(self._corpus.shape[0]) | |
| def min_distance( | |
| self, points: np.ndarray, point_owner: Optional[np.ndarray] = None | |
| ) -> np.ndarray: | |
| """L2 distance from each query row to its nearest corpus row. | |
| Args: | |
| points: [N, D] (or [D]) query configs; cast to float32. | |
| point_owner: Optional [N] demo ids; corpus rows with a matching owner are excluded | |
| from that row's minimum. | |
| Returns: | |
| [N] float64 distances, in radians. | |
| Raises: | |
| ValueError: point_owner given but the index carries no corpus owners. | |
| """ | |
| query = np.atleast_2d(np.asarray(points, dtype=np.float32)) | |
| n = query.shape[0] | |
| if n == 0: | |
| return np.zeros(0, dtype=np.float64) | |
| if point_owner is not None and self._owner is None: | |
| raise ValueError("owner-excluded query requires a corpus_owner-bearing index") | |
| query_t = torch.as_tensor(query, device=self._device) | |
| owner_t: Optional[torch.Tensor] = None | |
| if point_owner is not None: | |
| owner_t = torch.as_tensor( | |
| np.asarray(point_owner, dtype=np.int64), device=self._device | |
| ) | |
| out = np.empty(n, dtype=np.float64) | |
| for start in range(0, n, self._block_size): | |
| stop = start + self._block_size | |
| dist = torch.cdist(query_t[start:stop], self._corpus) | |
| if owner_t is not None: | |
| excluded = self._owner[None, :] == owner_t[start:stop, None] | |
| dist = dist.masked_fill(excluded, float("inf")) | |
| out[start:stop] = dist.min(dim=1).values.double().cpu().numpy() | |
| return out | |
| def finite_diff_vel(q_hist: np.ndarray) -> np.ndarray: | |
| """vh[t] = q_hist[t] - q_hist[t-1], edge-EXTRAPOLATED at t=0 with vh[1]. | |
| Never zero-padded, and derived from q_hist alone (never a separate qdot channel), so a | |
| window built at train time and the same raw content seen live produce byte-identical velocities | |
| — and therefore an identical e_Q and abstain decision. Single source of truth: the two | |
| former implementations disagreed on row 0 and silently split the abstain logits. | |
| Takes any leading batch shape ([T, D] or [N, T, D]); the difference is along axis -2. | |
| """ | |
| q_hist = np.asarray(q_hist, dtype=np.float32) | |
| v = np.zeros_like(q_hist) | |
| t = q_hist.shape[-2] | |
| if t > 1: | |
| v[..., 1:, :] = q_hist[..., 1:, :] - q_hist[..., :-1, :] | |
| v[..., 0, :] = v[..., 1, :] | |
| return v | |
| def off_manifold_distance(nodes: NodeTable, q_last: np.ndarray) -> float: | |
| """Nearest-node L2 distance from one joint config to any node, with no owner exclusion. | |
| DELIBERATELY NOT routed through NeighborIndex, unlike its batched sibling: this is a | |
| float64 numpy reduction while the index replays torch.cdist in float32 on whatever device | |
| is available. The two agree to ~1e-6 rad — fine for a training label, not fine here, since this | |
| value is pinned bit-exactly in tests/golden/parity.json (where_off_manifold_hex). | |
| Args: | |
| nodes: Graph node table. | |
| q_last: [D] joint config. | |
| Returns: | |
| The distance in radians. | |
| """ | |
| q_last = np.asarray(q_last, dtype=np.float64).reshape(-1) | |
| return float(np.min(np.linalg.norm(nodes.q.astype(np.float64) - q_last[None, :], axis=1))) | |
| def off_manifold_batch(nodes: NodeTable, q_last: np.ndarray) -> np.ndarray: | |
| """Vectorised off_manifold_distance over q_last [N, D] — same definition, one | |
| torch.cdist pass instead of N numpy calls. | |
| Args: | |
| nodes: Graph node table. | |
| q_last: [N, D] joint configs. | |
| Returns: | |
| [N] float64 distances, in radians. | |
| """ | |
| return NeighborIndex.from_nodes(nodes).min_distance(q_last) | |
| class PaddedHistory: | |
| """edge_pad_hist's result. Unpacks and subscripts as the (padded, was_padded) tuple | |
| it replaced, so callers writing arr, flag = edge_pad_hist(...) are unchanged. | |
| Attributes: | |
| window: [hist_len, ...] right-aligned array. | |
| was_padded: True when the input was shorter than hist_len and was edge-padded. | |
| """ | |
| window: np.ndarray | |
| was_padded: bool | |
| def __iter__(self): | |
| """Yields window then was_padded.""" | |
| yield self.window | |
| yield self.was_padded | |
| def __getitem__(self, i: int): | |
| """[0] is window, [1] is was_padded.""" | |
| return (self.window, self.was_padded)[i] | |
| def edge_pad_hist(arr: np.ndarray, hist_len: int) -> PaddedHistory: | |
| """Right-align arr to exactly hist_len rows: truncate to the most recent rows if longer, | |
| edge-pad (repeat the oldest row at the front) if shorter. | |
| Single source of truth for both the deploy and the train path, so a variable-length training | |
| window and a variable-length live q_hist are padded identically. | |
| Args: | |
| arr: [T, ...] input, oldest row first. | |
| hist_len: Target row count. | |
| Returns: | |
| The normalised window and whether padding occurred. | |
| """ | |
| arr = np.asarray(arr) | |
| n = arr.shape[0] | |
| padded = n < hist_len | |
| if padded: | |
| pad_n = hist_len - n | |
| arr = np.concatenate([np.repeat(arr[:1], pad_n, axis=0), arr], axis=0) | |
| elif n > hist_len: | |
| arr = arr[-hist_len:] | |
| return PaddedHistory(arr, padded) | |
| def graph_hash( | |
| nodes: NodeTable, edges: EdgeSet, *, constants: Any = None, c2_state: Any = None, | |
| ) -> str: | |
| """Deterministic content fingerprint of one (graph, constants, C2) triple — v2. | |
| graph_hash_v2 = SHA-256(node+edge geometry || constants.json || C2 checkpoint) | |
| The exact graph a retrieval head was trained on, so a head cannot be loaded against a graph it | |
| was not fit to. Both new terms are load-bearing: C0's constants and C2's weights change what the | |
| graph MEANS while leaving its geometry byte-identical, and the resulting failure is a foreign | |
| head returning confident, correctly-shaped, wrong answers. An absent term hashes as a marker, | |
| never as nothing, so a v1 stamp can never accidentally equal a v2 one. | |
| Args: | |
| nodes: The node table. | |
| edges: Its edge set. | |
| constants: C0's constants.json, as a path or as raw bytes/str; None when the graph shipped | |
| none. | |
| c2_state: The C2 checkpoint, as a mapping of key to array (see | |
| onf.graph.net.gnn.GraphRetrieverNet.c2_state); None when the head carries no C2. | |
| Returns: | |
| The hex digest. | |
| """ | |
| h = hashlib.sha256() | |
| h.update(_HASH_VERSION) | |
| h.update(f"V={len(nodes)}|E={len(edges.src)}|dim={nodes.dim}".encode()) | |
| for name in _HASH_NODE_FIELDS: | |
| h.update(np.ascontiguousarray(getattr(nodes, name)).tobytes()) | |
| for name in _HASH_EDGE_FIELDS: | |
| h.update(np.ascontiguousarray(getattr(edges, name)).tobytes()) | |
| h.update(b"|constants|") | |
| h.update(_constants_bytes(constants)) | |
| h.update(b"|c2|") | |
| h.update(_c2_bytes(c2_state)) | |
| return h.hexdigest() | |
| def _constants_bytes(constants: Any) -> bytes: | |
| """C0's constants.json as bytes: a path is read, bytes/str pass through, None is the marker.""" | |
| if constants is None: | |
| return _HASH_ABSENT | |
| if isinstance(constants, (bytes, bytearray)): | |
| return bytes(constants) | |
| if isinstance(constants, str) and not os.path.exists(constants): | |
| return constants.encode() | |
| path = os.fspath(constants) | |
| return _HASH_ABSENT if not os.path.exists(path) else open(path, "rb").read() | |
| def _c2_bytes(c2_state: Any) -> bytes: | |
| """The C2 checkpoint as bytes: keys in sorted order, each followed by its array's raw buffer.""" | |
| if c2_state is None: | |
| return _HASH_ABSENT | |
| parts: list[bytes] = [] | |
| for key in sorted(c2_state): | |
| parts.append(key.encode()) | |
| parts.append(np.ascontiguousarray(c2_state[key]).tobytes()) | |
| return _HASH_ABSENT if not parts else b"|".join(parts) | |
| def leave_one_demo_out_floors( | |
| points: np.ndarray, point_owner: np.ndarray, corpus: np.ndarray, corpus_owner: np.ndarray, | |
| ) -> np.ndarray: | |
| """Nearest-neighbour distance from each point to the corpus, excluding rows from that point's | |
| OWN demo. onf.graph.core.corpus keeps a numpy replay of this on purpose. | |
| Args: | |
| points: [N, D] query configs. | |
| point_owner: [N] demo ids for points. | |
| corpus: [M, D] reference configs. | |
| corpus_owner: [M] demo ids for corpus. | |
| Returns: | |
| [N] float64 floors, in radians. | |
| """ | |
| return NeighborIndex(corpus, corpus_owner).min_distance(points, point_owner) | |
| # ---- cleanliness: the WEIGHT half of QueryEncoder's pooling ------------------------------------- | |
| # The rule must MATCH between train and deploy -- a head trained under one pooling and evaluated | |
| # under the other is a silent train/deploy mismatch, not an ablation arm. So ONE CleanlinessConfig | |
| # is built at the composition root and injected into both paths; nothing downstream re-reads env. | |
| class CleanlinessField(Protocol): | |
| """The duck type CleanlinessScorer reads. Pins GeometricField and the trained | |
| onf.field.field.ONFField (not importable here at module scope) to one surface.""" | |
| def f_value(self, q0: np.ndarray) -> float: | |
| """Field distance f(q) at a single [D] config.""" | |
| ... | |
| class CleanlinessConfig: | |
| """Which cleanliness field a run uses, and how sharply it weights. | |
| Attributes: | |
| use_geometric: Use the weight-free GeometricField instead of a trained field. | |
| geo_scale: Distance scale in radians for the geometric field. | |
| temp: Sigmoid temperature applied to field distances. | |
| """ | |
| use_geometric: bool = False | |
| geo_scale: float = schema.GEO_SCALE | |
| temp: float = 1.0 | |
| def from_env(cls, temp: float = 1.0) -> "CleanlinessConfig": | |
| """Read ONF_CLEANLINESS/ONF_GEO_SCALE — the ONE place in the codebase that does. | |
| Args: | |
| temp: Sigmoid temperature; not environment-driven. | |
| Returns: | |
| The config this process's environment selects. | |
| """ | |
| return cls( | |
| use_geometric=os.environ.get("ONF_CLEANLINESS", "field").lower() == "geo", | |
| geo_scale=float(os.environ.get("ONF_GEO_SCALE", schema.GEO_SCALE)), | |
| temp=temp, | |
| ) | |
| class GeometricField: | |
| """Weight-free stand-in for onf.field.field.ONFField: | |
| f(q) = ||q - nearest node|| / scale. | |
| scale sets how fast w = sigmoid(-f) falls off; calibrated picks it so an arm | |
| using this field is comparable to one using a trained field rather than differing by a gain. | |
| Args: | |
| q_nodes: [M, D] node configs. | |
| scale: Distance normaliser in radians. | |
| """ | |
| def __init__(self, q_nodes: np.ndarray, scale: float): | |
| self.q = np.asarray(q_nodes, dtype=np.float64) | |
| self.scale = float(scale) | |
| def calibrated(cls, nodes: NodeTable, median_f: float, sample: int = 4096) -> "GeometricField": | |
| """Build with scale set so the median f over sampled leave-one-demo-out node | |
| distances equals median_f (a trained field's own median on the same graph). | |
| Args: | |
| nodes: Graph node table. | |
| median_f: Target median field distance. | |
| sample: Number of nodes to sample. | |
| Returns: | |
| A calibrated field. | |
| """ | |
| q = np.asarray(nodes.q, dtype=np.float64) | |
| owner = np.asarray(nodes.owner, dtype=np.int64) | |
| idx = np.linspace(0, len(q) - 1, min(sample, len(q))).astype(np.int64) | |
| d = leave_one_demo_out_floors(q[idx], owner[idx], q, owner) | |
| return cls(q, float(np.median(d)) / max(float(median_f), _EPS)) | |
| def f_value(self, q0: np.ndarray) -> float: | |
| """Field distance f(q) at a single [D] config, in units of scale.""" | |
| q0 = np.asarray(q0, dtype=np.float64).reshape(-1) | |
| return float(np.min(np.linalg.norm(self.q - q0[None, :], axis=1))) / self.scale | |
| def load_cleanliness_field( | |
| nodes: NodeTable, config: CleanlinessConfig, field_dir: Any = None | |
| ) -> CleanlinessField | None: | |
| """Resolve the field object a CleanlinessScorer should read. | |
| Args: | |
| nodes: Graph node table, used when the geometric field is selected. | |
| config: Cleanliness settings; see CleanlinessConfig.from_env. | |
| field_dir: Directory holding a trained field; ignored under config.use_geometric. | |
| Returns: | |
| A GeometricField, a trained ONFField, or None when neither is configured or | |
| the directory holds no field (uniform weights — not an error). | |
| """ | |
| if config.use_geometric: | |
| return GeometricField(nodes.q, config.geo_scale) | |
| if field_dir is None: | |
| return None | |
| try: | |
| from onf.field.field import ONFField | |
| return ONFField.load(field_dir) | |
| except (FileNotFoundError, OSError): | |
| return None | |
| class CleanlinessScorer: | |
| """Computes the per-step weights w_i = sigmoid(-f(q_i) / temp) that | |
| onf.graph.net.modules.QueryEncoder pools a query window by. | |
| Holds the field and temperature instead of taking them per call, so a control loop builds one | |
| scorer at startup and the rule cannot drift between steps. Falls back to uniform weights when no | |
| field is available, warning once per process — the fallback is a property of the run. | |
| Args: | |
| field: A trained or geometric field, or None for uniform weights. | |
| temp: Sigmoid temperature. | |
| """ | |
| _uniform_warning_emitted: ClassVar[bool] = False | |
| def __init__(self, field: CleanlinessField | None, temp: float = 1.0) -> None: | |
| self.field = field | |
| self.temp = max(float(temp), _EPS) | |
| if field is None: | |
| self._warn_uniform_once() | |
| def from_graph( | |
| cls, nodes: NodeTable, config: CleanlinessConfig, field_dir: Any = None | |
| ) -> "CleanlinessScorer": | |
| """Build the scorer a configuration implies. | |
| Args: | |
| nodes: Graph node table, used when the geometric field is selected. | |
| config: Cleanliness settings. | |
| field_dir: Directory holding a trained field; ignored under config.use_geometric. | |
| Returns: | |
| A ready scorer. | |
| """ | |
| return cls(load_cleanliness_field(nodes, config, field_dir), config.temp) | |
| def reset_uniform_warning(cls) -> None: | |
| """Re-arm the once-per-process uniform-weight warning. For tests.""" | |
| cls._uniform_warning_emitted = False | |
| def _warn_uniform_once(cls) -> None: | |
| """Emit the uniform-weight warning unless it has already fired this process.""" | |
| if cls._uniform_warning_emitted: | |
| return | |
| cls._uniform_warning_emitted = True | |
| warnings.warn( | |
| "no ONF field available for this suite -- falling back to uniform window weights (the " | |
| "query pools as a plain average instead of favoring the clean steps)", | |
| stacklevel=3, | |
| ) | |
| def is_uniform(self) -> bool: | |
| """True when no field backs this scorer, so every weight is 1.0.""" | |
| return self.field is None | |
| def weights(self, q_window: np.ndarray) -> np.ndarray: | |
| """Cleanliness weight per step of one window. | |
| Args: | |
| q_window: [T, D] joint configs. | |
| Returns: | |
| [T] float64 weights in (0, 1), or ones when is_uniform. | |
| """ | |
| q_window = np.atleast_2d(np.asarray(q_window, dtype=np.float64)) | |
| if self.field is None: | |
| return np.ones(q_window.shape[0], dtype=np.float64) | |
| f = np.array([self.field.f_value(q) for q in q_window], dtype=np.float64) | |
| return expit(-f / self.temp) | |
| def weights_batch(self, q_hist: np.ndarray) -> np.ndarray: | |
| """Cleanliness weights for a stacked query set. | |
| A per-window loop on purpose: field's only guaranteed method is the scalar f_value, | |
| so there is no batched surface. Runs once at dataset-build time, not per epoch. | |
| Args: | |
| q_hist: [N, T, D] stacked query windows. | |
| Returns: | |
| [N, T] float32 weights. | |
| """ | |
| return np.stack([self.weights(window) for window in q_hist]).astype(np.float32) | |
Xet Storage Details
- Size:
- 20.2 kB
- Xet hash:
- dc4f4baf3d9eba9816058adc285440d23d6ea7a44d3bac4e99141b06fd3f4ab0
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.