Skip to content

EgoDex: temporal action rates as Daft window expressions#47

Merged
everettVT merged 1 commit into
mainfrom
egodex-window-functions
Jul 7, 2026
Merged

EgoDex: temporal action rates as Daft window expressions#47
everettVT merged 1 commit into
mainfrom
egodex-window-functions

Conversation

@everettVT

Copy link
Copy Markdown
Contributor

What

Follow-up to #46. The pose-feature stage moves to one row per frame, and every temporal action rate moves out of NumPy-in-a-UDF and into the query plan as Daft window expressions.

  • EgoDexPipeline.frame_features() — an episode-level UDF computes instantaneous hand geometry and explodes it into per-frame rows keyed by (task, episode_id, frame_index).
  • EgoDexPipeline.temporal_features() / new temporal.py — every rate is an expression over Window().partition_by("task", "episode_id").order_by("frame_index"):
    • lead(1) diffs → curl_rate (grasping), wrist_vert_vel (lifting), arm_ext_rate (reaching)
    • euclidean_distance with lead(1)wrist_speed (stillness), articulation (in-hand)
    • forearm_roll — the one custom UDF, fed by the window (this frame's rotation + next frame's via lead(1)), smoothed with a centered rows_between(-2, 2) mean (twisting)
  • calculate_features() composes both stages; TemporalFeatureComputer is deleted.
  • calibrate() becomes Daft percentile aggregations over the pooled per-frame rows.
  • query.py scenarios are unchanged_match_episodes regroups frames into per-episode tracks via groupby.agg(list_agg), re-sorted by frame_index inside the match UDF (list_agg does not guarantee order).

Why

  • The temporal math stays in the query plan — no collect, visible to the optimizer.
  • The per-frame + window data model matches how frame-level robot data actually ships (LeRobot-style parquet, fleet logs), instead of being an HDF5-episode special case.
  • Restores the windowed-features story the EgoDex blog post tells; the post's snippets map ~1:1 onto temporal.py now.

Semantics

Window semantics are pinned to the old NumPy behavior: fill_null(0.0) reproduces the 0-at-last-frame convention, and rows_between(-2, 2) shrinks at episode edges exactly like the old centered mean.

Tests

  • New test_temporal_window_rates_match_numpy_reference: all six rates vs an independent NumPy reference at 1e-9 tolerance, on scrambled row order across two episodes (proves order_by recovers frame order).
  • All existing query-behavior assertions (openness/twisting/text/combined hits) pass unchanged — the refactor is behavior-preserving end to end.
  • make lint + ruff format --check clean; 5/5 in tests/test_egodex.py.

Breaking

The features table (and features_dir parquet from run_all) is now per-frame instead of one-row-per-episode; POSE_FEATURES_DTYPE and num_frames are gone. calibrate/query/pose_search signatures are unchanged.

🤖 Generated with Claude Code

The feature table becomes one row per frame. frame_features() explodes an
episode-level spatial UDF into per-frame rows; temporal_features() computes
every action rate (grasping, lifting, reaching, stillness, articulation,
twisting) as window expressions over partition_by(task, episode_id)
.order_by(frame_index) — lead(1) diffs, euclidean_distance speeds, and a
centered rows_between(-2, 2) smoothing mean, with forearm_roll as the one
custom UDF fed by the window. calculate_features() composes both stages.

Query scenarios are unchanged: _match_episodes regroups frames into
per-episode tracks via groupby.agg(list_agg), re-sorted by frame_index
inside the match UDF. calibrate() becomes Daft percentile aggregations
over the pooled per-frame rows. TemporalFeatureComputer is deleted; a new
test pins the window rates to a NumPy reference at 1e-9 on scrambled row
order, and the existing query-behavior tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@everettVT everettVT merged commit 4cb8620 into main Jul 7, 2026
3 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a4ddb57cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread datasets/egodex/query.py
Comment on lines +273 to +275
# list_agg does not guarantee frame order within an episode; re-sort every
# track by frame_index so mask position i is frame i.
order = np.argsort(np.asarray(frame_index))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve actual frame indexes when building segments

When the per-frame feature table has gaps or non-zero-based frame_index values (for example after filtering rows, downsampling, or using an external per-frame parquet), sorting by frame_index does not make mask position i equal frame i. The subsequent np.flatnonzero(mask)/segment stitching returns positional offsets instead of the original frame indexes, so reported segments point at the wrong video frames and combined text+pose queries can miss sampled frames whose index is beyond len(mask). Map matching positions back through the sorted frame_index values or enforce a dense frame index contract before querying.

Useful? React with 👍 / 👎.

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.

1 participant