Skip to content

RHEED low-level features + embedding-vector query - #86

Merged
chris-price19 merged 10 commits into
mainfrom
claude/agitated-cohen-349eec
Aug 10, 2026
Merged

RHEED low-level features + embedding-vector query#86
chris-price19 merged 10 commits into
mainfrom
claude/agitated-cohen-349eec

Conversation

@chris-price19

Copy link
Copy Markdown
Contributor

Exposes two RHEED capabilities through the SDK, backed by atom-cloud PR #1840 (which adds the corresponding backend routes + response field).

Low-level RHEED features

client.get(data_ids, include_low_level_features=True) now surfaces the pipeline's region-level features (area_n, eccentricity_n, fwhm_0_n, profile skews/CVs, …) as extra columns on RHEEDVideoResult.timeseries_data. The backend nests them per-point under low_level_features; RHEEDProvider.to_dataframe flattens that into raw-named columns. Off by default; ignored for non-RHEED types.

Embedding-vector query

The raw Chronos timeseries embeddings (the inputs to similarity matching, vs. the derived curve from get_similarity_trajectory) are now reachable:

  • client.get_rheed_embeddings(data_id, *, workflow, window_span, kind="window"|"prototype", offset, limit)RHEEDEmbeddingResult — numpy (N, D) vectors + time axis / cluster sizes, with .to_dataframe().
  • client.query_rheed_embeddings(data_id, *, workflow, window_span, kind, top_k) → tidy neighbors DataFrame ("find similar growths"), source excluded, sorted by similarity.

New RHEEDEmbeddingProvider (under atomscale/similarity/) mirrors the existing SimilarityTrajectoryProvider fetch/build split; RHEEDEmbeddingResult is exported from atomscale.results.

Tests

New pure unit tests (no network) for the provider/result conversions and the low-level flatten, plus creds-gated live integration tests. 26 passed, 2 skipped locally; full suite still collects (141) and test_core/test_align stay green.

Note: requires atom-cloud #1840 (the include_low_level_features flag + /embeddings/ routes) deployed for the live paths to return data; window_span must match an embedded span or results are empty.

🤖 Generated with Claude Code

- Client.get(..., include_low_level_features=True) surfaces the pipeline's
  region-level RHEED features (area_n, eccentricity_n, fwhm_0_n, ...) as extra
  timeseries_data columns; RHEEDProvider.to_dataframe flattens the nested
  low_level_features dict the backend now returns.
- client.get_rheed_embeddings(...) returns the raw Chronos window/prototype
  vectors as a numpy-backed RHEEDEmbeddingResult (with .to_dataframe()).
- client.query_rheed_embeddings(...) runs k-NN "find similar" over the
  embedding index, returning a tidy neighbors DataFrame.
- New RHEEDEmbeddingProvider mirrors the SimilarityTrajectoryProvider pattern.

Pairs with atom-cloud PR #1840 (backend routes + low_level_features field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR exposes three new RHEED capabilities through the SDK: low-level per-region features flattened into the timeseries DataFrame when include_low_level_features=True, per-frame segmentation masks fetchable standalone via get_frame_masks or merged into the timeseries via include_masks=True, and a k-NN embedding neighbor query via query_rheed_embeddings. The implementation is well-structured with good test coverage (unit + creds-gated live) and correct handling of edge cases like sparse mask coverage, windowed frame scoping, and empty responses.

  • Low-level features: _flatten_low_level_features uses json_normalize to expand per-point dicts into columns; collision protection prevents clobbering known metrics.
  • Frame masks: get_frame_masks scopes the backend request to the frame range the (possibly windowed) timeseries actually spans, avoiding unnecessary data transfer; attach_frame_masks does a left join on Frame Number preserving the multi-level index.
  • Embedding neighbor query: RHEEDEmbeddingProvider wraps the k-NN endpoint with a clean fetch/build split; decode_mask_rle is refactored into a shared helper now exported from atomscale.results.

Confidence Score: 5/5

  • Safe to merge. The three new features are well-isolated, all new code paths are covered by unit tests, and the frame-scoping logic correctly limits mask fetches to the windowed series range.
  • All new methods have clear validation, graceful empty-response handling, and thorough test coverage. The attach_frame_masks multi-index join and frame_number_bounds scoping logic are correct. No data loss or corruption paths were found.
  • No files require special attention. The only open item is whether the backend guarantees sorted neighbor results — client-side sorting in neighbors_to_dataframe would make that contract explicit.

Important Files Changed

Filename Overview
src/atomscale/similarity/embedding_provider.py New provider for k-NN embedding neighbor queries; clean fetch/build split, robust empty-response handling, good column ordering logic.
src/atomscale/timeseries/rheed.py Adds low-level feature flattening, frame mask attachment, and frame-range utilities. The json_normalize without max_level (noted in previous thread) and the overall logic are sound; mask join on MultiIndex works correctly.
src/atomscale/client.py Adds query_rheed_embeddings, get_frame_masks, and include_masks support to get_rheed_timeseries. All validation, frame scoping, and sentinel-based to_frame logic are correct.
src/atomscale/results/rheed_image.py Extracts decode_mask_rle into a standalone function to share between single-frame and per-frame mask endpoints; existing mask decode path now delegates to it without behavior change.
tests/test_rheed_timeseries.py Good coverage for attach_frame_masks (sparse coverage, empty masks, no-frame-axis passthrough, re-attach idempotency), frame_number_bounds, and include_masks integration via monkeypatched _get.

Sequence Diagram

sequenceDiagram
    participant User
    participant Client
    participant RHEEDProvider
    participant RHEEDEmbeddingProvider
    participant Backend

    Note over User,Backend: get_rheed_timeseries (include_masks=True)
    User->>Client: "get_rheed_timeseries(data_id, include_low_level_features=True, include_masks=True)"
    Client->>RHEEDProvider: "fetch_raw(data_id, include_low_level_features=True, ...)"
    RHEEDProvider->>Backend: "GET rheed/timeseries/{data_id}/"
    Backend-->>RHEEDProvider: "{series_by_angle: [{series: [{low_level_features: {...}}, ...]}]}"
    RHEEDProvider->>RHEEDProvider: to_dataframe() → _flatten_low_level_features()
    RHEEDProvider-->>Client: ts_df (MultiIndex: Angle, Frame Number)
    Client->>RHEEDProvider: frame_number_bounds(ts_df) → (first, last)
    Client->>Backend: "GET rheed/images/{data_id}/frame_masks?from=first&to=last"
    Backend-->>Client: "[{frame_number, mask_rle, mask_height, mask_width}, ...]"
    Client->>RHEEDProvider: attach_frame_masks(ts_df, mask_rows)
    RHEEDProvider-->>Client: ts_df with mask columns (sparse NA for unmasked frames)
    Client-->>User: DataFrame

    Note over User,Backend: query_rheed_embeddings
    User->>Client: query_rheed_embeddings(data_id, workflow, window_span, kind, top_k)
    Client->>RHEEDEmbeddingProvider: fetch_neighbors_raw(client, data_id, workflow, ...)
    RHEEDEmbeddingProvider->>Backend: "GET similarity/{workflow}/{data_id}/embeddings/neighbors/"
    Backend-->>RHEEDEmbeddingProvider: "{neighbors: [{data_id, similarity, ...}, ...]}"
    RHEEDEmbeddingProvider->>RHEEDEmbeddingProvider: neighbors_to_dataframe(raw)
    RHEEDEmbeddingProvider-->>Client: DataFrame (ordered columns)
    Client-->>User: DataFrame
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into claude/agitated..." | Re-trigger Greptile

Comment thread src/atomscale/similarity/embedding_provider.py Outdated
Comment thread src/atomscale/timeseries/rheed.py Outdated
Comment thread src/atomscale/similarity/embedding_provider.py Outdated
chris-price19 and others added 8 commits July 1, 2026 15:16
- RHEEDEmbeddingResult stores cluster_sizes as float64 so partial/missing
  cluster_size coerces to NaN instead of crashing np.asarray (int64 has no
  missing-value sentinel); + regression test.
- to_result requires point["index"] (raise on malformed data rather than
  silently indexing rows with None).
- _expand_low_level_features pins json_normalize(max_level=1) so a future
  nested feature value can't silently dot-explode into extra columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI's pinned ruff (v0.9.4) flags PD901 where the newer local ruff did not.
Rename the `df` DataFrame locals to `frame` in RHEEDEmbeddingResult.to_dataframe
and RHEEDEmbeddingProvider.neighbors_to_dataframe, and apply ruff-format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts in client.py and timeseries/rheed.py where this branch's
RHEED low-level-features + embedding work overlapped main's independently
merged embedding support (PRs #87/#88).

- rheed.py: keep main's canonical `_flatten_low_level_features`; drop this
  branch's equivalent `_expand_low_level_features` (this branch's tests exercise
  the public `to_dataframe` and still pass).
- client.py: union of both APIs. Keep this branch's `get_rheed_embeddings` and
  the new k-NN `query_rheed_embeddings`, and main's `get_embeddings`,
  `get_similarity_matches`, `get_rheed_timeseries`, and `get_frame`. The
  auto-merged `get(include_low_level_features=...)` and `_get_timeseries_result`
  changes are retained.

Note: get_embeddings (main) and get_rheed_embeddings (this branch) hit the same
embeddings endpoint with different return types — a redundancy to reconcile in
a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With include_masks=True, a last_n / elapsed_seconds query windowed the
timeseries but get_frame_masks still fetched the whole video's mask
artifact, discarding most rows in the left join. Add
RHEEDProvider.frame_number_bounds() and pass from_frame/to_frame derived
from the returned series so only the spanned frames' masks are fetched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an "Analysis Results" guide section covering the unified result-fetching
upgrade — get_rheed_timeseries(include_low_level_features=, include_masks=) with
low-level features and per-frame segmentation masks aligned on the Frame Number
axis, get_frame_masks / decode_mask_rle, and embedding fetch/query
(get_embeddings, query_rheed_embeddings). Register the new
atomscale.results.rheed_embedding module in the API reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… API

Following the merge of main's embedding support, this branch carried duplicate
fetching paths. Consolidate onto the canonical methods so the result-fetching
upgrade exposes exactly one way to do each thing:

- Drop Client.get_rheed_embeddings (duplicate of main's get_embeddings, same
  endpoint) and the now-orphaned RHEEDEmbeddingResult / rheed_embedding.py.
- Drop the get(include_low_level_features=) flag in favor of get_rheed_timeseries,
  which already returns low-level features (and masks) aligned by Frame Number.
- Keep the genuinely-new query_rheed_embeddings (k-NN "find similar"); trim
  RHEEDEmbeddingProvider to just the neighbor-query path and align its default
  window_span to 60.0 to match get_embeddings / get_similarity_matches.
- Update the Analysis Results guide and API reference accordingly.

Net result-fetching surface: get_embeddings (vectors), query_rheed_embeddings /
get_similarity_matches (find similar), get_rheed_timeseries
(include_low_level_features / include_masks), get_frame_masks, decode_mask_rle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chris-price19

Copy link
Copy Markdown
Contributor Author

@greptile

The `x != x` NaN self-inequality idiom in test_rheed_timeseries only
holds for float NaN. After a sparse left join, missing values in the
object-dtype mask columns (and what groupby().first() returns for an
all-missing group) can be Python None depending on the resolved pandas
version, where `None != None` is False. This failed on CI for Python
3.10/3.12/3.13 while 3.11 (a different pandas) passed.

pd.isna() returns True for both None and NaN, matching the tests'
documented "get NA" intent and making them version-robust.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chris-price19
chris-price19 merged commit 2b88706 into main Aug 10, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants