Skip to content

feat(v1): shoulder-midpoint anchor by default; one normalization scheme per study - #8

Merged
Mattral merged 3 commits into
mainfrom
feat/shoulder-anchor-and-scheme-guard
Oct 8, 2026
Merged

Mattral merged 3 commits into
mainfrom
feat/shoulder-anchor-and-scheme-guard

Conversation

@Mattral

@Mattral Mattral commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

Follow-ups 2 and 3 after run 1: a shoulder-midpoint pose anchor by default, and a guard that keeps one normalization scheme per study.

Two named schemes (worldgap.data.normalization.SCHEMES):

  • shoulder_midpoint (default): pose anchored at landmarks 11/12.
  • hip_midpoint: the spec 5.2 anchor (23/24), kept for framings where the hips are visible.

Both scale by shoulder width; hands are unchanged (wrist + bounding-box diagonal).

Why shoulders: in run 1's clean recordings MediaPipe's mean visibility was 0.005 for the hips vs 0.999 for the shoulders, so a hip anchor is an extrapolation. Spec 5.2 now records this as a deliberate, documented deviation, with that measurement as the justification.

The scheme is fixed per study, never per frame:

  • It's a single parameter per call, recorded as one string in every rollout's metadata["normalization"]["scheme"]. Nothing switches anchors on visibility; test_anchor_never_switches_on_visibility pins that.
  • Every loader takes normalization_scheme=. run_v1_real_data.py takes --normalization and writes it to a new study_settings.json next to the pre-registration, before any score.

The guard (same spirit as the temporal-provenance guard):

  • GapAnalyzer.fit() refuses training rollouts with mixed schemes.
  • compute_gap() refuses source/target rollouts whose schemes differ from each other or from the training scheme. Normalized vs raw counts as different. A gap across anchors includes the anchor change itself.
  • The training scheme is saved in checkpoints. Pre-existing checkpoints load with it unknown, and only the source/target check applies to them.

The analyzer gains no modality branch; the check reads metadata only.

Tests: tests/test_normalization_scheme_guard.py (7). The invariance and reference-point tests in test_normalization.py now run under both schemes. The smoke test checks study_settings.json.

  • Mutation check: with the compute_gap guard removed, 4 of the 7 guard tests fail. The other three cover the same-scheme path, fit's own guard and unnormalized sets, and pass as expected.

Note: re-scoring run 1's recordings under either scheme is a follow-up analysis; its numbers are not comparable with run 1's pre-registered result. This is stated in spec 5.2.

Verification

  • pytest -q: 155 passed
  • ruff check .: clean
  • scripts/run_v2_actuation.py re-run (analyzer changed): every value identical
  • Privacy greps clean
  • CI green on 3.10 / 3.11 / 3.12

Added after review

  • Conflict with docs: run 1's null is underpowered, not impossible #7 resolved (merge of main; the CHANGELOG keeps both sides).
  • Prominent upgrade note, at the top of [Unreleased] and in the README: 0.2.0's loaders return unnormalized coordinates, main normalizes by default with a shoulder anchor, so the same video yields different states. 0.2.0 stores, checkpoints and scores are not comparable, and the guard refuses to mix them.
  • The guard's error now names the remedy: re-extract with one scheme, or apply normalize_rollout(r, scheme) to rollouts saved unnormalized. The second is exact, because normalization is per frame. Verified on run 1's real data: normalizing the saved clean/take0 store vs re-extracting the video gave a maximum state difference of 0.0 across all 46 windows. A unit test pins that equivalence, and another asserts both refusals name the fix (9 guard tests).
  • Docs: docs/v1_first_run_results.md and spec 5.2 note that run 1's stores are unnormalized, so a re-score must normalize first, and that a re-score is a follow-up analysis not comparable with ρ = −0.224.

pytest -q: 157 passed · ruff check . clean.

🤖 Generated with Claude Code

Mattral and others added 3 commits October 8, 2026 12:34
Normalization gets two named schemes: shoulder_midpoint (default; hips
had visibility 0.005 vs 0.999 for shoulders in run 1) and the
spec-faithful hip_midpoint. The scheme is chosen once per study,
recorded in every rollout and in study_settings.json, and never switched
per frame. GapAnalyzer.fit refuses mixed schemes and compute_gap refuses
source/target schemes that differ from each other or from training; the
training scheme is saved in checkpoints. Spec 5.2 documents the
deviation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and-scheme-guard

# Conflicts:
#	CHANGELOG.md
Prominent [Unreleased] and README note: 0.2.0 loaders returned raw
coordinates, main normalizes with a shoulder anchor, so the same video
yields different states. Guard errors now say how to fix it: re-extract
with one scheme, or normalize_rollout() saved unnormalized rollouts,
which is exact (per-frame transform; verified on run 1's data, max
difference 0). Run 1's stores documented as unnormalized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Mattral
Mattral merged commit b91e8c9 into main Oct 8, 2026
3 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.

1 participant