Source code for atomscale.results.embeddings

"""Result object for similarity embedding vectors."""

from __future__ import annotations

import warnings
from typing import Any
from uuid import UUID

import numpy as np
from monty.json import MSONable
from numpy.typing import NDArray


[docs] class EmbeddingsResult(MSONable): """Embedding vectors for a single data entry. Returned by :meth:`atomscale.Client.get_embeddings`. The vectors are held as a dense ``(n_returned, dimension)`` float array in :attr:`vectors`, with parallel metadata arrays. The two ``kind`` variants carry different metadata: - ``kind="window"``: one time-resolved vector per window. :attr:`real_times` (relative seconds) and :attr:`unix_times_ms` (absolute milliseconds) give the point in time each vector corresponds to. - ``kind="prototype"``: a small set of representative vectors. :attr:`cluster_sizes` gives how many windows each one summarizes. Metadata arrays not relevant to the returned ``kind`` are ``None``. Attributes: data_id (UUID | str): Data ID the embeddings were computed for. workflow (str): Similarity workflow name (e.g. ``"rheed_stationary"``). kind (str): ``"window"`` or ``"prototype"``. window_span (float): Window span (seconds) the vectors were computed at. vectors (NDArray): ``(n_returned, dimension)`` array of embedding vectors, where ``n_returned == len(vectors)``. dimension (int): Length of each embedding vector (``0`` when the result is empty). count (int): Total vectors available for this ``data_id`` *before* ``offset``/``limit`` — may exceed ``len(vectors)``. The number actually returned is ``len(vectors)``. offset (int): Number of leading vectors skipped (window kind). truncated (bool): True when more vectors are available than were returned, so the result is incomplete. real_times (NDArray | None): ``(n_returned,)`` relative time in seconds (window kind). unix_times_ms (NDArray | None): ``(n_returned,)`` absolute unix time in ms (window kind). cluster_sizes (NDArray | None): ``(n_returned,)`` windows summarized per vector (prototype kind). """
[docs] def __init__( self, data_id: UUID | str, workflow: str, kind: str, window_span: float, vectors: NDArray, dimension: int, count: int, truncated: bool, offset: int = 0, real_times: NDArray | None = None, unix_times_ms: NDArray | None = None, cluster_sizes: NDArray | None = None, ): self.data_id = data_id self.workflow = workflow self.kind = kind self.window_span = window_span self.vectors = vectors self.dimension = dimension self.count = count self.offset = offset self.truncated = truncated self.real_times = real_times self.unix_times_ms = unix_times_ms self.cluster_sizes = cluster_sizes
def __repr__(self) -> str: return ( f"EmbeddingsResult(data_id={self.data_id!r}, kind={self.kind!r}, " f"returned={len(self.vectors)}, count={self.count}, " f"dimension={self.dimension}, truncated={self.truncated})" ) @staticmethod def _point_column( points: list[dict[str, Any]], key: str, *, integer: bool ) -> NDArray | None: """Collect ``point[key]`` across ``points`` into an array (``None`` if all absent). Returns an ``int64`` array for integer columns when every value is present; otherwise a ``float64`` array with ``NaN`` for missing entries (``int64`` cannot represent a null). Used for the per-kind metadata columns (``real_time_seconds``/``unix_time_ms``/``cluster_size``), which the endpoint only populates for the matching ``kind``. """ values = [p.get(key) for p in points] if all(v is None for v in values): return None if integer and all(v is not None for v in values): return np.asarray(values, dtype=np.int64) return np.asarray( [np.nan if v is None else v for v in values], dtype=np.float64 )
[docs] @classmethod def from_api( cls, payload: dict[str, Any] | None, *, data_id: UUID | str, workflow: str, kind: str, window_span: float, ) -> EmbeddingsResult: """Build an :class:`EmbeddingsResult` from a raw endpoint payload. When no embeddings are available for the given workflow / window span, this emits a :class:`UserWarning` and returns an empty result so loops over many IDs don't crash. """ payload = payload or {} points: list[dict[str, Any]] = payload.get("points") or [] # The endpoint echoes kind/window_span; prefer them, fall back to request. resolved_kind = payload.get("kind", kind) resolved_span = payload.get("window_span", window_span) if not points: warnings.warn( f"No embeddings returned for data_id {data_id!r} (workflow " f"{workflow!r}, kind {resolved_kind!r}, window_span " f"{resolved_span}). The entry may not be embedded for this " "workflow/window span.", stacklevel=3, ) return cls( data_id=data_id, workflow=workflow, kind=resolved_kind, window_span=resolved_span, vectors=np.empty((0, 0), dtype=np.float64), dimension=int(payload.get("dimension") or 0), count=int(payload.get("count", 0)), offset=int(payload.get("offset", 0)), truncated=bool(payload.get("truncated", False)), ) try: vectors = np.asarray([p["vector"] for p in points], dtype=np.float64) if vectors.ndim != 2: raise ValueError(f"expected 2-D vectors, got shape {vectors.shape}") except (ValueError, KeyError, TypeError) as exc: raise ValueError( f"Malformed embeddings payload for data_id {data_id!r} " f"(workflow {workflow!r}, kind {resolved_kind!r}): could not build " f"a numeric (count, dimension) array from the point vectors " f"(likely ragged or non-numeric). {exc}" ) from exc declared_dim = payload.get("dimension") dimension = int(declared_dim) if declared_dim else int(vectors.shape[1]) count = int(payload.get("count", vectors.shape[0])) return cls( data_id=data_id, workflow=workflow, kind=resolved_kind, window_span=resolved_span, vectors=vectors, dimension=dimension, count=count, offset=int(payload.get("offset", 0)), truncated=bool(payload.get("truncated", False)), real_times=cls._point_column(points, "real_time_seconds", integer=False), unix_times_ms=cls._point_column(points, "unix_time_ms", integer=True), cluster_sizes=cls._point_column(points, "cluster_size", integer=True), )