pepijn223 HF Staff commited on
Commit
f0f3d44
·
unverified ·
1 Parent(s): 6419f04

Improve DAgger explainer, add conclusion and expand references

Browse files

- Rebuild DAgger loop visualization with SVG icons, dark/light theme
support, curved arrows, iteration counter, and dataset growth bar
- Add conclusion sections: bigger picture, open questions, call to action
- Expand references from 4 to 11 papers, organized by category
(models, techniques, architecture components)
- Update DAgger section text in ablations chapter

Made-with: Cursor

Files changed (32) hide show
  1. app/public/images/folding/cam_base.jpg +3 -0
  2. app/public/images/folding/cam_left_wrist.jpg +3 -0
  3. app/public/images/folding/cam_right_wrist.jpg +3 -0
  4. app/src/components/Hero.astro +8 -46
  5. app/src/components/HtmlEmbed.astro +22 -0
  6. app/src/content/article.mdx +1 -12
  7. app/src/content/chapters/folding/01-hero.mdx +4 -4
  8. app/src/content/chapters/folding/02-results.mdx +2 -2
  9. app/src/content/chapters/folding/03-hardware.mdx +12 -23
  10. app/src/content/chapters/folding/04-data-collection.mdx +42 -10
  11. app/src/content/chapters/folding/05-data-diversity.mdx +1 -1
  12. app/src/content/chapters/folding/06-training.mdx +21 -40
  13. app/src/content/chapters/folding/08-ablations.mdx +85 -94
  14. app/src/content/chapters/folding/09-learnings.mdx +8 -12
  15. app/src/content/chapters/folding/12-references.mdx +14 -1
  16. app/src/content/embeds/folding/action-representations.html +7 -7
  17. app/src/content/embeds/folding/dagger-explainer.html +277 -0
  18. app/src/content/embeds/folding/experiment-results.html +397 -0
  19. app/src/content/embeds/folding/failure-analysis.html +105 -72
  20. app/src/content/embeds/folding/l1-time-quality.html +2 -2
  21. app/src/content/embeds/folding/loss-curves.html +25 -11
  22. app/src/content/embeds/folding/pi05-architecture.html +3 -3
  23. app/src/content/embeds/folding/pi05-sarm-architecture.html +370 -236
  24. app/src/content/embeds/folding/rabc-explainer.html +191 -0
  25. app/src/content/embeds/folding/rtc-explainer.html +275 -0
  26. app/src/content/embeds/folding/statistical-analysis.html +16 -16
  27. app/src/content/embeds/folding/subtask-heatmap.html +220 -79
  28. app/src/content/embeds/folding/success-rates.html +12 -12
  29. app/src/content/embeds/folding/total-score.html +8 -8
  30. compute_cld.py +156 -0
  31. export_wandb_losses.py +64 -0
  32. loss_curves.json +3 -0
app/public/images/folding/cam_base.jpg ADDED

Git LFS Details

  • SHA256: 1fbd6ffa678606b17a4570e09857a04019d0f997a27ec845a53bf5a634e34488
  • Pointer size: 129 Bytes
  • Size of remote file: 4.09 kB
app/public/images/folding/cam_left_wrist.jpg ADDED

Git LFS Details

  • SHA256: 0ee381ba6a9536630049cbd52ea81e114a158f5a80585639beb1a747bbee3ab3
  • Pointer size: 129 Bytes
  • Size of remote file: 3.43 kB
app/public/images/folding/cam_right_wrist.jpg ADDED

Git LFS Details

  • SHA256: d9421480beecccb408e159fbacceac5f61bd3e880575a94b1059ec2185c871e1
  • Pointer size: 129 Bytes
  • Size of remote file: 2.94 kB
app/src/components/Hero.astro CHANGED
@@ -100,19 +100,8 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
100
  <h1 class="hero-title" set:html={title} />
101
  <div class="hero-banner">
102
  <div class="hero-video-wrapper">
103
- <video id="hero-video" src={announcementVideo} autoplay loop muted playsinline style="width:100%;height:auto;border-radius:12px;"></video>
104
- <button id="hero-mute-btn" class="mute-btn" aria-label="Toggle audio">
105
- <svg class="icon-muted" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
106
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
107
- <line x1="23" y1="9" x2="17" y2="15"></line>
108
- <line x1="17" y1="9" x2="23" y2="15"></line>
109
- </svg>
110
- <svg class="icon-unmuted" style="display:none;" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
111
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
112
- <path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
113
- <path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
114
- </svg>
115
- </button>
116
  </div>
117
  {description && <p class="hero-desc">{description}</p>}
118
  </div>
@@ -408,20 +397,6 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
408
  </script>
409
  )}
410
 
411
- <script is:inline>
412
- (function() {
413
- const video = document.getElementById('hero-video');
414
- const btn = document.getElementById('hero-mute-btn');
415
- if (!video || !btn) return;
416
- const iconMuted = btn.querySelector('.icon-muted');
417
- const iconUnmuted = btn.querySelector('.icon-unmuted');
418
- btn.addEventListener('click', () => {
419
- video.muted = !video.muted;
420
- iconMuted.style.display = video.muted ? 'block' : 'none';
421
- iconUnmuted.style.display = video.muted ? 'none' : 'block';
422
- });
423
- })();
424
- </script>
425
 
426
  <style>
427
  /* Hero (full-width) */
@@ -447,25 +422,12 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
447
  display: inline-block;
448
  width: 100%;
449
  }
450
- .mute-btn {
451
- position: absolute;
452
- bottom: 16px;
453
- right: 16px;
454
- background: rgba(0, 0, 0, 0.5);
455
- border: none;
456
- border-radius: 50%;
457
- width: 48px;
458
- height: 48px;
459
- display: flex;
460
- align-items: center;
461
- justify-content: center;
462
- cursor: pointer;
463
- color: white;
464
- backdrop-filter: blur(4px);
465
- transition: background 0.2s;
466
- }
467
- .mute-btn:hover {
468
- background: rgba(0, 0, 0, 0.7);
469
  }
470
  .hero-desc {
471
  color: var(--muted-color);
 
100
  <h1 class="hero-title" set:html={title} />
101
  <div class="hero-banner">
102
  <div class="hero-video-wrapper">
103
+ <video id="hero-video" src={announcementVideo} autoplay controls muted playsinline style="width:100%;height:auto;border-radius:12px;"></video>
104
+ <p class="video-credit">Video by Simon Automne-Pinto</p>
 
 
 
 
 
 
 
 
 
 
 
105
  </div>
106
  {description && <p class="hero-desc">{description}</p>}
107
  </div>
 
397
  </script>
398
  )}
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  <style>
402
  /* Hero (full-width) */
 
422
  display: inline-block;
423
  width: 100%;
424
  }
425
+ .video-credit {
426
+ color: var(--muted-color);
427
+ font-size: 12px;
428
+ margin: 6px 0 0;
429
+ text-align: right;
430
+ opacity: 0.7;
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  }
432
  .hero-desc {
433
  color: var(--muted-color);
app/src/components/HtmlEmbed.astro CHANGED
@@ -520,6 +520,13 @@ const htmlWithId =
520
  }
521
  [data-theme="light"] .html-embed__card .insight strong { color: rgba(0,0,0,.85); }
522
 
 
 
 
 
 
 
 
523
  /* Wilson CI note (success-rates) */
524
  [data-theme="light"] .html-embed__card .ci-note {
525
  background: rgba(0,0,0,.02); border-color: #e2e4e9; color: #6b7280;
@@ -551,6 +558,21 @@ const htmlWithId =
551
  [data-theme="light"] .html-embed__card .toggle-btn:hover {
552
  border-color: #9ca3af; color: rgba(0,0,0,.85);
553
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  [data-theme="light"] .html-embed__card .divider { background: #e2e4e9; }
555
  [data-theme="light"] .html-embed__card .ctrl-label { color: #6b7280; }
556
 
 
520
  }
521
  [data-theme="light"] .html-embed__card .insight strong { color: rgba(0,0,0,.85); }
522
 
523
+ /* Insight boxes (failure-analysis) */
524
+ [data-theme="light"] .html-embed__card .insight-box {
525
+ background: #f7f8fa; border: 1px solid #e2e4e9;
526
+ border-left: 3px solid #f7934f; color: #6b7280;
527
+ }
528
+ [data-theme="light"] .html-embed__card .insight-box strong { color: rgba(0,0,0,.85); }
529
+
530
  /* Wilson CI note (success-rates) */
531
  [data-theme="light"] .html-embed__card .ci-note {
532
  background: rgba(0,0,0,.02); border-color: #e2e4e9; color: #6b7280;
 
558
  [data-theme="light"] .html-embed__card .toggle-btn:hover {
559
  border-color: #9ca3af; color: rgba(0,0,0,.85);
560
  }
561
+ [data-theme="light"] .html-embed__card .mode-btn {
562
+ background: #fff; border-color: #e2e4e9; color: #6b7280;
563
+ }
564
+ [data-theme="light"] .html-embed__card .mode-btn.active {
565
+ color: rgba(0,0,0,.85); border-color: #c5c8d0; background: #f3f4f6;
566
+ }
567
+ [data-theme="light"] .html-embed__card .mode-btn:hover:not(.active) {
568
+ color: rgba(0,0,0,.85);
569
+ }
570
+ [data-theme="light"] .html-embed__card .filter-toggle {
571
+ background: #fff; border-color: #e2e4e9; color: #6b7280;
572
+ }
573
+ [data-theme="light"] .html-embed__card .filter-toggle:hover {
574
+ border-color: #9ca3af; color: rgba(0,0,0,.85);
575
+ }
576
  [data-theme="light"] .html-embed__card .divider { background: #e2e4e9; }
577
  [data-theme="light"] .html-embed__card .ctrl-label { color: #6b7280; }
578
 
app/src/content/article.mdx CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: "Unfolding Robotics:\nThe Open-Source Recipe for Teaching a Robot to Fold Your Clothes"
3
- subtitle: "The complete open-source recipe for teaching robots to fold clothes"
4
  description: "We trained a bimanual robot to fold t-shirts using LeRobot and open-source hardware. We release the model, data, code, and every insight from data collection to deployment."
5
  authors:
6
  - name: "Pepijn Kooijmans"
@@ -54,20 +54,11 @@ import Hero from "./chapters/folding/01-hero.mdx";
54
  import Results from "./chapters/folding/02-results.mdx";
55
  import Hardware from "./chapters/folding/03-hardware.mdx";
56
  import DataCollection from "./chapters/folding/04-data-collection.mdx";
57
- import DataDiversity from "./chapters/folding/05-data-diversity.mdx";
58
  import Training from "./chapters/folding/06-training.mdx";
59
  import Experiments from "./chapters/folding/08-ablations.mdx";
60
  import Learnings from "./chapters/folding/09-learnings.mdx";
61
  import References from "./chapters/folding/12-references.mdx";
62
 
63
- import Introduction from "./chapters/demo/introduction.mdx";
64
- import BuiltWithThis from "./chapters/demo/built-with-this.mdx";
65
- import BestPractices from "./chapters/demo/best-pratices.mdx";
66
- import WritingYourContent from "./chapters/demo/writing-your-content.mdx";
67
- import GettingStarted from "./chapters/demo/getting-started.mdx";
68
- import Markdown from "./chapters/demo/markdown.mdx";
69
- import Components from "./chapters/demo/components.mdx";
70
- import VibeCodingCharts from "./chapters/demo/vibe-coding-charts.mdx";
71
 
72
  <Hero />
73
 
@@ -77,8 +68,6 @@ import VibeCodingCharts from "./chapters/demo/vibe-coding-charts.mdx";
77
 
78
  <DataCollection />
79
 
80
- <DataDiversity />
81
-
82
  <Training />
83
 
84
  <Experiments />
 
1
  ---
2
  title: "Unfolding Robotics:\nThe Open-Source Recipe for Teaching a Robot to Fold Your Clothes"
3
+ subtitle: ""
4
  description: "We trained a bimanual robot to fold t-shirts using LeRobot and open-source hardware. We release the model, data, code, and every insight from data collection to deployment."
5
  authors:
6
  - name: "Pepijn Kooijmans"
 
54
  import Results from "./chapters/folding/02-results.mdx";
55
  import Hardware from "./chapters/folding/03-hardware.mdx";
56
  import DataCollection from "./chapters/folding/04-data-collection.mdx";
 
57
  import Training from "./chapters/folding/06-training.mdx";
58
  import Experiments from "./chapters/folding/08-ablations.mdx";
59
  import Learnings from "./chapters/folding/09-learnings.mdx";
60
  import References from "./chapters/folding/12-references.mdx";
61
 
 
 
 
 
 
 
 
 
62
 
63
  <Hero />
64
 
 
68
 
69
  <DataCollection />
70
 
 
 
71
  <Training />
72
 
73
  <Experiments />
app/src/content/chapters/folding/01-hero.mdx CHANGED
@@ -6,16 +6,16 @@ import Stack from "../../../components/Stack.astro";
6
  > We trained an open-source bimanual robot to fold t-shirts autonomously, reaching 90% success rate. The biggest lever was data quality, not the model, not the architecture.
7
 
8
  <Sidenote>
9
- Read time: ~30 minutes. Each section stands on its own — feel free to skip to what interests you most.
10
  </Sidenote>
11
 
12
  This post walks through the complete journey: hardware choices, data collection, training recipes, and different experiments that show what actually matters. We cover the mistakes and dead ends alongside the things that worked, because the messy middle is where most of the learning happens.
13
 
14
  Some of what we found: cheap 3D-printed leader arms outperformed the expensive ones for teleoperation. Early data collection was more wasteful than expected. A trained reward model turned out to be essential for separating useful demonstrations from harmful ones. And curating a small, high-quality dataset did more than any algorithmic improvement on the full dataset.
15
 
16
- By sharing this we hope to contribute to our bigger vision: **democratize robotics and robot learning**. By open-sourcing every piece tools, data, models, and knowledge we want to enable a community that pushes this technology further. We've tried to avoid just listing what we did in favor of telling the story of how we got here. We hope being this open will help close the gap between closed-lab demos and what the open-source community can achieve.
17
 
18
- Everything we built for this project [SARM](https://huggingface.co/docs/lerobot/sarm), [RTC](https://huggingface.co/docs/lerobot/rtc), DAgger, [OpenArm](https://huggingface.co/docs/lerobot/openarm) and [OpenArm Mini](http://github.com/pkooij/open-arms-mini) is now merged into [LeRobot](https://github.com/huggingface/lerobot) and ready for the community to use.
19
 
20
  _Let's start with the results, does it actually work?_
21
 
@@ -26,7 +26,7 @@ _Let's start with the results, does it actually work?_
26
  <a href="https://huggingface.co/lerobot-data-collection/folding_sarm_reward" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>SARM Reward</strong><br/>HF Hub</a>
27
  <a href="https://huggingface.co/datasets/lerobot/high_quality_folding" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>HQ Dataset</strong><br/>HF Hub</a>
28
  <a href="https://huggingface.co/datasets/lerobot/full_folding" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>Full Dataset</strong><br/>HF Hub</a>
29
- <a href="http://github.com/pkooij/open-arms-mini" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>OpenArm Mini</strong><br/>Repo</a>
30
  <a href="https://github.com/huggingface/lerobot" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>LeRobot</strong><br/>Code</a>
31
  <a href="https://huggingface.co/docs/lerobot/index" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>LeRobot</strong><br/>Documentation</a>
32
  </Stack>
 
6
  > We trained an open-source bimanual robot to fold t-shirts autonomously, reaching 90% success rate. The biggest lever was data quality, not the model, not the architecture.
7
 
8
  <Sidenote>
9
+ Read time: ~25 minutes.
10
  </Sidenote>
11
 
12
  This post walks through the complete journey: hardware choices, data collection, training recipes, and different experiments that show what actually matters. We cover the mistakes and dead ends alongside the things that worked, because the messy middle is where most of the learning happens.
13
 
14
  Some of what we found: cheap 3D-printed leader arms outperformed the expensive ones for teleoperation. Early data collection was more wasteful than expected. A trained reward model turned out to be essential for separating useful demonstrations from harmful ones. And curating a small, high-quality dataset did more than any algorithmic improvement on the full dataset.
15
 
16
+ By sharing this we hope to contribute to our bigger vision: **democratize robotics and robot learning**. By open-sourcing every piece (tools, data, models, and knowledge) we want to enable a community that pushes this technology further. We've tried to avoid just listing what we did in favor of telling the story of how we got here. We hope being this open will help close the gap between closed-lab demos and what the open-source community can achieve.
17
 
18
+ Everything we built for this project ([SARM](https://huggingface.co/docs/lerobot/sarm), [RTC](https://huggingface.co/docs/lerobot/rtc), DAgger, [OpenArm](https://huggingface.co/docs/lerobot/openarm), and [OpenArm Mini](https://github.com/pkooij/open-arms-mini)) is now merged into [LeRobot](https://github.com/huggingface/lerobot) and ready for the community to use.
19
 
20
  _Let's start with the results, does it actually work?_
21
 
 
26
  <a href="https://huggingface.co/lerobot-data-collection/folding_sarm_reward" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>SARM Reward</strong><br/>HF Hub</a>
27
  <a href="https://huggingface.co/datasets/lerobot/high_quality_folding" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>HQ Dataset</strong><br/>HF Hub</a>
28
  <a href="https://huggingface.co/datasets/lerobot/full_folding" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>Full Dataset</strong><br/>HF Hub</a>
29
+ <a href="https://github.com/pkooij/open-arms-mini" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>OpenArm Mini</strong><br/>Repo</a>
30
  <a href="https://github.com/huggingface/lerobot" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>LeRobot</strong><br/>Code</a>
31
  <a href="https://huggingface.co/docs/lerobot/index" className="card" style="padding: 12px 16px; text-align: center; text-decoration: none;"><strong>LeRobot</strong><br/>Documentation</a>
32
  </Stack>
app/src/content/chapters/folding/02-results.mdx CHANGED
@@ -30,7 +30,7 @@ How well does it actually work? We evaluated our best model (Experiment 2.5) acr
30
  | **Combined** (Total SR) | **90%** | |
31
 
32
  <Sidenote>
33
- All evaluations filmed and scored from video. 20 rollouts per experiment (10 per level). Full methodology in the [Evaluation](#evaluation) section.
34
  </Sidenote>
35
 
36
- These numbers are the result of 11 experiments, each testing a different combination of model, data, and training strategies. The full breakdown is in the [Experiments](#experiments) section. But let's start from the beginning: the hardware.
 
30
  | **Combined** (Total SR) | **90%** | |
31
 
32
  <Sidenote>
33
+ All evaluations filmed and scored from video. 20 rollouts per experiment (10 per level). Full methodology in the [Model and Evaluation Setup](#model-and-evaluation-setup) section.
34
  </Sidenote>
35
 
36
+ These numbers are the result of 11 experiments, each testing a different combination of model, data, and training strategies. The full breakdown is in the [From Data to Deployment](#from-data-to-deployment) section. But let's start from the beginning: the hardware.
app/src/content/chapters/folding/03-hardware.mdx CHANGED
@@ -10,35 +10,32 @@ import openArmMini2 from "../../assets/image/openarm-mini2.jpg";
10
 
11
  ## Hardware
12
 
13
- LeRobot takes care of the entire robot learning stack — but you still need the physical hardware. Here's an averview of every piece we used.
14
 
15
  ### The Robot: Bimanual OpenArm
16
 
17
- For starters, the robot. We used the **bimanual [OpenArm](https://huggingface.co/docs/lerobot/openarm)**, open-source, human-like robot arms developed by [Enactic](https://openarm.dev) and built by [WowRobo](https://shop.wowrobo.com). Three reasons drove this choice:
18
 
19
- 1. **The humanoid trend.** We're seeing a wave of human-like robots. More human-form robots means more human-form data in the ecosystem. Building on this form factor positions our work for a future where human-like manipulation data is transferable.
20
- 2. **Smaller teleop gap.** When the robot's kinematics match a human arm, the teleoperator's motions transfer more naturally, meaning less mental remapping and faster learning.
21
- 3. **Open source, good specs.** Solid payload, good reach, and fully open hardware. We extended the upper arm by **+5 cm** to increase reach since our setup doesn't have a hip or torso to provide additional workspace.
22
 
23
- Everything is mounted on **aluminum extrusion profiles**, which let us quickly iterate on the physical arrangement and adjust both teleop and robot height between sessions to increase data diversity.
24
 
25
  <img src={openArmsImg.src} alt="OpenArm bimanual robot setup" style="width:100%; border-radius: 8px;" />
26
 
27
- ### Custom Grippers
28
-
29
- We designed **custom grippers with a larger surface area**, giving the robot a broader contact patch to grip, pinch, and slide fabric reliably. We also added a small polymer patch on one side of the gripper to reduce slippage and make the grasping of fabric easier.
30
-
31
  ### Teleop Arms: OpenArm Mini
32
 
33
  Next, we need a way to control the robot. We started with full-size OpenArm as leader arms for teleoperation. They seemed like the natural choice: same kinematics as the follower arms, one-to-one mapping.
34
 
35
- However, we quickly realized we needed a teleoperator with less inertia, to allow for fast and precise manipulation, and more adaptability to different human morphologies. This led us to develop the **OpenArm Mini**: small, Feetech-based, 3D-printed leader arms based on the [SO-101](https://github.com/TheRobotStudio/SO-ARM100) design. These gave us:
36
  1. **Less inertia** for quicker and more deliberate motions that cloth folding demands
37
  2. **Arm-length agnostic** and adaptable to any human operator size
38
  3. **Incredibly cheap** (~120 EUR per arm) making it easy to scale to multiple stations
39
- 4. **Still support DAgger**: lightweight, but strong enough to move during human-in-the-loop corrective data collection
40
 
41
- One small detail mattered more than expected: the **wrist strap**. It locks the wrist to the leader arm, providing the precise rotational control essential for cloth manipulation.
 
 
42
 
43
  <Note variant="info" emoji="🔗">
44
  [OpenArm Mini repo (3D print files, BOM, LeRobot integration)](https://github.com/pkooij/open-arms-mini)
@@ -54,17 +51,9 @@ One small detail mattered more than expected: the **wrist strap**. It locks the
54
 
55
  <br/>
56
 
57
- Another feature that made a surprisingly big difference: when both your hands are on the leader arms, you need a hands-free way to **start and stop episodes recording**. USB foot pedals solved this elegantly.
58
-
59
  ### Cameras
60
 
61
- Eventually, the robot needs to see what it is doing: for this purpose we used **three cameras** each serving a distinct purpose:
62
-
63
- | Camera | Position | Notes |
64
- |:---|:---|:---|
65
- | **Base camera** | Mounted between/above arms | Wide FOV to capture the full scene |
66
- | **Left wrist camera** | Mounted on left end-effector | Close-up view for precise manipulation |
67
- | **Right wrist camera** | Mounted on right end-effector | Close-up view for precise manipulation |
68
 
69
  <Note variant="info" emoji="🔗">
70
  Camera links: <a href="https://www.amazon.fr/-/en/Fafeicy-Camera-Module-Million-Conferencing/dp/B08GLSPTXY" target="_blank">Base camera (Fafeicy OV2710)</a> / <a href="https://www.arducam.com/12mp-imx708-usb-uvc-102-wide-angle-fixed-focus-camera-module-3.html" target="_blank">Wrist cameras (Arducam IMX708)</a>
@@ -77,6 +66,6 @@ Eventually, the robot needs to see what it is doing: for this purpose we used **
77
 
78
  ### LeRobot Integration
79
 
80
- Integrating OpenArm into LeRobot required adding **CAN-bus protocol** support for the arm's motors. It can now be found in the [LeRobot repository](https://github.com/huggingface/lerobot). We also created a UI for non-technical robot operators, so the CLI doesn't need to be used to start and stop episodes.
81
 
82
  With the hardware in place, the next step was the hardest and most time-consuming part of the entire project: collecting good data. And "good" is much harder to define than it sounds.
 
10
 
11
  ## Hardware
12
 
13
+ LeRobot handles the software stack, but you still need the physical hardware. Below we cover the base robot, custom grippers, a cheap 3D-printed teleop setup that outperformed the expensive one, and the camera layout.
14
 
15
  ### The Robot: Bimanual OpenArm
16
 
17
+ For starters, the robot. We used the **bimanual [OpenArm](https://huggingface.co/docs/lerobot/openarm)**. These are open-source, human-like robot arms developed by [Enactic](https://openarm.dev) and sold by multiple vendors like [WowRobo](https://shop.wowrobo.com). Two reasons drove this choice:
18
 
19
+ 1. **Smaller teleop gap.** When the robot's kinematics match a human arm, the teleoperator's motions transfer more naturally, meaning less mental remapping and faster learning. The humanoid form factor also aligns with where the ecosystem is heading: more human-form robots means more transferable data.
20
+ 2. **Open source, good specs.** Solid payload, good reach, and fully open hardware. We extended the upper arm (the bicep segment) by **+5 cm** to increase reach since our setup doesn't have a hip or torso to provide additional workspace.
 
21
 
22
+ Everything is mounted on **aluminum extrusion profiles**, which let us quickly iterate on the physical arrangement and adjust both teleop and robot height between sessions to increase data diversity. We also designed **custom grippers with a larger surface area**, giving the robot a broader contact patch to grip, pinch, and slide fabric reliably.
23
 
24
  <img src={openArmsImg.src} alt="OpenArm bimanual robot setup" style="width:100%; border-radius: 8px;" />
25
 
 
 
 
 
26
  ### Teleop Arms: OpenArm Mini
27
 
28
  Next, we need a way to control the robot. We started with full-size OpenArm as leader arms for teleoperation. They seemed like the natural choice: same kinematics as the follower arms, one-to-one mapping.
29
 
30
+ However, we quickly realized we needed a teleoperator arm with less inertia, that allows for fast and precise manipulation, and more adaptability to different human morphologies. This led us to develop the **OpenArm Mini**: small, Feetech-based, 3D-printed leader arms based on the [SO-101](https://github.com/TheRobotStudio/SO-ARM100) design. These gave us:
31
  1. **Less inertia** for quicker and more deliberate motions that cloth folding demands
32
  2. **Arm-length agnostic** and adaptable to any human operator size
33
  3. **Incredibly cheap** (~120 EUR per arm) making it easy to scale to multiple stations
34
+ 4. **Still supports itself**: strong enough to move during human-in-the-loop corrective data collection
35
 
36
+ One small detail mattered more than expected: the **wrist strap**. It locks the operator's wrist to the leader arm, providing rotational control essential for precise manipulation.
37
+
38
+ Another feature that made a surprisingly big difference: when both your hands are on the leader arms, you need a hands-free way to **start and stop episodes recording**. USB foot pedals solved this elegantly.
39
 
40
  <Note variant="info" emoji="🔗">
41
  [OpenArm Mini repo (3D print files, BOM, LeRobot integration)](https://github.com/pkooij/open-arms-mini)
 
51
 
52
  <br/>
53
 
 
 
54
  ### Cameras
55
 
56
+ We use **three cameras**, each serving a purpose. The **base camera** is mounted between the arms and provides a wide-angle overview of the full workspace, the model's primary understanding of the task state. The two **wrist cameras** are mounted directly on the end-effectors. Because they move with the grippers, they provide a natural depth signal and give a close-up view for precise manipulation. They also act as a proxy for touch, being so close to the grippers, they capture contact details: grip quality, slip, that humans normally sense through their fingers. More cameras could help, but every additional image stream requires more compute and time to process; three was the tradeoff we settled on.
 
 
 
 
 
 
57
 
58
  <Note variant="info" emoji="🔗">
59
  Camera links: <a href="https://www.amazon.fr/-/en/Fafeicy-Camera-Module-Million-Conferencing/dp/B08GLSPTXY" target="_blank">Base camera (Fafeicy OV2710)</a> / <a href="https://www.arducam.com/12mp-imx708-usb-uvc-102-wide-angle-fixed-focus-camera-module-3.html" target="_blank">Wrist cameras (Arducam IMX708)</a>
 
66
 
67
  ### LeRobot Integration
68
 
69
+ Integrating OpenArm into LeRobot required adding **CAN-bus** support. CAN-bus is the communication protocol the arm's motors use, think of it as a shared wire where LeRobot sends position commands ("move joint 3 to 45 degrees") and reads back the current joint angles. Everything else: capturing camera images, running the model, converting predictions into those joint positions, happens in Python inside LeRobot. The CAN-bus driver is the thin bridge between software and hardware. This integration can now be found in the [LeRobot repository](https://github.com/huggingface/lerobot). We also created a UI for non-technical robot operators, so the CLI doesn't need to be used to start and stop episodes.
70
 
71
  With the hardware in place, the next step was the hardest and most time-consuming part of the entire project: collecting good data. And "good" is much harder to define than it sounds.
app/src/content/chapters/folding/04-data-collection.mdx CHANGED
@@ -1,28 +1,60 @@
1
  import Note from "../../../components/Note.astro";
2
  import Sidenote from "../../../components/Sidenote.astro";
3
  import Accordion from "../../../components/Accordion.astro";
 
 
4
 
5
  ## Data Collection
6
 
7
  Data collection was the longest phase of this project, and arguably the most important. No amount of compute can compensate for bad demonstrations.
8
 
 
 
9
  We ran **8 setups** in parallel, optimizing for **maximum diversity**: 25+ different t-shirts, 8 different backgrounds, and varying camera and robot heights between sessions. We structured collection into two task levels: **Level 1** (fold a laid-out shirt) and **Level 2** (spread a messy shirt, fold it, place it aside).
10
 
11
  ### Learning to Teleoperate
12
 
13
- Here's an honest truth: **early data is worse than the final data**. Teleoperating a bimanual robot is a genuine skill, and it takes practice. The first episodes are slow, not deliberate, and full of failed attempts. Over hours of practice, operators get dramatically better and smoother motions, faster execution, and more consistent grasps.
14
 
15
- This creates one of the most important practical decisions of the project: **when do you start recording data for the final model?** Too early and you pollute the dataset with low-quality demonstrations that the model will faithfully reproduce, including hesitations and fumbles. Too late and you've wasted precious time.
16
 
17
- Aligning on a common strategy across operators was equally important. Folding is a very multi-modal task (there are many valid ways to fold a t-shirt) and the model learns better from a consistent strategy. Before each recording sprint, we held brief alignment sessions: experimenting with different techniques, sharing our learnings and then converging on the most efficient fold sequence.
18
 
19
  ### Tips for Good Data Collection
20
 
21
- 1. **Practice before you record.** Consistent, deliberate demonstrations are more valuable than fast, imprecise ones.
22
- 2. **Quality over speed. Always.** A fast but messy episode teaches bad habits that are hard to untrain.
23
- 3. **Each action should make sense from the current observation alone.** Most models don't have history, so avoid motions that only work because *you* remember what happened 5 seconds ago.
24
- 4. **Be consistent within episodes.** The model learns a coherent strategy more easily than movements that vary wildly each time.
25
- 5. **Start small, then extend.** Train a quick model, see what fails, then add diversity. Don't try to collect the perfect dataset on day one.
26
- 6. **Speed comes last.** Once you've dialed in quality and a consistent strategy, optimize for speed. But never sacrifice quality for it.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- After learning all these things and collecting data for multiple weeks we ended up with 5,688 episodes across 8 setups.
 
 
 
 
 
1
  import Note from "../../../components/Note.astro";
2
  import Sidenote from "../../../components/Sidenote.astro";
3
  import Accordion from "../../../components/Accordion.astro";
4
+ import Wide from "../../../components/Wide.astro";
5
+ import diversityGridImg from "../../assets/image/lerobot-data-collection_level12_rac_2_2026-02-08_1_grid_15x10.jpg";
6
 
7
  ## Data Collection
8
 
9
  Data collection was the longest phase of this project, and arguably the most important. No amount of compute can compensate for bad demonstrations.
10
 
11
+ T-shirt folding involves deformable objects with complex contact dynamics: simulation can't faithfully reproduce how fabric crumples and slides, world models can't yet predict cloth deformation reliably, and RL from scratch on real hardware is too sample-inefficient for a task this long-horizon (though RL *on top of* a pretrained VLA is a promising future direction). That leaves **real-world teleoperation** as the practical path. We chose **leader-follower arms** because they match the robot's kinematics exactly (what the operator does is what the robot does), giving low latency, high precision, and support for [DAgger](https://github.com/huggingface/lerobot/tree/main/examples/hil), where the teleop setup takes over from a running policy to collect corrections.
12
+
13
  We ran **8 setups** in parallel, optimizing for **maximum diversity**: 25+ different t-shirts, 8 different backgrounds, and varying camera and robot heights between sessions. We structured collection into two task levels: **Level 1** (fold a laid-out shirt) and **Level 2** (spread a messy shirt, fold it, place it aside).
14
 
15
  ### Learning to Teleoperate
16
 
17
+ Here's an honest truth: **early data collection is worse than the final data**. Teleoperating a bimanual robot is a genuine skill, and it takes practice. The first episodes are slow, not deliberate, and full of failed attempts. Over hours of practice, operators get dramatically better, with smoother motions, faster execution, and more consistent grasps.
18
 
19
+ This raises an important practical question: **when do we start using the robot operators data?** Every hour of data collection requires a human operator, it's expensive and doesn't scale easily. But bad demonstrations aren't just wasted effort, they might actively hurt the model, which can reproduce hesitations and inconsistent strategies. But defining what makes a "good" demonstration is itself an unsolved problem in robot learning. Duration is a rough proxy, a fast episode can still contain poor grasps, and a slow one can be perfectly executed. We ended up relying on a combination of heuristics and a learned reward model (introduced later) to separate useful data from harmful data.
20
 
21
+ Aligning on a common strategy across operators was equally important. Folding is a very multi-modal task (there are many valid ways to fold a t-shirt) and with unlimited data per approach the model could probably learn them all. But at our scale, mixing strategies means each one is under-represented. Aligning on a single strategy is a more data-efficient path. Before each recording sprint, we held brief alignment sessions: experimenting with different techniques, sharing our learnings and then converging on the most efficient fold sequence.
22
 
23
  ### Tips for Good Data Collection
24
 
25
+ After weeks of collecting data across operators, these are the guidelines we found most useful:
26
+
27
+ 1. **Practice before you record.** Consistent, deliberate demonstrations are more valuable than hesitant or inconsistent ones.
28
+ 2. **Quality over speed.** High quality task execution is more valuable than fast, sloppy ones.
29
+ 3. **Be consistent within episodes.** The model learns a coherent strategy more easily than movements that vary wildly each time.
30
+ 4. **Start small, then extend.** Train a quick model, see what fails, then add diversity. Don't try to collect the perfect dataset on day one.
31
+ 5. **Speed after quality.** Once you've dialed in quality and a consistent strategy, optimize for speed. But never sacrifice quality for it.
32
+
33
+ ### What we ended up with
34
+
35
+ After multiple weeks of collection across 8 setups we had **5,688 episodes**, the **full dataset**. Not all of them are equally useful: some contain hesitations, inconsistent strategies, or poor fold quality. Later in the project, we built a smaller **high-quality dataset** of 1,200 episodes by selecting the best recordings from the full set and adding new demonstrations collected with a more unified strategy.
36
+
37
+ | Metric | Full dataset | High-quality dataset |
38
+ |:---|:---:|:---:|
39
+ | Total episodes | **5,688** | **1,200** |
40
+ | Total frames | **14.1M** | **3.2M** |
41
+ | Total hours | **~131 h** | **~30 h** |
42
+ | FPS | **30** | **30** |
43
+ | Cameras | **3** (base 480×640, wrists 720×1280) | **3** (same) |
44
+ | Action dims | **16** (7 joints + gripper × 2 arms) | **16** (same) |
45
+
46
+ All data is stored in the [**LeRobotDataset v3.0**](https://huggingface.co/blog/lerobot-datasets-v3) format, which encodes camera streams as video rather than individual images. This makes the dataset **20x–40x more compressed** compared to storing raw frames, making it practical to share and stream datasets of this scale.
47
+
48
+ The grid below shows one frame from each of 100 different episodes. Notice the variation in t-shirt color, background and camera viewpoint.
49
+
50
+ <Wide>
51
+ <img src={diversityGridImg.src} alt="Trajectory diversity grid showing variation in t-shirt color, background, camera viewpoint, and robot height" style="width:100%; border-radius: 8px;" />
52
+ </Wide>
53
+
54
+ Explore the dataset yourself with the [LeRobot Data Visualizer](https://huggingface.co/spaces/lerobot/visualize_dataset):
55
 
56
+ <Wide>
57
+ <div className="card" style="overflow: hidden; border-radius: 10px; height: 640px;">
58
+ <iframe src="https://lerobot-visualize-dataset.hf.space/?path=%2Flerobot%2Fhigh_quality_folding%2Fepisode_0" width="125%" height="1000" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" style="border: none; transform: scale(0.8); transform-origin: top left;"></iframe>
59
+ </div>
60
+ </Wide>
app/src/content/chapters/folding/05-data-diversity.mdx CHANGED
@@ -3,7 +3,7 @@ import diversityGridImg from "../../assets/image/lerobot-data-collection_level12
3
 
4
  ## Data Overview
5
 
6
- After weeks of collection we had **5,688 episodes** across 8 setups. Not all of them are equally useful — later we curated a smaller, high-quality subset. Here are both datasets side by side:
7
 
8
  ### Dataset Statistics
9
 
 
3
 
4
  ## Data Overview
5
 
6
+ After weeks of collection we had **5,688 episodes** across 8 setups, the **full dataset**. Not all of them are equally useful: some contain hesitations, inconsistent strategies, or poor fold quality. Later in the project, we built a smaller **high-quality dataset** of 1,200 episodes by selecting the best recordings from the full set and adding new demonstrations collected with a more unified strategy. Here are both side by side:
7
 
8
  ### Dataset Statistics
9
 
app/src/content/chapters/folding/06-training.mdx CHANGED
@@ -6,23 +6,24 @@ import HtmlEmbed from "../../../components/HtmlEmbed.astro";
6
 
7
  ## Model and Evaluation Setup
8
 
9
- Before diving into experiments, let's establish the model architecture we're training and how we measure performance. These stay constant across every iteration.
10
 
11
  ### Model Architecture
12
 
13
- At its core, the model is a **Vision-Language-Action (VLA)** model. It takes in camera images and a task description, and outputs actions — joint angle targets and gripper commands — for the next second, at a frequency of 30Hz.
14
 
15
  <Wide>
16
  <HtmlEmbed
17
  id="pi05-sarm-arch"
18
  src="folding/pi05-sarm-architecture.html"
19
- title="π0.5 + SARM Architecture"
20
- desc="Interactive system overview. <strong>Full System</strong> shows how the π0.5 VLA and SARM reward model work together. <strong>π0.5 Detail</strong> zooms into the VLM backbone, attention pattern, and flow matching action expert. <strong>SARM Detail</strong> shows the two-transformer reward model with stage classification and subtask progress prediction. Hover any component for details."
 
21
  frameless
22
  />
23
  </Wide>
24
 
25
- The model generates actions through **flow matching**, a generative approach that transforms random noise into coherent action sequences, conditioned on what the cameras see and what the motors are doing. This allows the model to represent **multi-modal action distributions**: when there are multiple valid ways to grasp a sleeve or start a fold, the model can capture that ambiguity rather than averaging to a meaningless middle ground.
26
 
27
  <Sidenote>
28
  Flow matching is closely related to diffusion models but uses a simpler, more direct interpolation path between noise and data.
@@ -30,43 +31,23 @@ The model generates actions through **flow matching**, a generative approach tha
30
 
31
  #### [Real-Time Chunking (RTC)](https://huggingface.co/docs/lerobot/rtc)
32
 
33
- RTC is crucial for real-world deployment. Instead of waiting for the predicted action chunk to finish before generating the next, RTC generates the next chunk while executing the current one. It "freezes" actions that are already committed and "inpaints" the remaining ones, producing smooth asynchronous motion. In practice, this sped up our rollouts by at least a factor of 2.
34
 
35
- ```mermaid
36
- sequenceDiagram
37
- participant R as Robot
38
- participant M as Model
39
- loop Every execution_horizon steps
40
- R->>M: Current observation
41
- M->>R: Action chunk (30 steps)
42
- Note over R: Execute while next chunk generates
43
- end
44
- ```
45
-
46
- All experiments use **RTC** with an action queue size of 30 and a maximum action horizon of 20, along with **action interpolation** (upsampling from 30 Hz to 90 Hz) which makes the robot much quieter and smoother.
47
-
48
- ### Models
49
-
50
- We focused on two VLA architectures for our cloth folding data:
51
-
52
- - **π0** the base flow-matching VLA, trained with standard imitation learning
53
- - **[π0.5](https://huggingface.co/docs/lerobot/pi05)** an improved variant with more pretraining and several improvements to the flow matching denoising process
54
 
55
- Both are finetuned from pretrained checkpoints. Starting from this pretrained foundation rather than training from scratch gives the model a head start on visual understanding and basic manipulation concepts.
56
 
57
- ### Training Setup
58
 
59
- | Parameter | Value |
60
- |:---|:---:|
61
- | GPUs | **8x H100** |
62
- | Batch size | **32** (with gradient accumulation), total batch size is 256 |
63
- | Action chunk size | **30** |
64
- | Optimizer | AdamW |
65
- | Learning rate | **1e-4** (with warmup + cosine decay) |
66
 
67
- <Sidenote>
68
- Multi-GPU training with 8xH100 and gradient accumulation was necessary to fit the large batch sizes needed for stable VLA training.
69
- </Sidenote>
70
 
71
  ---
72
 
@@ -82,15 +63,15 @@ For every experiment, we evaluate the model on:
82
  Each t-shirt fold is attempted **twice consecutively**, giving **10 rollouts per level** and **20 rollouts total per experiment**. Every evaluation is filmed and scored from video afterward, so judgment is decoupled from execution.
83
 
84
  <Note>
85
- The evaluation protocol t-shirts, attempts count, scoring rubric, and filming setup is identical across every experiment.
86
  </Note>
87
 
88
  ### Metrics
89
 
90
  We report four complementary metrics:
91
 
92
- 1. **Success Rate** Binary pass/fail per rollout.
93
- 2. **Score** Partial credit based on subtasks completed. This distinguishes a model that consistently reaches Fold 3 from one that fails at Unfold, even if neither achieves full success.
94
 
95
  <Accordion title="Scoring rubric Level 1 and Level 2">
96
 
 
6
 
7
  ## Model and Evaluation Setup
8
 
9
+ Before diving into experiments, let's establish the model we're training and how we measure performance. These stay constant across every iteration.
10
 
11
  ### Model Architecture
12
 
13
+ The dominant approach in robot learning today is to train a **Vision-Language-Action (VLA)** model: a single neural network that takes in camera images and a task description and outputs motor commands. The recipe most labs follow is to **pretrain** a VLA on large, multi-robot, multi-task datasets to learn general visual and manipulation priors, then **fine-tune** it on data from your specific robot and task. Several strong pretrained VLAs exist: [π0](https://huggingface.co/docs/lerobot/pi0), [π0.5](https://huggingface.co/docs/lerobot/pi05), [GR00T](https://developer.nvidia.com/isaac/gr00t), [SmolVLA](https://huggingface.co/docs/lerobot/smolvla), among others. We chose to use **π0 and π0.5** because they showed the strongest performance in our early experiments, likely due to the scale of their pretraining data. We start from the **pretrained checkpoints** and fine-tune on our folding data.
14
 
15
  <Wide>
16
  <HtmlEmbed
17
  id="pi05-sarm-arch"
18
  src="folding/pi05-sarm-architecture.html"
19
+ title="π0.5 Architecture"
20
+ desc="Data flows left to right. Three inputs are fused in the VLM backbone, then the action expert generates motor commands via flow matching. Hover any component for details."
21
+ config={{ initialTab: "pi05", hideTabBar: true }}
22
  frameless
23
  />
24
  </Wide>
25
 
26
+ The model takes in three things: **camera images** (3 views: base + 2 wrist), **joint angles** (16D: 7 joints + gripper per arm), and a **language prompt** ("fold the t-shirt"). The images pass through a frozen **SigLIP** vision encoder that converts raw pixels into visual tokens, a compact representation the rest of the model can work with. Those visual tokens, the text, and the joint state all feed into the **Gemma VLM backbone** (2B parameters), which fuses the three modalities through cross-attention. This is where the model builds its understanding of the scene: what the shirt looks like, where the arms are, and what it's supposed to do. The backbone's output then conditions the **Action Expert** (300M parameters), which uses **flow matching** to turn random Gaussian noise into a coherent sequence of 30 actions, which equals one second of joint targets and gripper commands. Flow matching lets the model represent **multi-modal action distributions**: when there are multiple valid ways to grasp a sleeve, it can capture that ambiguity rather than averaging to a meaningless middle ground.
27
 
28
  <Sidenote>
29
  Flow matching is closely related to diffusion models but uses a simpler, more direct interpolation path between noise and data.
 
31
 
32
  #### [Real-Time Chunking (RTC)](https://huggingface.co/docs/lerobot/rtc)
33
 
34
+ The model outputs 30 actions at once (a "chunk"), but generating that chunk takes ~100..200ms, during which the robot is still moving. Without RTC, the robot would **pause** between chunks while waiting for the next prediction, producing jerky stop-and-go motion. RTC solves this by generating the next chunk **while the current one is still executing**. The key idea: by the time the new chunk is ready, several actions from the old chunk have already been executed and can't be changed, so RTC **freezes** those committed actions and **inpaints** the rest of the new chunk to be consistent with them. This produces smooth, continuous motion with no pauses.
35
 
36
+ <HtmlEmbed
37
+ id="rtc-explainer"
38
+ src="folding/rtc-explainer.html"
39
+ title="Real-Time Chunking"
40
+ desc="Toggle between synchronous and RTC mode. In synchronous mode, the robot idles during inference. RTC overlaps inference with execution, freezing committed actions (locked) and inpainting the rest. Press play to see the timing."
41
+ frameless
42
+ />
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ In practice, RTC sped up our rollouts by at least a factor of 2 compared to synchronous inference.
45
 
46
+ All experiments use **RTC** with an action queue size of 30 and a maximum action horizon of 20, along with **action interpolation** (upsampling from 30 Hz to 90 Hz) which makes the robot much quieter and smoother.
47
 
48
+ ### Training recipe
 
 
 
 
 
 
49
 
50
+ We fine-tune two variants of this architecture: **π0**, the base flow-matching VLA, and **π0.5**, an improved version with more pretraining data and refinements to the denoising process. Both start from pretrained checkpoints. Training runs on **8× H100 GPUs** with a per-GPU batch size of 32 (a total batch size of 256), gradient accumulation, and using **AdamW** with a learning rate of **1e-4** (warmup + cosine decay). The large batch size is important for stable VLA training, and it's what drives the multi-GPU requirement.
 
 
51
 
52
  ---
53
 
 
63
  Each t-shirt fold is attempted **twice consecutively**, giving **10 rollouts per level** and **20 rollouts total per experiment**. Every evaluation is filmed and scored from video afterward, so judgment is decoupled from execution.
64
 
65
  <Note>
66
+ The evaluation protocol (t-shirts, attempts count, scoring rubric, and filming setup) is identical across every experiment.
67
  </Note>
68
 
69
  ### Metrics
70
 
71
  We report four complementary metrics:
72
 
73
+ 1. **Success Rate.** Binary pass/fail per rollout.
74
+ 2. **Score.** Partial credit based on subtasks completed. This distinguishes a model that consistently reaches Fold 3 from one that fails at Unfold, even if neither achieves full success.
75
 
76
  <Accordion title="Scoring rubric Level 1 and Level 2">
77
 
app/src/content/chapters/folding/08-ablations.mdx CHANGED
@@ -22,23 +22,11 @@ We started with the simplest possible recipe: train π0 and π0.5 on the full da
22
  | 1.1 | π0 | All data (5,688 episodes) | 200k | Baseline |
23
  | 1.2 | π0.5 | All data (5,688 episodes) | 200k | Baseline |
24
 
25
- The loss curves show both models converging, but convergence of loss for robotics doesn't necessarily mean good performance:
26
 
27
- <Wide>
28
- <HtmlEmbed
29
- id="loss-curves"
30
- src="folding/loss-curves.html"
31
- title="Training Loss"
32
- desc="Click legend entries to toggle runs. <strong>Series 1</strong>: trained from pretrained checkpoints on full data (200k steps). <strong>Series 2</strong>: fine-tuned on curated high-quality data (100k steps)."
33
- frameless
34
- />
35
- </Wide>
36
 
37
- When we evaluated, the results were disappointing. Experiment 1.1 (π0) achieved 40% Level 1 success but 0% Level 2. Experiment 1.2 (π0.5) managed only 20% total success rate, also with 0% Level 2. The models could sometimes fold a laid-out shirt, but they were slow, and produced poor-quality folds. Level 2, spreading a messy shirt before folding was completely out of reach.
38
-
39
- Watching the rollouts, the problem was clear: the policy had learned the *average* of many different folding strategies in the dataset. Different operators used different grip points, different fold sequences, and different speeds. The model averaged over all of them, producing indecisive, hesitant motions.
40
-
41
- One early lesson: **undertraining makes the policy shaky**. Make sure your model has converged before drawing conclusions about what works.
42
 
43
  ---
44
 
@@ -55,7 +43,26 @@ We filtered episodes in two ways:
55
 
56
  #### Stage-Aware Reward Modeling (SARM)
57
 
58
- To go beyond binary keep/discard filtering, we trained a reward model: **[SARM](https://huggingface.co/docs/lerobot/sarm)** (Stage-Aware Reward Modeling). SARM is a learned "critic" that scores trajectories based on how well the robot is progressing toward task completion. It predicts a 0–1 progression signal at every timestep, correctly identifying **mistakes** (drops in value) and **progress** (increases) in real time:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  <Wide>
61
  <Stack layout="3-column" gap="small">
@@ -69,13 +76,13 @@ We annotated every episode in both datasets using SARM. This gave us continuous
69
 
70
  ---
71
 
72
- ### Algorithmic improvements
73
 
74
- With cleaner data and SARM scores in hand, we systematically tested three algorithmic improvements.
75
 
76
  #### Relative actions
77
 
78
- Following the [UMI](https://arxiv.org/abs/2402.10329) approach, we switched from absolute actions to **relative trajectories**: each action in the chunk is an offset from the robot's current state at prediction time, not from the previous action. The state observation remains absolute — only the action targets are relative. This avoids error accumulation (unlike true delta) and doesn't require a global coordinate frame (unlike absolute).
79
 
80
  <HtmlEmbed
81
  id="action-representations"
@@ -84,9 +91,9 @@ Following the [UMI](https://arxiv.org/abs/2402.10329) approach, we switched from
84
  desc="Relative trajectory (blue) references all actions to the current state. Delta (yellow) chains each action to the previous one, accumulating error. Absolute (red) requires a global coordinate frame. Diagram adapted from UMI (Chi et al., 2024)."
85
  />
86
 
87
- See the [LeRobot action representations docs](https://huggingface.co/docs/lerobot/action_representations) for a full guide.
88
 
89
- The result: experiment 1.3 (π0.5 + relative actions + quantile normalization) jumped to **35% total success rate and 70% Level 1**, up from 1.2's 20% total and 40% Level 1. The improvement was consistent across every comparison we made.
90
 
91
  <Sidenote>
92
  π0.5 is likely pretrained with relative actions, so 1.3 fine-tunes in a regime consistent with pretraining, while 1.2 fine-tunes against it.
@@ -94,52 +101,63 @@ The result: experiment 1.3 (π0.5 + relative actions + quantile normalization) j
94
 
95
  #### RABC (Reward-Advantage-Based Conditioning)
96
 
97
- Next, we put SARM's per-timestep scores to work during training. With **RABC**, every episode is weighted by its contribution to progress: high-reward actions contribute more to the loss, low-reward ones contribute less. Negative progress is clipped to 0. Unlike binary success/fail labels, this provides a continuous signal at every timestep.
98
 
99
- Experiments 1.4 and 1.5 tested RABC with different κ values (0.01 and 0.0215). Both showed improvement, with 1.5's higher κ performing slightly better.
 
 
 
 
 
100
 
101
  #### Combining everything
102
 
103
- Experiment 1.7 combined all three improvements: **relative actions + RABC + quantile normalization**. The result: **40% total success rate, 80% Level 1** — the best of Series 1.
104
 
105
  | Experiment | Key change | Total SR | Level 1 SR | Level 2 SR |
106
  |:---:|:---|:---:|:---:|:---:|
107
- | 1.1 | π0 baseline | 20% | 40% | 0% |
108
  | 1.2 | π0.5 baseline | 20% | 40% | 0% |
109
- | 1.3 | + Relative actions | 35% | 70% | 0% |
110
- | 1.4 | + RABC (κ=0.01) | 25% | 50% | 0% |
111
- | 1.5 | + RABC (κ=0.0215) | 30% | 60% | 0% |
112
- | 1.7 | Relative + RABC + QUANTILES | **40%** | **80%** | **0%** |
113
 
114
- The trend was encouraging, but one number was impossible to ignore: **every Series 1 experiment achieved exactly 0% Level 2 success**. Algorithmic improvements alone couldn't compensate for the inconsistency in the full dataset.
115
 
116
  #### DAgger: targeted corrections
117
 
118
- We also explored **DAgger** (Dataset Aggregation) as a way to fix specific failure modes. The idea: the operator watches the policy run, pauses when it fails, teleoperates a recovery, and hands control back. This generates correction data precisely where the model struggles.
 
 
 
 
 
 
 
 
119
 
120
- DAgger felt promising — corrections targeting the model's actual failure modes should be highly effective at pushing success rates higher. The full [Human-in-the-Loop infrastructure](https://github.com/huggingface/lerobot/tree/main/examples/hil) is now merged into LeRobot for anyone to use.
121
 
122
  ---
123
 
124
  ### The breakthrough: high-quality data
125
 
126
- No Series 1 experiment cracked Level 2. We needed a fundamentally better dataset.
127
 
128
- We went back to data collection with a clear plan: collect new demonstrations specifically for Level 1 and Level 2, with a **unified strategy** across operators. Every episode followed the same grip points, the same fold sequence, the same timing. We also manually curated the best episodes from the large dataset, selecting only those with consistent technique and clear progress.
129
 
130
- The result was the **high-quality dataset**: 1,200 episodes combining the best of the original data with new, strategically collected demonstrations. Less data, but dramatically better data.
131
 
132
- **Series 2** fine-tuned the best Series 1 checkpoints on this curated dataset for 100k steps:
133
 
134
  | Experiment | Base | Key change | Total SR | Level 1 SR | Level 2 SR |
135
  |:---:|:---:|:---|:---:|:---:|:---:|
136
- | 2.1 | 1.3 | HQ data only | 35% | 70% | 0% |
137
- | 2.2 | 1.3 | HQ + RABC + Relative | **75%** | **100%** | **50%** |
138
- | 2.5 | 1.7 | HQ + RABC + Relative | **90%** | **100%** | **80%** |
139
 
140
- The jump was dramatic. Experiment 2.5 reached **90% total success rate** — 100% Level 1, 80% Level 2 — up from 40% in Series 1. The same architecture, the same training recipe, just better data.
141
 
142
- Both 2.2 and 2.5 used the same recipe (HQ + RABC + Relative Actions), but 2.5 fine-tuned from 1.7 (the stronger base with relative actions + RABC already baked in) while 2.2 fine-tuned from 1.3. The difference (75% → 90%) likely reflects this stronger starting point. Data quality was the single biggest lever, and RABC's effect was strongest on **Level 2** — the longer, harder task where emphasizing the best demonstrations mattered most.
143
 
144
  ---
145
 
@@ -147,7 +165,8 @@ Both 2.2 and 2.5 used the same recipe (HQ + RABC + Relative Actions), but 2.5 fi
147
 
148
  Not every idea improved results:
149
 
150
- - **Mirroring augmentation** (experiment 2.3): only 5% total success rate. Mirroring the data doubles the number of valid strategies the model sees, which is the opposite of what we needed — convergence on a single consistent approach.
 
151
  - **Larger chunk size** (experiment 2.4, chunk=45): no improvement over the default chunk size of 30.
152
 
153
  ---
@@ -156,76 +175,48 @@ Not every idea improved results:
156
 
157
  Now that the full journey is clear, here is the complete picture across all 11 experiments.
158
 
159
- <Wide>
160
-
161
- | # | Model | Base data | Steps | Normalization | Key experiment |
162
- |:---:|:---:|:---|:---:|:---:|:---|
163
- | 1.1 | π0 | All data | 200k | MEAN_STD | Baseline |
164
- | 1.2 | π0.5 | All data | 200k | MEAN_STD | Baseline |
165
- | 1.3 | π0.5 | All data | 200k | QUANTILES | Relative Action |
166
- | 1.4 | π0.5 | All data | 200k | MEAN_STD | Reward model (SARM) with RABC κ=0.01 |
167
- | 1.5 | π0.5 | All data | 200k | MEAN_STD | Reward model (SARM) with RABC κ=0.0215 |
168
- | 1.7 | π0.5 | All data | 200k | QUANTILES | Relative Action + Reward model (SARM) with RABC κ=0.0215 |
169
- | 2.1 | π0.5 | High-quality only | 100k | QUANTILES | Fine-tune from 1.3 |
170
- | 2.2 | π0.5 | High-quality only | 100k | QUANTILES | Fine-tune from 1.3 + Reward model (SARM) with RABC κ=0.0265 + Relative Action |
171
- | 2.3 | π0.5 | High-quality + mirrored | 100k | QUANTILES | Fine-tune from 1.3 + Relative Action + image transforms + mirroring setup (data augmentation) |
172
- | 2.4 | π0.5 | High-quality only | 100k | QUANTILES | Fine-tune from 1.3 · chunk=45 |
173
- | 2.5 | π0.5 | High-quality only | 100k | QUANTILES | Fine-tune from 1.7 + Reward model (SARM) with RABC κ=0.0265 + Relative Action |
174
-
175
- </Wide>
176
-
177
- <HtmlEmbed
178
- id="success-rates"
179
- src="folding/success-rates.html"
180
- title="Success Rates by Experiment"
181
- desc="Success rates (Total, Level 1, Level 2) across all experiments. Series 1 trains from scratch on full data; Series 2 finetunes the best Series 1 checkpoint on curated high-quality data."
182
- />
183
 
184
  <HtmlEmbed
185
- id="total-score"
186
- src="folding/total-score.html"
187
- title="Total Score by Experiment"
188
- desc="Overall score (% of maximum 1500) per experiment. The dashed line marks 50% as a reference point."
189
  />
190
 
191
- Total score captures partial progress that binary success rate misses. Even failed rollouts earn credit for completed subtasks, revealing that some Series 1 experiments make meaningful progress despite 0% Level 2 success.
192
 
193
- <HtmlEmbed
194
- id="l1-time-quality"
195
- src="folding/l1-time-quality.html"
196
- title="Level 1 Completion Time vs. Fold Quality"
197
- desc="Each bubble is one experiment. X-axis = completion time (faster is left), Y-axis = fold quality (higher is better), bubble size = total success rate. The best experiments cluster in the top-left corner."
198
- />
199
-
200
- Speed and quality correlate strongly with data quality. Series 2 experiments fold 2–3x faster than Series 1 (40s vs 100s+), and fold quality only breaks past 3.0 with high-quality training data. Faster isn't a separate goal from better — it's a consequence of the policy learning a clear, unambiguous strategy.
201
 
202
  <HtmlEmbed
203
  id="subtask-heatmap"
204
  src="folding/subtask-heatmap.html"
205
  title="Subtask Timing Heatmap"
206
- desc="Average time (seconds) per subtask across all experiments. Green is fast, red is slow. Dashes indicate the subtask was never completed."
207
  />
208
 
 
 
209
  ### Where the policies fail
210
 
211
- Understanding *how* each experiment fails — not just whether it fails — is essential for knowing what to try next.
212
 
213
  <HtmlEmbed
214
  id="failure-analysis"
215
  src="folding/failure-analysis.html"
216
  title="Failure Analysis"
217
- desc="Breakdown of failure modes across experiments for Level 2, Level 1, and combined. Use the tabs to switch between levels. Each bar shows which subtask the policy failed at, revealing where different experiments struggle."
218
- />
219
-
220
- ### Which differences are real?
221
-
222
- With 20 rollouts per experiment, not every visible gap is real. We run **[STEP](https://tri-ml.github.io/step/)** (Sequential Testing for Efficient Policy comparison) on all 55 pairs with **Bonferroni correction** (α = 0.10, per-pair α < 0.0018), following [TRI's statistical evaluation framework](https://medium.com/toyotaresearch/statistical-thinking-for-robot-policy-evaluation-from-rigorous-a-b-testing-to-effective-0ae886fbd68d). STEP is a sequential method, meaning we can extend the experiment with additional rollouts (up to n<sub>max</sub> = 50) without risking p-hacking, something batch tests like Barnard's don't allow. The chart below shows the full **Bayesian Beta posterior** over each policy's true success rate. **CLD letters** above each violin indicate which experiments are statistically separable, policies sharing a letter are not significantly different.
223
-
224
- <HtmlEmbed
225
- id="statistical-analysis"
226
- src="folding/statistical-analysis.html"
227
- title="Statistical Analysis"
228
- desc="Bayesian posterior distributions over each policy's true success rate, with Compact Letter Display (CLD) groups summarising statistical separability. Toggle between Total, Level 1, and Level 2."
229
  />
230
 
231
- The CLD tells a clear story: **Series 2 separates from Series 1**. Experiments 2.5 and 2.2 form group 'a' — statistically distinct from the bulk of Series 1 experiments which cluster in groups 'c' and 'd'. Within Series 1, no experiment separates from another, meaning none of the algorithmic changes (RABC, relative actions, normalization) produced a statistically detectable improvement on their own when trained on the full dataset. The jump only comes from switching to curated high-quality data in Series 2. Toggle to Level 2 and the separation is even starker: every Series 1 experiment sits in group 'b' with 0% success, while 2.5 stands alone in group 'a'.
 
22
  | 1.1 | π0 | All data (5,688 episodes) | 200k | Baseline |
23
  | 1.2 | π0.5 | All data (5,688 episodes) | 200k | Baseline |
24
 
25
+ When we evaluated, the results were mixed. Experiment 1.1 (π0) achieved 40% total success rate (80% Level 1) but 0% Level 2. Experiment 1.2 (π0.5) managed only 20% total (40% Level 1), also with 0% Level 2. The models could sometimes fold a laid-out shirt, but they were slow and produced poor-quality folds. Level 2 (spreading a messy shirt before folding) was completely out of reach.
26
 
27
+ Watching the rollouts, we suspected that the problem was in the data. Different operators used different grip points and strategies for unspreading the shirt, and the diversity in our dataset was high. The model didn't converge enough and was producing indecisive motions.
 
 
 
 
 
 
 
 
28
 
29
+ An additional early lesson: **undertraining makes the policy shaky**. Make sure your model has converged before drawing conclusions about what works.
 
 
 
 
30
 
31
  ---
32
 
 
43
 
44
  #### Stage-Aware Reward Modeling (SARM)
45
 
46
+ To go beyond binary keep/discard filtering, we trained a reward model: **[SARM](https://huggingface.co/docs/lerobot/sarm)** (Stage-Aware Reward Modeling). The core problem SARM solves: how do you measure "progress" in a long, multi-stage task like t-shirt folding, where demonstrations vary wildly in length and strategy? You can't just use elapsed time, a shirt that's fully flattened might happen at frame 200 in one demo and frame 800 in another. SARM instead learns a semantic notion of progress that generalizes across demonstrations.
47
+
48
+ **How it works.** SARM is a **vision-language** reward model built on a frozen [CLIP](https://openai.com/index/clip/) backbone. It takes **8 RGB frames** (sampled 1 second apart from the base camera), a **task description** ("fold the t-shirt"), and the robot's **joint state**. The language conditioning means the same model could in principle score different tasks; it's not hard-coded to folding.
49
+
50
+ The images and text are encoded by CLIP into embeddings. These flow into a **Stage Transformer**, which classifies which subtask the robot is in (e.g., "grab", "flatten", "fold", "place"), and a **Subtask Transformer**, conditioned on the predicted stage, which estimates fine-grained progress τ within that stage. The final output is a single progress score (`stage + τ`, normalized to [0, 1]). This two-level decomposition makes the reward signal stable across demonstrations of different lengths and styles: a fully flattened shirt always maps to roughly the same progress value, regardless of how long it took to get there. The score is used downstream for **data curation** (ranking episodes by quality) and **RABC training weights** (emphasizing high-progress actions during policy training).
51
+
52
+ <Wide>
53
+ <HtmlEmbed
54
+ id="sarm-architecture"
55
+ src="folding/pi05-sarm-architecture.html"
56
+ title="SARM Architecture"
57
+ desc="Data flows left to right. Inputs are encoded by CLIP, then two transformers predict stage and within-stage progress. The resulting score is used for data curation and training weights. Hover any component for details."
58
+ config={{ initialTab: "sarm", hideTabBar: true }}
59
+ frameless
60
+ />
61
+ </Wide>
62
+
63
+ **Training data.** SARM supports multi-stage annotations, but we use it in **single-stage mode**: the entire task (Level 2, which includes Level 1) is treated as one stage with progress going linearly from 0 to 1. This means no human subtask annotation is needed, progress labels are derived directly from the frame index within each episode.
64
+
65
+ Once trained, SARM can score *any* trajectory, including noisy demonstrations it was never trained on. It predicts a 0–1 progression signal at every timestep, correctly identifying **mistakes** (drops in value) and **progress** (increases) in real time:
66
 
67
  <Wide>
68
  <Stack layout="3-column" gap="small">
 
76
 
77
  ---
78
 
79
+ ### Better training recipes
80
 
81
+ With cleaner data and SARM scores in hand, we turned to how the model trains on that data.
82
 
83
  #### Relative actions
84
 
85
+ Following the [UMI](https://arxiv.org/abs/2402.10329) approach, we switched from absolute actions to **relative action trajectories**: each action in the chunk is an offset from the robot's current state at prediction time, not from the previous action. The state observation remains absolute; only the action targets are relative. This avoids error accumulation (unlike true delta).
86
 
87
  <HtmlEmbed
88
  id="action-representations"
 
91
  desc="Relative trajectory (blue) references all actions to the current state. Delta (yellow) chains each action to the previous one, accumulating error. Absolute (red) requires a global coordinate frame. Diagram adapted from UMI (Chi et al., 2024)."
92
  />
93
 
94
+ See the [LeRobot action representations docs](https://huggingface.co/docs/lerobot/main/en/action_representations) for a full guide.
95
 
96
+ The result: experiment 1.3 (π0.5 + relative actions) jumped to **35% total success rate and 70% Level 1**, up from 1.2's 20% total and 40% Level 1.
97
 
98
  <Sidenote>
99
  π0.5 is likely pretrained with relative actions, so 1.3 fine-tunes in a regime consistent with pretraining, while 1.2 fine-tunes against it.
 
101
 
102
  #### RABC (Reward-Advantage-Based Conditioning)
103
 
104
+ Next, we put SARM's per-timestep scores to work during training with **[RABC](https://huggingface.co/docs/lerobot/sarm)** (Reward-Aligned Behavior Cloning). Standard behavior cloning treats every action in the dataset equally. RABC instead computes a per-timestep "progress delta" from SARM (the change in predicted task progress over one action chunk) and uses it to weight the training loss. A threshold κ controls how selective the weighting is: actions with progress above κ get full weight, those below are softly down-weighted, and negative-progress actions are clipped to zero weight entirely. The result is that the policy focuses on the best moments in each demonstration without discarding entire episodes.
105
 
106
+ <HtmlEmbed
107
+ id="rabc-explainer"
108
+ src="folding/rabc-explainer.html"
109
+ title="How RABC works"
110
+ desc="Top: SARM progress curve for a simulated episode. Background color shows where progress advances (green), stalls (yellow), or regresses (red). Bottom: the resulting per-timestep training weight. Drag the κ slider to see how selectivity changes."
111
+ />
112
 
113
  #### Combining everything
114
 
115
+ Experiment 1.7 combined **relative actions + RABC**. The result: **40% total success rate, 80% Level 1**, the best of initial training.
116
 
117
  | Experiment | Key change | Total SR | Level 1 SR | Level 2 SR |
118
  |:---:|:---|:---:|:---:|:---:|
119
+ | 1.1 | π0 baseline | 40% | 80% | 0% |
120
  | 1.2 | π0.5 baseline | 20% | 40% | 0% |
121
+ | 1.3 | π0.5 + Relative actions | 35% (+15) | 70% (+30) | 0% (+0) |
122
+ | 1.7 | π0.5 + Relative + RABC | **40%** (+20) | **80%** (+40) | **0%** (+0) |
 
 
123
 
124
+ The trend was encouraging, but one number was impossible to ignore: **every initial-training experiment achieved exactly 0% Level 2 success**. Algorithmic improvements alone couldn't compensate for the inconsistency in the full dataset.
125
 
126
  #### DAgger: targeted corrections
127
 
128
+ Throughout our work on Level 1, we developed and integrated **DAgger** (Dataset Aggregation) as a [Human-in-the-Loop (HIL)](https://github.com/huggingface/lerobot/tree/main/examples/hil) data collection method in LeRobot. The idea: the operator watches the policy run, pauses when it fails, teleoperates a recovery, and hands control back. This generates correction data precisely where the model struggles.
129
+
130
+ <HtmlEmbed
131
+ id="dagger-explainer"
132
+ src="folding/dagger-explainer.html"
133
+ title="DAgger Loop"
134
+ desc="The DAgger cycle: the policy runs until it fails, a human operator takes over to demonstrate the correct recovery, and the correction is recorded as new training data. Click to advance manually."
135
+ frameless
136
+ />
137
 
138
+ We didn't test DAgger in an isolated experiment, but added the correction data to both the full dataset and the high-quality dataset. In our early experiments, using DAgger to collect targeted corrections proved very beneficial for making the policy more robust to failures, the model learns to recover from states it would otherwise get stuck in. The full [Human-in-the-Loop infrastructure](https://github.com/huggingface/lerobot/tree/main/examples/hil) is now merged into LeRobot for anyone to use.
139
 
140
  ---
141
 
142
  ### The breakthrough: high-quality data
143
 
144
+ No initial-training experiment cracked Level 2. We needed a fundamentally better dataset.
145
 
146
+ We went back to data collection with a clear plan: collect new demonstrations, with a more **unified strategy** across operators. Every episode followed the similar grip points, sequences and timing. We also manually curated the best episodes from the large dataset, selecting only those with consistent technique and clear progress.
147
 
148
+ The result was the **high-quality dataset**: 1,200 episodes combining the best of the original data with new, strategically collected demonstrations.
149
 
150
+ We then **fine-tuned** the best initial-training checkpoints on this curated dataset for 100k steps:
151
 
152
  | Experiment | Base | Key change | Total SR | Level 1 SR | Level 2 SR |
153
  |:---:|:---:|:---|:---:|:---:|:---:|
154
+ | 2.1 | 1.3 | HQ data only | 40% (+5) | 70% (+0) | 10% (+10) |
155
+ | 2.2 | 1.3 | HQ + RABC + Relative | **75%** (+40) | **100%** (+30) | **50%** (+50) |
156
+ | 2.5 | 1.7 | HQ + RABC + Relative | **90%** (+50) | **100%** (+20) | **80%** (+80) |
157
 
158
+ The jump was dramatic. Experiment 2.5 reached **90% total success rate**: 100% Level 1, 80% Level 2, up from 40% with initial training. The same architecture, the same training recipe, just better data.
159
 
160
+ Both 2.2 and 2.5 used the same recipe (HQ + RABC + Relative Actions), but 2.5 fine-tuned from 1.7 (the stronger base with relative actions + RABC already baked in) while 2.2 fine-tuned from 1.3. The difference (75% → 90%) likely reflects this stronger starting point. Data quality was the single biggest lever, and RABC's effect was strongest on **Level 2**, the longer, harder task where emphasizing the best demonstrations mattered most.
161
 
162
  ---
163
 
 
165
 
166
  Not every idea improved results:
167
 
168
+ - **RABC on the full dataset** (experiments 1.4, 1.5; without relative actions): we applied SARM's reward weighting directly during initial training on the full dataset, with different selectivity thresholds. Both performed worse than the baseline (1.4: 15% total, 1.5: 0% total). The reward model was only trained on the high-quality dataset, so when applied to the larger dataset it likely down-weighted too much Level 1 data, meaning the model couldn't reliably accomplish the first steps of the task.
169
+ - **Mirroring augmentation** (experiment 2.3): only 5% total success rate. Mirroring the data doubles the number of valid strategies the model sees, which is the opposite of what we needed: convergence on a single consistent approach.
170
  - **Larger chunk size** (experiment 2.4, chunk=45): no improvement over the default chunk size of 30.
171
 
172
  ---
 
175
 
176
  Now that the full journey is clear, here is the complete picture across all 11 experiments.
177
 
178
+ | # | Model | Data | Steps | Key change |
179
+ |:---:|:---:|:---|:---:|:---|
180
+ | 1.1 | π0 | All | 200k | Baseline |
181
+ | 1.2 | π0.5 | All | 200k | Baseline |
182
+ | 1.3 | π0.5 | All | 200k | Relative actions |
183
+ | 1.4 | π0.5 | All | 200k | RABC κ=0.01 |
184
+ | 1.5 | π0.5 | All | 200k | RABC κ=0.0215 |
185
+ | 1.7 | π0.5 | All | 200k | Rel + RABC κ=0.0215 |
186
+ | 2.1 | π0.5 | HQ | 100k | From 1.3 |
187
+ | 2.2 | π0.5 | HQ | 100k | From 1.3 + RABC + Rel |
188
+ | 2.3 | π0.5 | HQ + mirror | 100k | From 1.3 + Rel + mirroring |
189
+ | 2.4 | π0.5 | HQ | 100k | From 1.3 · chunk=45 |
190
+ | 2.5 | π0.5 | HQ | 100k | From 1.7 + RABC + Rel |
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  <HtmlEmbed
193
+ id="experiment-results"
194
+ src="folding/experiment-results.html"
195
+ title="Experiment Results"
196
+ desc="Bars show success rate for the selected level; balloons show Bayesian Beta posterior. Letters above each bar are CLD groups from STEP sequential testing (Bonferroni α=0.10/55). Shared letter means not significantly different. Toggle between Total, Level 1, and Level 2 to re-sort and update. Hover for details."
197
  />
198
 
199
+ Total score captures partial progress that binary success rate misses: even failed rollouts earn credit for completed subtasks. The CLD letters (from **[STEP](https://tri-ml.github.io/step/)** sequential testing with **Bonferroni correction**, α = 0.10, per-pair α < 0.0018, n<sub>max</sub> = 50, following [TRI's statistical evaluation framework](https://medium.com/toyotaresearch/statistical-thinking-for-robot-policy-evaluation-from-rigorous-a-b-testing-to-effective-0ae886fbd68d)) tell a clear story: **fine-tuning separates from initial training**. Experiments 2.5 and 2.2 form group 'a', statistically distinct from the bulk of initial-training experiments which cluster in groups 'c' and 'd'. Within initial training, no experiment separates from another, meaning none of the algorithmic changes (RABC, relative actions, normalization) produced a statistically detectable improvement on their own when trained on the full dataset. The jump only comes from switching to curated high-quality data in fine-tuning. Toggle to Level 2 and the separation is even starker: every initial-training experiment sits in group 'b' with 0% success, while 2.5 stands alone in group 'a'.
200
 
201
+ Success rate isn't the full picture. A policy that succeeds but takes twice as long or produces sloppy folds isn't really better. The heatmap below breaks down per-subtask timing and fold quality across experiments, revealing which policies are not just more reliable but also faster and more precise:
 
 
 
 
 
 
 
202
 
203
  <HtmlEmbed
204
  id="subtask-heatmap"
205
  src="folding/subtask-heatmap.html"
206
  title="Subtask Timing Heatmap"
207
+ desc="Each cell shows the experiment's time relative to the cross-experiment average for that subtask (1.0× = average). The two summary columns after the divider show aggregate L1 completion time and fold quality (1–5). Hover for absolute times. Empty cells indicate the subtask was never completed."
208
  />
209
 
210
+ Speed and quality correlate strongly with data quality: fine-tuned experiments fold 2–3× faster than initial training (40s vs 100s+), and fold quality only breaks past 3.0 with high-quality training data. Faster isn't a separate goal from better: it's a consequence of the policy learning a clear, unambiguous strategy.
211
+
212
  ### Where the policies fail
213
 
214
+ Understanding *how* each experiment fails, not just whether it fails, is essential for knowing what to try next. The chart below breaks down every failed rollout by the subtask where the policy got stuck. Level 2 failures are dominated by "Unfold", most initial-training policies can't reliably spread a messy shirt, so they never reach the folding steps. Fine-tuning dramatically reduces unfold failures (2.5 has just 2). Switch to Level 1 (where unfolding is skipped) and the pattern shifts: failures spread across mid-task folds, and the best fine-tuned experiments nearly eliminate them entirely.
215
 
216
  <HtmlEmbed
217
  id="failure-analysis"
218
  src="folding/failure-analysis.html"
219
  title="Failure Analysis"
220
+ desc="Breakdown of failure modes across experiments for Level 1, Level 2. Use the tabs to switch between levels. Each bar shows which subtask the policy failed at, revealing where different experiments struggle."
 
 
 
 
 
 
 
 
 
 
 
221
  />
222
 
 
app/src/content/chapters/folding/09-learnings.mdx CHANGED
@@ -8,16 +8,16 @@ Running all these experiments taught us a lot. Some expected, some not. Here's w
8
 
9
  Beyond the experiment findings above, several practical insights stood out:
10
 
11
- - **Train a reward model.** [SARM](https://huggingface.co/docs/lerobot/sarm) gave us data scoring, advantage conditioning, and curation in one package. We recommend it even for tasks where you think manual filtering would suffice.
12
  - **Invest in recording quality early.** More time upfront on clean and consistent recordings pays off more than extra volume.
13
- - **Record at higher frequency.** We'd record at 50 fps if we had to do it again. Folding is dynamic and higher record rates capture transitions better.
14
 
15
  ### For the community: the order of operations
16
 
17
  If you're training a policy for a new manipulation task with LeRobot, **here's the sequence we'd recommend**:
18
 
19
  1. **Define your task protocol first.** Before collecting a single episode, define exactly how the task should be performed.
20
- 2. **Collect 50–100 clean demonstrations.** Quality over volume. Consistent technique, good camera angles, deliberate motions. This is your foundation, everything else builds on it.
21
  3. **Train a reward model.** Use [SARM](https://huggingface.co/docs/lerobot/sarm) to score your episodes and enable RABC during training. This allows the policy to focus on the best demonstrations, which is crucial for longer tasks.
22
  4. **Train a baseline and watch it fail.** Film the rollouts. Understanding *how* and *where* it breaks tells you exactly what kind of data to collect next.
23
  5. **Enable action interpolation and [RTC](https://huggingface.co/docs/lerobot/rtc).** This smooths transitions and speeds up execution. Action interpolation upsamples the policy's 30 Hz output to your robot's control frequency (e.g. 90 Hz), and RTC overlaps inference with execution. Both features are available at inference time with the corresponding flags:
@@ -31,7 +31,7 @@ python examples/rtc/eval_with_real_robot.py \
31
  --interpolation_multiplier=3
32
  ```
33
 
34
- 6. **Find the right [action representation](https://huggingface.co/docs/lerobot/action_representations).** LeRobot uses absolute actions by default. Switching to relative trajectory was one of our key improvements, and unlocked consistency with π0.5 pretraining. To enable relative actions for π0/π0.5 using LeRobot, first precompute the relative action statistics for your dataset, then train with the flag enabled:
35
 
36
  ```bash
37
  # Precompute relative action stats
@@ -56,14 +56,10 @@ lerobot-train \
56
  All the innovations from this project [SARM](https://huggingface.co/docs/lerobot/sarm), [RTC](https://huggingface.co/docs/lerobot/rtc), DAgger, [OpenArm](https://huggingface.co/docs/lerobot/openarm), and OpenArm Mini are merged into [LeRobot repository](https://github.com/huggingface/lerobot). You can use our full pipeline as a starting point and swap in your own task.
57
  </Note>
58
 
59
- ### What's next
60
 
61
- This project is far from done. We're releasing the final model, full dataset, and all training configs on HF Hub. Here's where LeRobot is headed next:
62
 
63
- - **Massive-scale training.** We want LeRobot and LeRobotDataset to support 10–100x the data we used here, with billions of frames, powered by the new [HF Buckets](https://huggingface.co/docs/hub/en/storage-buckets) for storage and streaming at scale.
64
- - **More robots, teleoperators, VLAs, and reward models.** We're continuing to expand the ecosystem of supported hardware, teleoperation setups, and model architectures in LeRobot.
65
- - **Reinforcement Learning support.** Extending LeRobot with new reinforcement learning methods and all the infrastructure needed to train policies online, not just from offline demonstrations.
66
- - **Better data interpretability tools.** Our biggest lever was data quality, but finding *which* demonstrations help and which hurt is still a hard problem. The field needs tools that provide understanding of what makes a trajectory useful before you train on it. If you're exploring data curation or interpretability for robotics, we'd love to hear from you — [let's talk](https://huggingface.co/lerobot).
67
- - **Democratize robot learning.** Continue to lower the barrier to entry and share every insight, tool, and method with the community.
68
 
69
- We also encourage you to use our dataset directly. Train your own policies, try new architectures, experiment with different training recipes. If you find something promising, we'd be happy to run your models on our physical setups and share the results back !
 
8
 
9
  Beyond the experiment findings above, several practical insights stood out:
10
 
11
+ - **Train a reward model.** [SARM](https://huggingface.co/docs/lerobot/sarm) gave us data scoring, advantage conditioning, and curation in one package.
12
  - **Invest in recording quality early.** More time upfront on clean and consistent recordings pays off more than extra volume.
13
+ - **Record/Teleoperate at higher frequency.** We'd record at 50 fps instead of 30 fps if we had to do it again. Folding is dynamic and higher record rates capture transitions better.
14
 
15
  ### For the community: the order of operations
16
 
17
  If you're training a policy for a new manipulation task with LeRobot, **here's the sequence we'd recommend**:
18
 
19
  1. **Define your task protocol first.** Before collecting a single episode, define exactly how the task should be performed.
20
+ 2. **Collect 30-50 clean demonstrations per item/background** Quality over volume. Consistent technique, deliberate motions. This is your foundation, everything else builds on it.
21
  3. **Train a reward model.** Use [SARM](https://huggingface.co/docs/lerobot/sarm) to score your episodes and enable RABC during training. This allows the policy to focus on the best demonstrations, which is crucial for longer tasks.
22
  4. **Train a baseline and watch it fail.** Film the rollouts. Understanding *how* and *where* it breaks tells you exactly what kind of data to collect next.
23
  5. **Enable action interpolation and [RTC](https://huggingface.co/docs/lerobot/rtc).** This smooths transitions and speeds up execution. Action interpolation upsamples the policy's 30 Hz output to your robot's control frequency (e.g. 90 Hz), and RTC overlaps inference with execution. Both features are available at inference time with the corresponding flags:
 
31
  --interpolation_multiplier=3
32
  ```
33
 
34
+ 6. **Find the right [action representation](https://huggingface.co/docs/lerobot/main/en/action_representations).** LeRobot uses absolute actions by default. Switching to relative trajectory was one of our key improvements, and unlocked consistency with π0.5 pretraining. To enable relative actions for π0/π0.5 using LeRobot, first precompute the relative action statistics for your dataset, then train with the flag enabled:
35
 
36
  ```bash
37
  # Precompute relative action stats
 
56
  All the innovations from this project [SARM](https://huggingface.co/docs/lerobot/sarm), [RTC](https://huggingface.co/docs/lerobot/rtc), DAgger, [OpenArm](https://huggingface.co/docs/lerobot/openarm), and OpenArm Mini are merged into [LeRobot repository](https://github.com/huggingface/lerobot). You can use our full pipeline as a starting point and swap in your own task.
57
  </Note>
58
 
59
+ ### The bigger picture
60
 
61
+ Across 11 experiments we varied models, action representations, reward weighting, and chunk sizes. Algorithmic changes moved success rates by 5–20 percentage points. Finetuning on a curated dataset one-fifth the size moved it by 50. The bottleneck wasn't the model, it was the data. That means you don't need thousands of hours or a fleet of robots to get strong results. You need dozens of hours of clean, consistent demonstrations and the tooling to score and curate them, which now exists in LeRobot.
62
 
63
+ ### Build on this
 
 
 
 
64
 
65
+ We open-sourced everything: code, data, models, reward model, and now the full story of what worked and what didn't, so the community can push further. Use our [dataset](https://huggingface.co/datasets/lerobot/high_quality_folding) directly, try new architectures, experiment with different recipes. If you find something that works, we'd be happy to run your models on our physical setups and share the results back.
app/src/content/chapters/folding/12-references.mdx CHANGED
@@ -23,11 +23,24 @@ This project would not have been possible without the contributions and support
23
 
24
  A special thank you to our robot operators, who spent countless hours patiently demonstrating shirt folds and collecting the high-quality data that made this entire project possible: **Shiyu Liu**, **Alison Magniez**, **Hamza Ben Taieb**, **Victor Gomez**, **Stéphane Combo**, **Domitille Bissery**, **Tiphaine de Cherisey**, **Nathalie Vi** and **Melaku Yemaneberhan**
25
 
 
 
26
  ### Papers
27
 
28
- - **π0.5** Black et al. (2025). *A Vision-Language-Action Model with Open-World Generalization.* [pi.website/blog/pi05](https://www.pi.website/blog/pi05) · [LeRobot docs](https://huggingface.co/docs/lerobot/pi05)
 
 
 
 
 
 
29
  - **RTC** Black, Galliker & Levine (2025). *Real-Time Execution of Action Chunking Flow Policies.* [pi.website/research/real_time_chunking](https://www.pi.website/research/real_time_chunking) · [LeRobot docs](https://huggingface.co/docs/lerobot/rtc)
30
  - **SARM** Chen et al. (2025). *Stage-Aware Reward Modeling for Long Horizon Robot Manipulation.* [arxiv.org/abs/2509.25358](https://arxiv.org/abs/2509.25358) · [LeRobot docs](https://huggingface.co/docs/lerobot/sarm)
 
31
  - **DAgger** Ross, Gordon & Bagnell (2011). *A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning.* [arxiv.org/abs/1011.0686](https://arxiv.org/abs/1011.0686)
 
 
 
 
32
 
33
  *PS if you want to use this format for writing your own blog, check out the [Research Article Template](https://huggingface.co/spaces/tfrere/research-article-template).*
 
23
 
24
  A special thank you to our robot operators, who spent countless hours patiently demonstrating shirt folds and collecting the high-quality data that made this entire project possible: **Shiyu Liu**, **Alison Magniez**, **Hamza Ben Taieb**, **Victor Gomez**, **Stéphane Combo**, **Domitille Bissery**, **Tiphaine de Cherisey**, **Nathalie Vi** and **Melaku Yemaneberhan**
25
 
26
+ We also want to thank the [Enactic](https://github.com/enactic/openarm) team and [WowRobo](https://wowrobo.com/) for their support throughout this project.
27
+
28
  ### Papers
29
 
30
+ #### Models we trained
31
+
32
+ - **π0** Black et al. (2024). *π0: A Vision-Language-Action Flow Model for General Robot Control.* [arxiv.org/abs/2410.24164](https://arxiv.org/abs/2410.24164) · [LeRobot docs](https://huggingface.co/docs/lerobot/pi0)
33
+ - **π0.5** Black et al. (2025). *π0.5: A Vision-Language-Action Model with Open-World Generalization.* [pi.website/blog/pi05](https://www.pi.website/blog/pi05) · [LeRobot docs](https://huggingface.co/docs/lerobot/pi05)
34
+
35
+ #### Techniques we used
36
+
37
  - **RTC** Black, Galliker & Levine (2025). *Real-Time Execution of Action Chunking Flow Policies.* [pi.website/research/real_time_chunking](https://www.pi.website/research/real_time_chunking) · [LeRobot docs](https://huggingface.co/docs/lerobot/rtc)
38
  - **SARM** Chen et al. (2025). *Stage-Aware Reward Modeling for Long Horizon Robot Manipulation.* [arxiv.org/abs/2509.25358](https://arxiv.org/abs/2509.25358) · [LeRobot docs](https://huggingface.co/docs/lerobot/sarm)
39
+ - **UMI** Chi et al. (2024). *Universal Manipulation Interface: In-The-Wild Robot Teaching Without In-The-Wild Robots.* [arxiv.org/abs/2402.10329](https://arxiv.org/abs/2402.10329)
40
  - **DAgger** Ross, Gordon & Bagnell (2011). *A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning.* [arxiv.org/abs/1011.0686](https://arxiv.org/abs/1011.0686)
41
+ - **HG-DAgger** Kelly et al. (2019). *HG-DAgger: Interactive Imitation Learning with Human Experts.* [arxiv.org/abs/1810.02890](https://arxiv.org/abs/1810.02890)
42
+ - **RaC** Hu et al. (2025). *Robot Learning for Long-Horizon Tasks by Scaling Recovery and Correction.* [arxiv.org/abs/2509.07953](https://arxiv.org/abs/2509.07953)
43
+ - **STEP** Mandalika et al. (2025). *STEP: A Statistical Testing Framework for Efficient Policy Evaluation.* [tri-ml.github.io/step](https://tri-ml.github.io/step/)
44
+
45
 
46
  *PS if you want to use this format for writing your own blog, check out the [Research Article Template](https://huggingface.co/spaces/tfrere/research-article-template).*
app/src/content/embeds/folding/action-representations.html CHANGED
@@ -59,20 +59,20 @@ g.append('path').datum(poses).attr('d', line)
59
  .attr('stroke', '#444').attr('stroke-dasharray', '5,4').attr('stroke-width', 0.8);
60
  });
61
  g.append('text').attr('x', x(2)).attr('y', -4).attr('text-anchor', 'middle')
62
- .attr('fill', SUB).attr('font-size', 9).text('Inference start at t=0');
63
  g.append('text').attr('x', x(6)).attr('y', -4).attr('text-anchor', 'middle')
64
- .attr('fill', SUB).attr('font-size', 9).text('Inference start at t=4');
65
 
66
  // Axes
67
  g.append('line').attr('x1', x(0)).attr('x2', x(8.3)).attr('y1', h).attr('y2', h).attr('stroke', '#444');
68
  g.append('line').attr('x1', x(0)).attr('x2', x(0)).attr('y1', h).attr('y2', y(6.2)).attr('stroke', '#444');
69
  for (let t = 0; t <= 8; t++) {
70
  g.append('text').attr('x', x(t)).attr('y', h + 14).attr('text-anchor', 'middle')
71
- .attr('fill', SUB).attr('font-size', 10).text(t);
72
  }
73
- g.append('text').attr('x', x(8.4)).attr('y', h + 4).attr('fill', SUB).attr('font-size', 10).text('Time');
74
  g.append('text').attr('x', x(-0.1)).attr('y', y(6.4)).attr('text-anchor', 'end')
75
- .attr('fill', SUB).attr('font-size', 10).text('Pose');
76
 
77
  // --- DELTA (yellow): straight arrows, offset below trajectory ---
78
  const dOff = 14;
@@ -112,7 +112,7 @@ const bY = h + 22;
112
  .attr('d', `M${x(t0)},${bY-3} L${x(t0)},${bY} L${x(t1)},${bY} L${x(t1)},${bY-3}`)
113
  .attr('fill', 'none').attr('stroke', COL_REL).attr('stroke-width', 1.2).attr('opacity', 0.5);
114
  g.append('text').attr('x', (x(t0) + x(t1)) / 2).attr('y', bY + 12)
115
- .attr('text-anchor', 'middle').attr('fill', COL_REL).attr('font-size', 9).attr('opacity', 0.7)
116
  .text('chunk = 4');
117
  });
118
 
@@ -138,7 +138,7 @@ legItems.forEach(item => {
138
  }
139
  lx += sw + 6;
140
  const t = g.append('text').attr('x', lx).attr('y', legY + 6)
141
- .attr('fill', item.col).attr('font-size', 10).attr('font-weight', 500).text(item.label);
142
  lx += t.node().getComputedTextLength() + 16;
143
  });
144
  }
 
59
  .attr('stroke', '#444').attr('stroke-dasharray', '5,4').attr('stroke-width', 0.8);
60
  });
61
  g.append('text').attr('x', x(2)).attr('y', -4).attr('text-anchor', 'middle')
62
+ .attr('fill', SUB).attr('font-size', 11).text('Inference start at t=0');
63
  g.append('text').attr('x', x(6)).attr('y', -4).attr('text-anchor', 'middle')
64
+ .attr('fill', SUB).attr('font-size', 11).text('Inference start at t=4');
65
 
66
  // Axes
67
  g.append('line').attr('x1', x(0)).attr('x2', x(8.3)).attr('y1', h).attr('y2', h).attr('stroke', '#444');
68
  g.append('line').attr('x1', x(0)).attr('x2', x(0)).attr('y1', h).attr('y2', y(6.2)).attr('stroke', '#444');
69
  for (let t = 0; t <= 8; t++) {
70
  g.append('text').attr('x', x(t)).attr('y', h + 14).attr('text-anchor', 'middle')
71
+ .attr('fill', SUB).attr('font-size', 12).text(t);
72
  }
73
+ g.append('text').attr('x', x(8.4)).attr('y', h + 4).attr('fill', SUB).attr('font-size', 12).text('Time');
74
  g.append('text').attr('x', x(-0.1)).attr('y', y(6.4)).attr('text-anchor', 'end')
75
+ .attr('fill', SUB).attr('font-size', 12).text('Pose');
76
 
77
  // --- DELTA (yellow): straight arrows, offset below trajectory ---
78
  const dOff = 14;
 
112
  .attr('d', `M${x(t0)},${bY-3} L${x(t0)},${bY} L${x(t1)},${bY} L${x(t1)},${bY-3}`)
113
  .attr('fill', 'none').attr('stroke', COL_REL).attr('stroke-width', 1.2).attr('opacity', 0.5);
114
  g.append('text').attr('x', (x(t0) + x(t1)) / 2).attr('y', bY + 12)
115
+ .attr('text-anchor', 'middle').attr('fill', COL_REL).attr('font-size', 11).attr('opacity', 0.7)
116
  .text('chunk = 4');
117
  });
118
 
 
138
  }
139
  lx += sw + 6;
140
  const t = g.append('text').attr('x', lx).attr('y', legY + 6)
141
+ .attr('fill', item.col).attr('font-size', 12).attr('font-weight', 500).text(item.label);
142
  lx += t.node().getComputedTextLength() + 16;
143
  });
144
  }
app/src/content/embeds/folding/dagger-explainer.html ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <style>
7
+ * { box-sizing: border-box; margin: 0; padding: 0; }
8
+ body { background: transparent; font-family: system-ui, -apple-system, sans-serif; }
9
+ .dagger-wrap { position: relative; width: 100%; }
10
+ .dagger-desc {
11
+ text-align: center; font-size: 14px; font-weight: 500;
12
+ min-height: 36px; padding: 2px 20px; line-height: 1.6;
13
+ transition: color 0.3s;
14
+ }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <div class="dagger-wrap">
19
+ <svg id="dagger-svg"></svg>
20
+ <div class="dagger-desc" id="dagger-desc"></div>
21
+ </div>
22
+ <script>
23
+ function _initDAgger() {
24
+
25
+ var container = document.getElementById("dagger-svg").parentElement;
26
+ var svg = d3.select("#dagger-svg");
27
+ var descEl = document.getElementById("dagger-desc");
28
+
29
+ function isLight() {
30
+ try {
31
+ var el = document.querySelector("[data-theme]") || document.documentElement;
32
+ return el.getAttribute("data-theme") === "light";
33
+ } catch(e) { return false; }
34
+ }
35
+
36
+ var DARK = {
37
+ text: "#e8eaf0", subtext: "#8b8fa8", dim: "#333", dimArrow: "#3a3d50",
38
+ nodeBg: "#1a1d2e", glowAlpha: 0.22, barBg: "#252540", barFill: "#10b981",
39
+ };
40
+ var LIGHT = {
41
+ text: "#1a1a2e", subtext: "#6b7280", dim: "#d1d5db", dimArrow: "#bfc3cc",
42
+ nodeBg: "#f9fafb", glowAlpha: 0.14, barBg: "#e5e7eb", barFill: "#10b981",
43
+ };
44
+ function th() { return isLight() ? LIGHT : DARK; }
45
+
46
+ var stages = [
47
+ {
48
+ id: "policy", label: "Policy runs",
49
+ dark: "#22d3ee", light: "#0891b2",
50
+ desc: "The learned policy executes autonomously on a new task attempt",
51
+ },
52
+ {
53
+ id: "failure", label: "Failure",
54
+ dark: "#ef4444", light: "#dc2626",
55
+ desc: "Policy drifts into unfamiliar states. Traditional cloning suffers T\u00B2\u03B5 compounding errors.",
56
+ },
57
+ {
58
+ id: "human", label: "Human takeover",
59
+ dark: "#f59e0b", light: "#d97706",
60
+ desc: "Operator pauses the policy and teleoperates the correct recovery",
61
+ },
62
+ {
63
+ id: "record", label: "Record correction",
64
+ dark: "#10b981", light: "#059669",
65
+ desc: "Correction trajectory added to the aggregate dataset: D \u2190 D \u222A D\u1D62",
66
+ },
67
+ {
68
+ id: "retrain", label: "Retrain",
69
+ dark: "#8b5cf6", light: "#7c3aed",
70
+ desc: "Policy retrained on all data. Each iteration closes the distribution gap.",
71
+ },
72
+ ];
73
+
74
+ var N = stages.length;
75
+ var activeIdx = 0;
76
+ var iteration = 1;
77
+ var animTimer = null;
78
+
79
+ function col(i) { return isLight() ? stages[i].light : stages[i].dark; }
80
+
81
+ function drawIcon(g, id, cx, cy, size, fill) {
82
+ var s = size;
83
+ if (id === "policy") {
84
+ g.append("polygon")
85
+ .attr("points", (cx-s*0.22)+","+(cy-s*0.42)+" "+(cx+s*0.48)+","+cy+" "+(cx-s*0.22)+","+(cy+s*0.42))
86
+ .attr("fill", fill);
87
+ } else if (id === "failure") {
88
+ g.append("rect")
89
+ .attr("x", cx-1.6).attr("y", cy-s*0.34).attr("width", 3.2).attr("height", s*0.4)
90
+ .attr("rx", 1.6).attr("fill", fill);
91
+ g.append("circle").attr("cx", cx).attr("cy", cy+s*0.26).attr("r", 2).attr("fill", fill);
92
+ } else if (id === "human") {
93
+ g.append("circle").attr("cx", cx).attr("cy", cy-s*0.2).attr("r", s*0.16).attr("fill", fill);
94
+ g.append("path")
95
+ .attr("d", "M"+(cx-s*0.32)+","+(cy+s*0.38)+" Q"+(cx-s*0.32)+","+(cy+s*0.06)+" "+cx+","+(cy+s*0.06)+" Q"+(cx+s*0.32)+","+(cy+s*0.06)+" "+(cx+s*0.32)+","+(cy+s*0.38))
96
+ .attr("fill", fill);
97
+ } else if (id === "record") {
98
+ var lw = 2.6;
99
+ g.append("line").attr("x1", cx-s*0.28).attr("y1", cy).attr("x2", cx+s*0.28).attr("y2", cy)
100
+ .attr("stroke", fill).attr("stroke-width", lw).attr("stroke-linecap", "round");
101
+ g.append("line").attr("x1", cx).attr("y1", cy-s*0.28).attr("x2", cx).attr("y2", cy+s*0.28)
102
+ .attr("stroke", fill).attr("stroke-width", lw).attr("stroke-linecap", "round");
103
+ } else if (id === "retrain") {
104
+ var rr = s * 0.3;
105
+ var sa = -Math.PI * 0.65, ea = Math.PI * 0.8;
106
+ g.append("path")
107
+ .attr("d", "M"+(cx+rr*Math.cos(sa))+","+(cy+rr*Math.sin(sa))+" A"+rr+","+rr+" 0 1,1 "+(cx+rr*Math.cos(ea))+","+(cy+rr*Math.sin(ea)))
108
+ .attr("fill", "none").attr("stroke", fill).attr("stroke-width", 2.4)
109
+ .attr("stroke-linecap", "round");
110
+ var tx = cx + rr * Math.cos(ea), ty = cy + rr * Math.sin(ea);
111
+ var tang = ea + Math.PI / 2;
112
+ var as = 4.5;
113
+ g.append("polygon")
114
+ .attr("points",
115
+ (tx + as * Math.cos(tang - 0.6))+","+(ty + as * Math.sin(tang - 0.6))+" "+
116
+ tx+","+ty+" "+
117
+ (tx + as * Math.cos(tang + 0.6))+","+(ty + as * Math.sin(tang + 0.6)))
118
+ .attr("fill", fill);
119
+ }
120
+ }
121
+
122
+ function render() {
123
+ svg.selectAll("*").remove();
124
+ var c = th();
125
+
126
+ var W = container.clientWidth || 500;
127
+ var padX = 74;
128
+ var R = Math.min((W - padX * 2) * 0.42, 118);
129
+ var nodeR = Math.min(26, R * 0.24);
130
+ var labelH = nodeR + 14 + 8;
131
+ var cx = W / 2, cy = R + labelH + 6;
132
+ var H = cy + R + labelH + 10;
133
+
134
+ svg.attr("width", W).attr("height", H);
135
+ var g = svg.append("g");
136
+
137
+ var defs = svg.append("defs");
138
+
139
+ var angleStart = -Math.PI / 2;
140
+ var positions = stages.map(function(s, i) {
141
+ var a = angleStart + (2 * Math.PI * i) / N;
142
+ return { x: cx + R * Math.cos(a), y: cy + R * Math.sin(a), angle: a };
143
+ });
144
+
145
+ for (var i = 0; i < N; i++) {
146
+ var isAct = i === activeIdx;
147
+ var ac = isAct ? col(i) : c.dimArrow;
148
+ defs.append("marker").attr("id", "da-" + i)
149
+ .attr("viewBox", "0 0 10 8").attr("refX", 9).attr("refY", 4)
150
+ .attr("markerWidth", 6).attr("markerHeight", 5).attr("orient", "auto")
151
+ .append("polygon").attr("points", "0,0 10,4 0,8").attr("fill", ac);
152
+ }
153
+
154
+ for (var i = 0; i < N; i++) {
155
+ var from = positions[i], to = positions[(i + 1) % N];
156
+ var isAct = i === activeIdx;
157
+ var dx = to.x - from.x, dy = to.y - from.y;
158
+ var dist = Math.sqrt(dx * dx + dy * dy);
159
+ var ux = dx / dist, uy = dy / dist;
160
+ var gap = nodeR + 6;
161
+ var x1 = from.x + ux * gap, y1 = from.y + uy * gap;
162
+ var x2 = to.x - ux * (gap + 4), y2 = to.y - uy * (gap + 4);
163
+
164
+ var mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
165
+ var bow = dist * 0.1;
166
+ var pmx = (from.x + to.x) / 2 - cx, pmy = (from.y + to.y) / 2 - cy;
167
+ var pDist = Math.sqrt(pmx * pmx + pmy * pmy) || 1;
168
+ var cpx = mx + (pmx / pDist) * bow, cpy = my + (pmy / pDist) * bow;
169
+
170
+ g.append("path")
171
+ .attr("d", "M"+x1+","+y1+" Q"+cpx+","+cpy+" "+x2+","+y2)
172
+ .attr("fill", "none")
173
+ .attr("stroke", isAct ? col(i) : c.dimArrow)
174
+ .attr("stroke-width", isAct ? 2.2 : 1.4)
175
+ .attr("marker-end", "url(#da-" + i + ")")
176
+ .attr("opacity", isAct ? 1 : 0.35);
177
+ }
178
+
179
+ positions.forEach(function(pos, i) {
180
+ var s = stages[i];
181
+ var isAct = i === activeIdx;
182
+ var sc = col(i);
183
+
184
+ if (isAct) {
185
+ g.append("circle")
186
+ .attr("cx", pos.x).attr("cy", pos.y).attr("r", nodeR + 6)
187
+ .attr("fill", sc).attr("opacity", c.glowAlpha);
188
+ }
189
+
190
+ g.append("circle")
191
+ .attr("cx", pos.x).attr("cy", pos.y).attr("r", nodeR)
192
+ .attr("fill", isAct ? sc : c.nodeBg)
193
+ .attr("stroke", isAct ? sc : c.dim)
194
+ .attr("stroke-width", isAct ? 2 : 1.4)
195
+ .attr("opacity", isAct ? 1 : 0.55);
196
+
197
+ var iconG = g.append("g").attr("opacity", isAct ? 1 : 0.5);
198
+ drawIcon(iconG, s.id, pos.x, pos.y, nodeR * 0.8, isAct ? "#fff" : sc);
199
+
200
+ var a = pos.angle;
201
+ var cosA = Math.cos(a), sinA = Math.sin(a);
202
+ var labelDist = nodeR + 14;
203
+ var lx = pos.x + labelDist * cosA;
204
+ var ly = pos.y + labelDist * sinA;
205
+ var anchor = "middle";
206
+ if (cosA < -0.3) anchor = "end";
207
+ else if (cosA > 0.3) anchor = "start";
208
+ if (sinA < -0.5) ly -= 3;
209
+ if (sinA > 0.5) ly += 5;
210
+ lx = Math.max(4, Math.min(W - 4, lx));
211
+
212
+ g.append("text")
213
+ .attr("x", lx).attr("y", ly)
214
+ .attr("text-anchor", anchor).attr("dominant-baseline", "middle")
215
+ .attr("fill", isAct ? (isLight() ? sc : "#e8eaf0") : c.subtext)
216
+ .attr("font-size", 12).attr("font-weight", isAct ? 700 : 400)
217
+ .attr("font-family", "system-ui, sans-serif")
218
+ .text(s.label);
219
+ });
220
+
221
+ g.append("text")
222
+ .attr("x", cx).attr("y", cy - 8)
223
+ .attr("text-anchor", "middle").attr("dominant-baseline", "middle")
224
+ .attr("font-size", 12).attr("font-weight", 600)
225
+ .attr("fill", c.subtext).attr("font-family", "system-ui, sans-serif")
226
+ .text("Iteration " + iteration);
227
+
228
+ var barW = R * 0.55, barH = 3.5, barY = cy + 5;
229
+ g.append("rect")
230
+ .attr("x", cx - barW / 2).attr("y", barY)
231
+ .attr("width", barW).attr("height", barH)
232
+ .attr("rx", 1.75).attr("fill", c.barBg);
233
+ var fillW = barW * Math.min(1, iteration / 6);
234
+ if (fillW > 0) {
235
+ g.append("rect")
236
+ .attr("x", cx - barW / 2).attr("y", barY)
237
+ .attr("width", fillW).attr("height", barH)
238
+ .attr("rx", 1.75).attr("fill", c.barFill).attr("opacity", 0.7);
239
+ }
240
+ g.append("text")
241
+ .attr("x", cx).attr("y", barY + barH + 10)
242
+ .attr("text-anchor", "middle").attr("dominant-baseline", "middle")
243
+ .attr("font-size", 10).attr("fill", c.subtext).attr("opacity", 0.6)
244
+ .attr("font-family", "system-ui, sans-serif")
245
+ .text("dataset size");
246
+
247
+ descEl.textContent = stages[activeIdx].desc;
248
+ descEl.style.color = col(activeIdx);
249
+ }
250
+
251
+ function step() {
252
+ activeIdx = (activeIdx + 1) % N;
253
+ if (activeIdx === 0) iteration = iteration >= 6 ? 1 : iteration + 1;
254
+ render();
255
+ }
256
+
257
+ render();
258
+ animTimer = setInterval(step, 2200);
259
+ window.addEventListener("resize", render);
260
+
261
+ svg.node().addEventListener("click", function() {
262
+ clearInterval(animTimer);
263
+ step();
264
+ animTimer = setInterval(step, 2200);
265
+ });
266
+
267
+ var obs = new MutationObserver(function() { render(); });
268
+ var themeEl = document.querySelector("[data-theme]") || document.documentElement;
269
+ obs.observe(themeEl, { attributes: true, attributeFilter: ["data-theme"] });
270
+
271
+ }
272
+
273
+ if (typeof d3 !== "undefined") { _initDAgger(); }
274
+ else { var s = document.createElement("script"); s.src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"; s.onload=_initDAgger; document.head.appendChild(s); }
275
+ </script>
276
+ </body>
277
+ </html>
app/src/content/embeds/folding/experiment-results.html ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <style>
7
+ :root { --bg: transparent; --text: #e8eaf0; --subtext: #8b8fa8; --grid: #2a2d3a; --border: #2a2d3a; }
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { background: var(--bg); font-family: system-ui, sans-serif; color: var(--text); }
10
+
11
+ .controls {
12
+ display: flex; align-items: center; justify-content: center;
13
+ gap: 8px; flex-wrap: wrap; margin-bottom: 8px;
14
+ }
15
+ .ctrl-label { font-size: 13px; color: var(--subtext); }
16
+ .toggle-btn {
17
+ background: #1a1d27; border: 1.5px solid #2a2d3a; color: #8b8fa8;
18
+ font-size: 14px; padding: 6px 16px; border-radius: 20px; cursor: pointer;
19
+ transition: all .15s; user-select: none; font-weight: 500;
20
+ }
21
+ .toggle-btn:hover { border-color: #555; color: #e8eaf0; }
22
+ .toggle-btn.active { background: rgba(167,139,250,0.18); border-color: #a78bfa; color: #a78bfa; }
23
+ .filter-toggle {
24
+ font-size: 12px; color: #8b8fa8; cursor: pointer; border: 1px solid #2a2d3a;
25
+ background: #1a1d27; padding: 4px 12px; border-radius: 14px; transition: all .15s;
26
+ user-select: none; display: inline-flex; align-items: center; gap: 4px;
27
+ }
28
+ .filter-toggle:hover { border-color: #555; color: #e8eaf0; }
29
+ .filter-toggle.showing-all { background: rgba(248,147,79,0.12); border-color: #f7934f; color: #f7934f; }
30
+
31
+ .legend {
32
+ display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-bottom: 6px;
33
+ font-size: 13px; color: var(--subtext);
34
+ }
35
+ .legend-item { display: flex; align-items: center; gap: 5px; }
36
+ .legend-swatch { width: 14px; height: 10px; border-radius: 2px; }
37
+ .legend-balloon { width: 10px; height: 18px; border-radius: 50%; background: rgba(167,139,250,0.22); border: 1px solid #a78bfa; }
38
+
39
+ .info-note {
40
+ background: rgba(255,255,255,0.03); border: 1px solid #2a2d3a;
41
+ border-radius: 8px; padding: 8px 14px; margin-bottom: 8px;
42
+ font-size: 12px; color: #8b8fa8; line-height: 1.6;
43
+ display: flex; align-items: flex-start; gap: 10px;
44
+ }
45
+ .info-note-icon { font-size: 13px; flex-shrink: 0; margin-top: 1px; }
46
+ .info-note strong { color: #c8cad8; }
47
+
48
+ .tooltip {
49
+ position: absolute; background: #1a1d27; border: 1px solid var(--border);
50
+ border-radius: 8px; padding: 10px 14px; font-size: 13px; pointer-events: none;
51
+ opacity: 0; transition: opacity .15s; z-index: 10; min-width: 250px;
52
+ box-shadow: 0 4px 16px rgba(0,0,0,.4);
53
+ }
54
+ .tooltip strong { display: block; margin-bottom: 6px; font-size: 14px; color: var(--text); }
55
+ .tooltip-row { display: flex; justify-content: space-between; gap: 12px; margin-top: 3px; font-size: 12px; color: var(--subtext); }
56
+ .tooltip-row span:last-child { color: var(--text); font-weight: 600; }
57
+ .tooltip-ci { font-size: 10px; color: #555; margin-left: 4px; }
58
+ .tooltip-note { margin-top: 7px; padding-top: 7px; border-top: 1px solid #2a2d3a; font-size: 11px; color: #8b8fa8; line-height: 1.5; }
59
+
60
+ .axis text { fill: var(--subtext); font-size: 13px; }
61
+ .axis line, .axis path { stroke: var(--grid); }
62
+ .grid line { stroke: var(--grid); stroke-dasharray: 2,4; opacity: 0.5; }
63
+ .bar { cursor: pointer; transition: opacity .12s; }
64
+ .bar:hover { opacity: 0.85 !important; }
65
+ </style>
66
+ </head>
67
+ <body>
68
+
69
+ <div class="controls">
70
+ <span class="ctrl-label">Success rate overlay:</span>
71
+ <button class="toggle-btn active" id="btn-total" onclick="setLevel('total')">Total</button>
72
+ <button class="toggle-btn" id="btn-l1" onclick="setLevel('l1')">Level 1</button>
73
+ <button class="toggle-btn" id="btn-l2" onclick="setLevel('l2')">Level 2</button>
74
+ <span style="width:12px"></span>
75
+ <button class="filter-toggle" id="btn-filter" onclick="toggleFilter()">Show all 11</button>
76
+ </div>
77
+
78
+ <div class="legend">
79
+ <div class="legend-item"><div class="legend-swatch" style="background:#f7934f"></div>Initial training (1.x)</div>
80
+ <div class="legend-item"><div class="legend-swatch" style="background:#4dc98a"></div>Fine-tuned (2.x)</div>
81
+ <div class="legend-item"><div class="legend-balloon"></div>Success rate posterior</div>
82
+ <div class="legend-item" style="color:#a78bfa;font-size:12px"><strong>a</strong>,<strong>b</strong>… = CLD group</div>
83
+ </div>
84
+
85
+ <div class="info-note">
86
+ <span class="info-note-icon">ℹ</span>
87
+ <span>
88
+ Bars = success rate for selected level. Violin overlays = Bayesian posterior.
89
+ Letters = CLD from <a href="https://tri-ml.github.io/step/" style="color:#818cf8">STEP</a> sequential test (<a href="https://medium.com/toyotaresearch/statistical-thinking-for-robot-policy-evaluation-from-rigorous-a-b-testing-to-effective-0ae886fbd68d" style="color:#818cf8">TRI framework</a>, Bonferroni α=0.10/55); shared letter = no significant difference.
90
+ </span>
91
+ </div>
92
+
93
+ <div style="position:relative">
94
+ <svg id="er-chart" style="overflow:visible"></svg>
95
+ <div class="tooltip" id="er-tooltip"></div>
96
+ </div>
97
+
98
+ <script>
99
+ function _initExperimentResults() {
100
+
101
+ const EXPERIMENTS = {
102
+ "1.1 π0": { note:"π0 base model fine-tuned on full dataset. Default normalization." },
103
+ "1.2 π0.5": { note:"Upgraded to π0.5 with MEAN_STD normalization." },
104
+ "1.3 Relative": { note:"Adds relative actions and switches to QUANTILES normalization." },
105
+ "1.4 RABC low": { note:"RABC with low κ, not very selective. No relative actions." },
106
+ "1.5 RABC high": { note:"RABC with higher κ (mean + ½σ). No relative actions." },
107
+ "1.7 Rel+RABC": { note:"Best of initial training. Base checkpoint for 2.5." },
108
+ "2.1 HQ": { note:"Fine-tunes 1.3 on curated high-quality data. No RABC." },
109
+ "2.2 HQ+RABC+Rel": { note:"Adds RABC on high-quality fine-tune from 1.3." },
110
+ "2.3 HQ+mirror": { note:"Augments HQ data with mirrored trajectories." },
111
+ "2.4 HQ chunk45": { note:"Larger action chunk size (45 vs default 30)." },
112
+ "2.5 HQ+RABC+Rel★": { note:"Top performer. Best overall result." },
113
+ };
114
+
115
+ const N = { total: 20, l1: 10, l2: 10 };
116
+
117
+ const raw = [
118
+ { label:"1.1 π0", series:"1", score:440, pct:29.3, total:40, l1:80, l2:0 },
119
+ { label:"1.2 π0.5", series:"1", score:480, pct:32.0, total:20, l1:40, l2:0 },
120
+ { label:"1.3 Relative", series:"1", score:460, pct:30.7, total:35, l1:70, l2:0 },
121
+ { label:"1.4 RABC low", series:"1", score:330, pct:22.0, total:15, l1:30, l2:0 },
122
+ { label:"1.5 RABC high", series:"1", score:170, pct:11.3, total:0, l1:0, l2:0 },
123
+ { label:"1.7 Rel+RABC", series:"1", score:600, pct:40.0, total:40, l1:80, l2:0 },
124
+ { label:"2.1 HQ", series:"2", score:620, pct:41.3, total:40, l1:70, l2:10 },
125
+ { label:"2.2 HQ+RABC+Rel", series:"2", score:1090, pct:72.7, total:75, l1:100, l2:50 },
126
+ { label:"2.3 HQ+mirror", series:"2", score:310, pct:20.7, total:5, l1:0, l2:10 },
127
+ { label:"2.4 HQ chunk45", series:"2", score:460, pct:30.7, total:20, l1:40, l2:0 },
128
+ { label:"2.5 HQ+RABC+Rel★", series:"2", score:1300, pct:86.7, total:90, l1:100, l2:80 },
129
+ ];
130
+
131
+ const CLD = {
132
+ total: {"2.5 HQ+RABC+Rel★":"a","2.2 HQ+RABC+Rel":"ab","1.1 π0":"bc","1.7 Rel+RABC":"bc","2.1 HQ":"bc","1.3 Relative":"bc","1.2 π0.5":"cd","2.4 HQ chunk45":"cd","1.4 RABC low":"cd","2.3 HQ+mirror":"cd","1.5 RABC high":"d"},
133
+ l1: {"2.2 HQ+RABC+Rel":"a","2.5 HQ+RABC+Rel★":"a","1.1 π0":"a","1.7 Rel+RABC":"a","1.3 Relative":"a","2.1 HQ":"a","1.2 π0.5":"ab","2.4 HQ chunk45":"ab","1.4 RABC low":"ab","2.3 HQ+mirror":"b","1.5 RABC high":"b"},
134
+ l2: {"2.5 HQ+RABC+Rel★":"a","2.2 HQ+RABC+Rel":"ab","2.1 HQ":"ab","2.3 HQ+mirror":"ab","1.1 π0":"b","1.2 π0.5":"b","1.3 Relative":"b","1.4 RABC low":"b","1.5 RABC high":"b","1.7 Rel+RABC":"b","2.4 HQ chunk45":"b"},
135
+ };
136
+
137
+ // Clopper-Pearson exact 90% CI
138
+ function lgamma(x) {
139
+ if (x < 0.5) return Math.log(Math.PI / Math.sin(Math.PI * x)) - lgamma(1 - x);
140
+ x--;
141
+ let a = 0.99999999999980993;
142
+ const c = [676.5203681218851,-1259.1392167224028,771.32342877765313,-176.61502916214059,
143
+ 12.507343278686905,-0.13857109526572012,9.9843695780195716e-6,1.5056327351493116e-7];
144
+ for (let i = 0; i < 8; i++) a += c[i] / (x + i + 1);
145
+ const t = x + 8 - 0.5;
146
+ return 0.5 * Math.log(2 * Math.PI) + (x + 0.5) * Math.log(t) - t + Math.log(a);
147
+ }
148
+ function betaCf(x, a, b) {
149
+ const qab = a + b, qap = a + 1, qam = a - 1;
150
+ let c = 1, d = 1 - qab * x / qap;
151
+ if (Math.abs(d) < 1e-30) d = 1e-30;
152
+ d = 1 / d; let h = d;
153
+ for (let m = 1; m <= 200; m++) {
154
+ const m2 = 2 * m;
155
+ let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
156
+ d = 1 + aa * d; if (Math.abs(d) < 1e-30) d = 1e-30;
157
+ c = 1 + aa / c; if (Math.abs(c) < 1e-30) c = 1e-30;
158
+ d = 1 / d; h *= d * c;
159
+ aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
160
+ d = 1 + aa * d; if (Math.abs(d) < 1e-30) d = 1e-30;
161
+ c = 1 + aa / c; if (Math.abs(c) < 1e-30) c = 1e-30;
162
+ d = 1 / d; const del = d * c; h *= del;
163
+ if (Math.abs(del - 1) < 3e-12) break;
164
+ }
165
+ return h;
166
+ }
167
+ function betaI(x, a, b) {
168
+ if (x <= 0) return 0; if (x >= 1) return 1;
169
+ const bt = Math.exp(lgamma(a+b) - lgamma(a) - lgamma(b) + a*Math.log(x) + b*Math.log(1-x));
170
+ return x < (a+1)/(a+b+2) ? bt * betaCf(x,a,b) / a : 1 - bt * betaCf(1-x,b,a) / b;
171
+ }
172
+ function betaInv(p, a, b) {
173
+ let lo = 0, hi = 1;
174
+ for (let i = 0; i < 100; i++) {
175
+ const mid = (lo + hi) / 2;
176
+ if (betaI(mid, a, b) < p) lo = mid; else hi = mid;
177
+ }
178
+ return (lo + hi) / 2;
179
+ }
180
+ function clopperPearson(pct, n, alpha = 0.10) {
181
+ const k = Math.round(pct / 100 * n);
182
+ const lo = k === 0 ? 0 : betaInv(alpha / 2, k, n - k + 1);
183
+ const hi = k === n ? 1 : betaInv(1 - alpha / 2, k + 1, n - k);
184
+ return { lo: lo * 100, hi: hi * 100 };
185
+ }
186
+
187
+ function betaPdf(x, a, b) {
188
+ if (x <= 0 || x >= 1) return 0;
189
+ return Math.exp(lgamma(a+b) - lgamma(a) - lgamma(b) + (a-1)*Math.log(x) + (b-1)*Math.log(1-x));
190
+ }
191
+
192
+ function betaPosteriorCurve(pct, n, steps) {
193
+ const k = Math.round(pct / 100 * n);
194
+ const a = 1 + k, b = 1 + n - k;
195
+ const ci = clopperPearson(pct, n);
196
+ const lo = Math.max(0, ci.lo / 100 - 0.03);
197
+ const hi = Math.min(1, ci.hi / 100 + 0.03);
198
+ const pts = [];
199
+ for (let i = 0; i <= steps; i++) {
200
+ const p = lo + (hi - lo) * i / steps;
201
+ pts.push({ p: p * 100, density: betaPdf(p, a, b) });
202
+ }
203
+ return pts;
204
+ }
205
+
206
+ // Pre-compute CIs and posteriors
207
+ const allData = raw.map(d => {
208
+ const out = {...d};
209
+ ["total","l1","l2"].forEach(k => {
210
+ out[k + "_ci"] = clopperPearson(d[k], N[k]);
211
+ out[k + "_post"] = betaPosteriorCurve(d[k], N[k], 80);
212
+ });
213
+ return out;
214
+ });
215
+
216
+ const seriesColor = s => s === "2" ? "#4dc98a" : "#f7934f";
217
+ const srColor = "#a78bfa";
218
+ let activeLevel = "total";
219
+ const HIDDEN_BY_DEFAULT = new Set(["1.4 RABC low","1.5 RABC high","2.3 HQ+mirror","2.4 HQ chunk45"]);
220
+ let showAll = false;
221
+
222
+ window.setLevel = function(lv) {
223
+ activeLevel = lv;
224
+ ["total","l1","l2"].forEach(k => {
225
+ document.getElementById("btn-" + k).className = "toggle-btn" + (k === lv ? " active" : "");
226
+ });
227
+ render();
228
+ };
229
+
230
+ window.toggleFilter = function() {
231
+ showAll = !showAll;
232
+ const btn = document.getElementById("btn-filter");
233
+ btn.textContent = showAll ? "Key experiments" : "Show all 11";
234
+ btn.classList.toggle("showing-all", showAll);
235
+ render();
236
+ };
237
+
238
+ const margin = { top: 28, right: 20, bottom: 72, left: 44 };
239
+ const svg = d3.select("#er-chart");
240
+ const container = svg.node().parentElement;
241
+ const tooltip = d3.select("#er-tooltip");
242
+
243
+ function render() {
244
+ svg.selectAll("*").remove();
245
+ const lvKey = activeLevel;
246
+ const data = (showAll ? allData : allData.filter(d => !HIDDEN_BY_DEFAULT.has(d.label)))
247
+ .slice().sort((a, b) => b[lvKey] - a[lvKey] || b.pct - a.pct);
248
+ const W = container.clientWidth;
249
+ const H = Math.max(310, Math.min(420, W * 0.52));
250
+ const w = W - margin.left - margin.right;
251
+ const h = H - margin.top - margin.bottom;
252
+ svg.attr("width", W).attr("height", H);
253
+ const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
254
+
255
+ const x = d3.scaleBand().domain(data.map(d => d.label)).range([0, w]).padding(0.22);
256
+ const y = d3.scaleLinear().domain([0, 105]).range([h, 0]);
257
+
258
+ // Grid — only 50% and 100%
259
+ g.append("g").attr("class","grid").selectAll("line").data([50,100]).join("line")
260
+ .attr("x1",0).attr("x2",w).attr("y1",d=>y(d)).attr("y2",d=>y(d));
261
+
262
+ // X axis
263
+ g.append("g").attr("class","axis").attr("transform",`translate(0,${h})`)
264
+ .call(d3.axisBottom(x).tickSize(0))
265
+ .call(gg => {
266
+ gg.select(".domain").remove();
267
+ gg.selectAll("text").attr("transform","rotate(-40)")
268
+ .attr("text-anchor","end").attr("dx","-0.5em").attr("dy","0.3em").attr("font-size",11);
269
+ });
270
+
271
+ // Y axis
272
+ g.append("g").attr("class","axis")
273
+ .call(d3.axisLeft(y).tickValues([0,50,100]).tickFormat(d => d+"%").tickSize(0))
274
+ .call(ax => { ax.select(".domain").remove(); ax.selectAll(".tick line").remove(); });
275
+
276
+ // Series color pip under each label
277
+ data.forEach(d => {
278
+ g.append("rect")
279
+ .attr("x", x(d.label)).attr("width", x.bandwidth())
280
+ .attr("y", h + 52).attr("height", 3).attr("rx", 1.5)
281
+ .attr("fill", seriesColor(d.series)).attr("opacity", 0.6);
282
+ });
283
+
284
+ const bw = x.bandwidth();
285
+
286
+ data.forEach(d => {
287
+ const bx = x(d.label);
288
+ const cx = bx + bw / 2;
289
+ const col = seriesColor(d.series);
290
+
291
+ // SR bar for active level
292
+ const barVal = d[lvKey];
293
+ g.append("rect").attr("class","bar")
294
+ .attr("x", bx).attr("width", bw)
295
+ .attr("y", y(barVal)).attr("height", h - y(barVal))
296
+ .attr("fill", col).attr("rx", 2).attr("opacity", 0.55)
297
+ .on("mousemove", function(event) { showTooltip(event, d); })
298
+ .on("mouseleave", () => tooltip.style("opacity", 0));
299
+
300
+ // Bayesian posterior balloon (mini violin) — subtle ghost
301
+ const sr = d[activeLevel];
302
+ const post = d[activeLevel + "_post"];
303
+ const maxDensity = d3.max(post, p => p.density) || 1;
304
+ const halfW = bw * 0.22;
305
+
306
+ const areaGen = d3.area()
307
+ .y(p => y(p.p))
308
+ .x0(p => cx - (p.density / maxDensity) * halfW)
309
+ .x1(p => cx + (p.density / maxDensity) * halfW)
310
+ .curve(d3.curveBasis);
311
+
312
+ g.append("path")
313
+ .datum(post)
314
+ .attr("d", areaGen)
315
+ .attr("fill", srColor).attr("fill-opacity", 0.12)
316
+ .attr("stroke", srColor).attr("stroke-width", 0.8).attr("stroke-opacity", 0.45)
317
+ .attr("cursor","pointer")
318
+ .on("mousemove", function(event) { showTooltip(event, d); })
319
+ .on("mouseleave", () => tooltip.style("opacity", 0));
320
+
321
+ // Median tick
322
+ g.append("line")
323
+ .attr("x1", cx - halfW * 0.7).attr("x2", cx + halfW * 0.7)
324
+ .attr("y1", y(sr)).attr("y2", y(sr))
325
+ .attr("stroke", srColor).attr("stroke-width", 1.5).attr("stroke-opacity", 0.7);
326
+
327
+ // CLD letter above bar
328
+ const cldLetter = CLD[activeLevel][d.label] || "?";
329
+ const labelY = Math.min(y(barVal), y(sr)) - 7;
330
+ g.append("text")
331
+ .attr("x", cx).attr("y", labelY)
332
+ .attr("text-anchor","middle").attr("fill", srColor)
333
+ .attr("font-size", 12).attr("font-weight","700")
334
+ .text(cldLetter);
335
+ });
336
+
337
+ // Sort annotation
338
+ const lvLabels = { total:"total SR", l1:"Level 1 SR", l2:"Level 2 SR" };
339
+ g.append("text").attr("x", w).attr("y", -6).attr("text-anchor","end")
340
+ .attr("fill","#555").attr("font-size",11)
341
+ .text("sorted: highest → lowest " + lvLabels[lvKey]);
342
+ }
343
+
344
+ function showTooltip(event, d) {
345
+ const lvLabels = { total:"Total", l1:"Level 1", l2:"Level 2" };
346
+ const cldLetter = CLD[activeLevel][d.label] || "?";
347
+ const col = seriesColor(d.series);
348
+
349
+ tooltip.style("opacity", 1).html(`
350
+ <strong>Experiment ${d.label} <small style="color:${col}">(Series ${d.series})</small></strong>
351
+ <div class="tooltip-row"><span>Total Score</span><span>${d.score} / 1500 (${d.pct}%)</span></div>
352
+ <div class="tooltip-row">
353
+ <span>Total SR</span>
354
+ <span style="color:#a78bfa">${d.total}%
355
+ <span class="tooltip-ci">[${d.total_ci.lo.toFixed(0)}–${d.total_ci.hi.toFixed(0)}%]</span>
356
+ </span>
357
+ </div>
358
+ <div class="tooltip-row">
359
+ <span>Level 1 SR</span>
360
+ <span style="color:#4dc98a">${d.l1}%
361
+ <span class="tooltip-ci">[${d.l1_ci.lo.toFixed(0)}–${d.l1_ci.hi.toFixed(0)}%]</span>
362
+ </span>
363
+ </div>
364
+ <div class="tooltip-row">
365
+ <span>Level 2 SR</span>
366
+ <span style="color:#f87171">${d.l2}%
367
+ <span class="tooltip-ci">[${d.l2_ci.lo.toFixed(0)}–${d.l2_ci.hi.toFixed(0)}%]</span>
368
+ </span>
369
+ </div>
370
+ <div style="margin-top:5px;font-size:10px;color:#555">
371
+ CLD group (${lvLabels[activeLevel]}): <strong style="color:#a78bfa">${cldLetter}</strong> · shared letter = not significantly different
372
+ </div>
373
+ ${(EXPERIMENTS[d.label]||{}).note ? `<div class="tooltip-note">${EXPERIMENTS[d.label].note}</div>` : ""}
374
+ `);
375
+
376
+ const bx = container.getBoundingClientRect();
377
+ const ex = event.clientX - bx.left, ey = event.clientY - bx.top;
378
+ tooltip.style("left", Math.min(ex + 14, container.clientWidth - 270) + "px")
379
+ .style("top", Math.max(ey - 140, 0) + "px");
380
+ }
381
+
382
+ render();
383
+ window.addEventListener("resize", render);
384
+
385
+ }
386
+
387
+ if (typeof d3 !== "undefined") {
388
+ _initExperimentResults();
389
+ } else {
390
+ var s = document.createElement("script");
391
+ s.src = "https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js";
392
+ s.onload = _initExperimentResults;
393
+ document.head.appendChild(s);
394
+ }
395
+ </script>
396
+ </body>
397
+ </html>
app/src/content/embeds/folding/failure-analysis.html CHANGED
@@ -4,12 +4,23 @@
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
  <style>
 
 
 
 
 
 
 
 
 
 
 
7
  * { box-sizing: border-box; margin: 0; padding: 0; }
8
 
9
  body {
10
- background: transparent;
11
  font-family: system-ui, sans-serif;
12
- color: #e8eaf0;
13
  padding: 0;
14
  min-height: 100vh;
15
  }
@@ -24,7 +35,7 @@
24
  display: flex;
25
  gap: 4px;
26
  margin-bottom: 20px;
27
- border-bottom: 1px solid #2a2d3a;
28
  padding-bottom: 0;
29
  }
30
 
@@ -33,10 +44,10 @@
33
  border: 1px solid transparent;
34
  border-bottom: none;
35
  background: none;
36
- color: #8b8fa8;
37
  cursor: pointer;
38
  font-family: inherit;
39
- font-size: 11px;
40
  letter-spacing: 0.05em;
41
  text-transform: uppercase;
42
  border-radius: 4px 4px 0 0;
@@ -44,20 +55,20 @@
44
  position: relative;
45
  bottom: -1px;
46
  }
47
- .tab:hover { color: #e8eaf0; background: #1a1d27; }
48
  .tab.active {
49
- color: #e8eaf0;
50
- background: #1a1d27;
51
- border-color: #2a2d3a;
52
- border-bottom-color: #1a1d27;
53
  }
54
 
55
  .panel { display: none; }
56
  .panel.active { display: block; }
57
 
58
  .chart-wrap {
59
- background: #1a1d27;
60
- border: 1px solid #2a2d3a;
61
  border-radius: 6px;
62
  padding: 20px 20px 12px;
63
  }
@@ -72,10 +83,10 @@
72
  }
73
 
74
  .chart-title {
75
- font-size: 11px;
76
  text-transform: uppercase;
77
  letter-spacing: 0.08em;
78
- color: #8b8fa8;
79
  }
80
 
81
  .mode-toggle {
@@ -83,20 +94,20 @@
83
  gap: 0;
84
  }
85
  .mode-btn {
86
- padding: 4px 12px;
87
- font-size: 10px;
88
  font-family: inherit;
89
  cursor: pointer;
90
- border: 1px solid #2a2d3a;
91
  background: none;
92
- color: #8b8fa8;
93
  transition: all 0.15s;
94
  letter-spacing: 0.04em;
95
  }
96
  .mode-btn:first-child { border-radius: 4px 0 0 4px; }
97
  .mode-btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
98
- .mode-btn.active { background: #252835; color: #e8eaf0; border-color: #4a4d5a; }
99
- .mode-btn:hover:not(.active) { color: #e8eaf0; }
100
 
101
  .legend {
102
  display: flex;
@@ -104,14 +115,14 @@
104
  gap: 10px 18px;
105
  margin-top: 14px;
106
  padding-top: 12px;
107
- border-top: 1px solid #2a2d3a;
108
  }
109
  .legend-item {
110
  display: flex;
111
  align-items: center;
112
  gap: 6px;
113
- font-size: 10px;
114
- color: #8b8fa8;
115
  letter-spacing: 0.03em;
116
  }
117
  .legend-swatch {
@@ -130,10 +141,10 @@
130
  display: flex;
131
  align-items: center;
132
  gap: 6px;
133
- font-size: 10px;
134
  letter-spacing: 0.05em;
135
  text-transform: uppercase;
136
- color: #8b8fa8;
137
  }
138
  .series-badge span {
139
  width: 24px;
@@ -145,58 +156,54 @@
145
  svg text { font-family: system-ui, sans-serif; }
146
 
147
  .note {
148
- font-size: 10px;
149
- color: #555e7a;
150
  margin-top: 10px;
151
  line-height: 1.6;
152
  letter-spacing: 0.02em;
153
  }
154
 
155
- @media (prefers-color-scheme: dark) {
156
- .note { color: #c8cad8; }
157
- }
158
-
159
  .insight-box {
160
- background: rgba(128,128,128,0.06);
161
- border: 1px solid rgba(128,128,128,0.15);
162
- border-left: 3px solid rgba(128,128,128,0.3);
163
  border-radius: 4px;
164
  padding: 10px 14px;
165
  margin-bottom: 16px;
166
- font-size: 10px;
167
- color: #555;
168
  line-height: 1.6;
169
  }
170
- .insight-box strong { color: #111; }
171
-
172
- @media (prefers-color-scheme: dark) {
173
- .insight-box {
174
- background: #12151f;
175
- border: 1px solid #2a2d3a;
176
- border-left: 3px solid #f7934f;
177
- color: #8b8fa8;
178
- }
179
- .insight-box strong { color: #e8eaf0; }
180
  }
 
 
181
  </style>
182
  </head>
183
  <body>
184
  <div class="container">
185
 
186
- <div class="tab-row">
187
- <button class="tab active" onclick="showTab('l2')">Level 2 — Failure point</button>
188
- <button class="tab" onclick="showTab('l1')">Level 1 — Failure point</button>
 
 
 
189
  </div>
190
 
191
  <!-- LEVEL 2 PANEL -->
192
- <div class="panel active" id="panel-l2">
193
  <div class="insight-box">
194
- <strong>Series 1:</strong> nearly all level 2 failures occur at Unfold — the robot never gets past step 1.&nbsp;
195
- <strong>Series 2:</strong> Unfold failures collapse (2.5: 0%), but late-stage failures (Fold 3, Rotation) emerge — the model now reliably unfolds but precision degrades at the end.
196
  </div>
197
  <div class="chart-wrap">
198
  <div class="chart-header">
199
- <div class="chart-title">Where does the robot fail? — Level 2 failed rollouts by subtask</div>
200
  <div class="mode-toggle">
201
  <button class="mode-btn active" id="mode-l2-abs" onclick="setMode('l2','abs')">Counts</button>
202
  <button class="mode-btn" id="mode-l2-pct" onclick="setMode('l2','pct')">Percentage</button>
@@ -205,17 +212,17 @@
205
  <svg id="chart-l2" width="100%" height="320" style="overflow:visible"></svg>
206
  <div class="legend" id="legend-l2"></div>
207
  </div>
208
- <p class="note">Each bar = one experiment, showing how its failed Level 2 rollouts distribute across subtasks. Only failed rollouts shown — successful rollouts are excluded. Toggle "Percentage" to compare failure distributions regardless of total failure count.</p>
209
  </div>
210
 
211
  <!-- LEVEL 1 PANEL -->
212
- <div class="panel" id="panel-l1">
213
  <div class="insight-box">
214
- <strong>Level 1 failures</strong> are more distributed since unfolding is given. Series 1 failures concentrate at Fold 2 and Fold 4 (mid-task precision). Series 2 nearly eliminates failures entirely — only 2.3 (mirroring) and 2.4 (chunk=45) regress significantly.
215
  </div>
216
  <div class="chart-wrap">
217
  <div class="chart-header">
218
- <div class="chart-title">Where does the robot fail? — Level 1 failed rollouts by subtask</div>
219
  <div class="mode-toggle">
220
  <button class="mode-btn active" id="mode-l1-abs" onclick="setMode('l1','abs')">Counts</button>
221
  <button class="mode-btn" id="mode-l1-pct" onclick="setMode('l1','pct')">Percentage</button>
@@ -285,6 +292,23 @@ const COLORS = {
285
  'Rotation': '#818cf8',
286
  };
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  const modes = { l2: 'abs', l1: 'abs' };
289
 
290
  function setMode(level, mode) {
@@ -296,7 +320,15 @@ function setMode(level, mode) {
296
  renderTab(level);
297
  }
298
 
 
 
 
 
299
  function buildStackedBar(svgId, legendId, data, subtasks, experiments, normalize) {
 
 
 
 
300
  const svgEl = document.getElementById(svgId);
301
  const W = svgEl.parentElement.clientWidth - 40;
302
  const H = 340;
@@ -349,7 +381,7 @@ function buildStackedBar(svgId, legendId, data, subtasks, experiments, normalize
349
  .call(d3.axisLeft(y).tickSize(-innerW).tickFormat('').ticks(5))
350
  .call(gg => {
351
  gg.select('.domain').remove();
352
- gg.selectAll('line').attr('stroke', '#2a2d3a').attr('stroke-dasharray', '3,3');
353
  });
354
 
355
  // Stacked bars
@@ -370,8 +402,8 @@ function buildStackedBar(svgId, legendId, data, subtasks, experiments, normalize
370
  .attr('x', d => x(d.id) + x.bandwidth() / 2)
371
  .attr('y', d => d._total === 0 ? y(0) - 4 : y(d._displayTotal) - 5)
372
  .attr('text-anchor', 'middle')
373
- .attr('fill', d => d._total === 0 ? '#3a3d4a' : '#8b8fa8')
374
- .attr('font-size', '9')
375
  .text(d => {
376
  if (d._total === 0) return '✓ 0 failures';
377
  return normalize ? `n=${d._total}` : d._total;
@@ -385,14 +417,14 @@ function buildStackedBar(svgId, legendId, data, subtasks, experiments, normalize
385
  g.append('line')
386
  .attr('x1', xDiv).attr('x2', xDiv)
387
  .attr('y1', -22).attr('y2', innerH + 4)
388
- .attr('stroke', '#3a3d4a').attr('stroke-width', 1).attr('stroke-dasharray', '4,3');
389
 
390
  g.append('text').attr('x', xDiv - 6).attr('y', -18).attr('text-anchor', 'end')
391
- .attr('fill', '#f7934f').attr('font-size', '8').attr('letter-spacing', '0.06em').text('SERIES 1');
392
 
393
  if (s2First) {
394
  g.append('text').attr('x', xDiv + 6).attr('y', -18).attr('text-anchor', 'start')
395
- .attr('fill', '#4dc98a').attr('font-size', '8').attr('letter-spacing', '0.06em').text('SERIES 2');
396
  }
397
  }
398
 
@@ -400,21 +432,21 @@ function buildStackedBar(svgId, legendId, data, subtasks, experiments, normalize
400
  g.append('g')
401
  .call(d3.axisLeft(y).ticks(5).tickSize(4).tickFormat(d => normalize ? d + '%' : d))
402
  .call(gg => {
403
- gg.select('.domain').attr('stroke', '#2a2d3a');
404
- gg.selectAll('text').attr('fill', '#8b8fa8').attr('font-size', '9');
405
- gg.selectAll('line').attr('stroke', '#2a2d3a');
406
  });
407
 
408
  g.append('g').attr('transform', `translate(0,${innerH})`)
409
  .call(d3.axisBottom(x).tickSize(0))
410
  .call(gg => {
411
- gg.select('.domain').attr('stroke', '#2a2d3a');
412
  gg.selectAll('text')
413
  .attr('fill', d => {
414
  const a = experiments.find(a => a.id === d);
415
  return a?.series === 2 ? '#4dc98a' : '#f7934f';
416
  })
417
- .attr('font-size', '9')
418
  .attr('transform', 'rotate(-40)')
419
  .attr('text-anchor', 'end')
420
  .attr('dx', '-0.5em')
@@ -424,7 +456,7 @@ function buildStackedBar(svgId, legendId, data, subtasks, experiments, normalize
424
  // Y axis label
425
  svg.append('text').attr('transform', 'rotate(-90)')
426
  .attr('x', -(margin.top + innerH / 2)).attr('y', 10).attr('text-anchor', 'middle')
427
- .attr('fill', '#555e7a').attr('font-size', '9')
428
  .text(normalize ? 'Failure distribution (%)' : 'Failed rollouts (n)');
429
 
430
  // Legend
@@ -443,8 +475,9 @@ function renderTab(id) {
443
  if (rendered[id]) return;
444
  rendered[id] = true;
445
  const normalize = modes[id] === 'pct';
446
- if (id === 'l2') buildStackedBar('chart-l2', 'legend-l2', L2_FAILURES, SUBTASKS_L2, EXPERIMENTS, normalize);
447
- if (id === 'l1') buildStackedBar('chart-l1', 'legend-l1', L1_FAILURES, SUBTASKS_L1, EXPERIMENTS, normalize);
 
448
  }
449
 
450
  function showTab(id) {
@@ -460,7 +493,7 @@ function showTab(id) {
460
  window.showTab = showTab;
461
  window.setMode = setMode;
462
 
463
- renderTab('l2');
464
 
465
  }
466
 
 
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
  <style>
7
+ :root {
8
+ --bg: transparent;
9
+ --text: #e8eaf0;
10
+ --subtext: #8b8fa8;
11
+ --border: #2a2d3a;
12
+ --chart-bg: #1a1d27;
13
+ --btn-active-bg: #252835;
14
+ --btn-active-text: #e8eaf0;
15
+ --btn-active-border: #4a4d5a;
16
+ }
17
+
18
  * { box-sizing: border-box; margin: 0; padding: 0; }
19
 
20
  body {
21
+ background: var(--bg);
22
  font-family: system-ui, sans-serif;
23
+ color: var(--text);
24
  padding: 0;
25
  min-height: 100vh;
26
  }
 
35
  display: flex;
36
  gap: 4px;
37
  margin-bottom: 20px;
38
+ border-bottom: 1px solid var(--border);
39
  padding-bottom: 0;
40
  }
41
 
 
44
  border: 1px solid transparent;
45
  border-bottom: none;
46
  background: none;
47
+ color: var(--subtext);
48
  cursor: pointer;
49
  font-family: inherit;
50
+ font-size: 13px;
51
  letter-spacing: 0.05em;
52
  text-transform: uppercase;
53
  border-radius: 4px 4px 0 0;
 
55
  position: relative;
56
  bottom: -1px;
57
  }
58
+ .tab:hover { color: var(--text); background: var(--chart-bg); }
59
  .tab.active {
60
+ color: var(--text);
61
+ background: var(--chart-bg);
62
+ border-color: var(--border);
63
+ border-bottom-color: var(--chart-bg);
64
  }
65
 
66
  .panel { display: none; }
67
  .panel.active { display: block; }
68
 
69
  .chart-wrap {
70
+ background: var(--chart-bg);
71
+ border: 1px solid var(--border);
72
  border-radius: 6px;
73
  padding: 20px 20px 12px;
74
  }
 
83
  }
84
 
85
  .chart-title {
86
+ font-size: 13px;
87
  text-transform: uppercase;
88
  letter-spacing: 0.08em;
89
+ color: var(--subtext);
90
  }
91
 
92
  .mode-toggle {
 
94
  gap: 0;
95
  }
96
  .mode-btn {
97
+ padding: 5px 14px;
98
+ font-size: 12px;
99
  font-family: inherit;
100
  cursor: pointer;
101
+ border: 1px solid var(--border);
102
  background: none;
103
+ color: var(--subtext);
104
  transition: all 0.15s;
105
  letter-spacing: 0.04em;
106
  }
107
  .mode-btn:first-child { border-radius: 4px 0 0 4px; }
108
  .mode-btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
109
+ .mode-btn.active { background: var(--btn-active-bg); color: var(--btn-active-text); border-color: var(--btn-active-border); }
110
+ .mode-btn:hover:not(.active) { color: var(--text); }
111
 
112
  .legend {
113
  display: flex;
 
115
  gap: 10px 18px;
116
  margin-top: 14px;
117
  padding-top: 12px;
118
+ border-top: 1px solid var(--border);
119
  }
120
  .legend-item {
121
  display: flex;
122
  align-items: center;
123
  gap: 6px;
124
+ font-size: 12px;
125
+ color: var(--subtext);
126
  letter-spacing: 0.03em;
127
  }
128
  .legend-swatch {
 
141
  display: flex;
142
  align-items: center;
143
  gap: 6px;
144
+ font-size: 12px;
145
  letter-spacing: 0.05em;
146
  text-transform: uppercase;
147
+ color: var(--subtext);
148
  }
149
  .series-badge span {
150
  width: 24px;
 
156
  svg text { font-family: system-ui, sans-serif; }
157
 
158
  .note {
159
+ font-size: 12px;
160
+ color: var(--subtext);
161
  margin-top: 10px;
162
  line-height: 1.6;
163
  letter-spacing: 0.02em;
164
  }
165
 
 
 
 
 
166
  .insight-box {
167
+ background: #12151f;
168
+ border: 1px solid #2a2d3a;
169
+ border-left: 3px solid #f7934f;
170
  border-radius: 4px;
171
  padding: 10px 14px;
172
  margin-bottom: 16px;
173
+ font-size: 12px;
174
+ color: #8b8fa8;
175
  line-height: 1.6;
176
  }
177
+ .insight-box strong { color: #e8eaf0; }
178
+ .filter-toggle {
179
+ font-size: 12px; color: var(--subtext); cursor: pointer; border: 1px solid var(--border);
180
+ background: none; padding: 4px 12px; border-radius: 14px; transition: all .15s;
181
+ user-select: none; display: inline-flex; align-items: center; gap: 4px; font-family: inherit;
 
 
 
 
 
182
  }
183
+ .filter-toggle:hover { color: var(--text); }
184
+ .filter-toggle.showing-all { background: rgba(248,147,79,0.12); border-color: #f7934f; color: #f7934f; }
185
  </style>
186
  </head>
187
  <body>
188
  <div class="container">
189
 
190
+ <div class="tab-row" style="justify-content:space-between;align-items:center">
191
+ <div style="display:flex;gap:4px">
192
+ <button class="tab active" onclick="showTab('l1')">Level 1: Failure point</button>
193
+ <button class="tab" onclick="showTab('l2')">Level 2: Failure point</button>
194
+ </div>
195
+ <button class="filter-toggle" id="fa-filter-btn" onclick="window._faToggleFilter()">Show all 11</button>
196
  </div>
197
 
198
  <!-- LEVEL 2 PANEL -->
199
+ <div class="panel" id="panel-l2">
200
  <div class="insight-box">
201
+ <strong>Initial training (1.x):</strong> nearly all level 2 failures occur at Unfold (the robot never gets past step 1).&nbsp;
202
+ <strong>Fine-tuned (2.x):</strong> Unfold failures collapse (2.5: 0%), but late-stage failures (Fold 3, Rotation) emerge: the model now reliably unfolds but precision degrades at the end.
203
  </div>
204
  <div class="chart-wrap">
205
  <div class="chart-header">
206
+ <div class="chart-title">Where does the robot fail? Level 2 failed rollouts by subtask</div>
207
  <div class="mode-toggle">
208
  <button class="mode-btn active" id="mode-l2-abs" onclick="setMode('l2','abs')">Counts</button>
209
  <button class="mode-btn" id="mode-l2-pct" onclick="setMode('l2','pct')">Percentage</button>
 
212
  <svg id="chart-l2" width="100%" height="320" style="overflow:visible"></svg>
213
  <div class="legend" id="legend-l2"></div>
214
  </div>
215
+ <p class="note">Each bar = one experiment, showing how its failed Level 2 rollouts distribute across subtasks. Only failed rollouts shown; successful rollouts are excluded. Toggle "Percentage" to compare failure distributions regardless of total failure count.</p>
216
  </div>
217
 
218
  <!-- LEVEL 1 PANEL -->
219
+ <div class="panel active" id="panel-l1">
220
  <div class="insight-box">
221
+ <strong>Level 1 failures</strong> are more distributed since unfolding is given. Initial-training failures concentrate at Fold 2 and Fold 4 (mid-task precision). Fine-tuning nearly eliminates failures entirely; only 2.3 (mirroring) and 2.4 (chunk=45) regress significantly.
222
  </div>
223
  <div class="chart-wrap">
224
  <div class="chart-header">
225
+ <div class="chart-title">Where does the robot fail? Level 1 failed rollouts by subtask</div>
226
  <div class="mode-toggle">
227
  <button class="mode-btn active" id="mode-l1-abs" onclick="setMode('l1','abs')">Counts</button>
228
  <button class="mode-btn" id="mode-l1-pct" onclick="setMode('l1','pct')">Percentage</button>
 
292
  'Rotation': '#818cf8',
293
  };
294
 
295
+ const HIDDEN_BY_DEFAULT = new Set(['1.4 RABC low','1.5 RABC high','2.3 HQ+mirror','2.4 HQ chunk45']);
296
+ let faShowAll = false;
297
+
298
+ window._faToggleFilter = function() {
299
+ faShowAll = !faShowAll;
300
+ const btn = document.getElementById("fa-filter-btn");
301
+ btn.textContent = faShowAll ? "Key experiments" : "Show all 11";
302
+ btn.classList.toggle("showing-all", faShowAll);
303
+ rendered.l1 = false;
304
+ rendered.l2 = false;
305
+ renderTab(document.querySelector('.panel.active').id.replace('panel-',''));
306
+ };
307
+
308
+ function getVisibleExperiments() {
309
+ return faShowAll ? EXPERIMENTS : EXPERIMENTS.filter(e => !HIDDEN_BY_DEFAULT.has(e.id));
310
+ }
311
+
312
  const modes = { l2: 'abs', l1: 'abs' };
313
 
314
  function setMode(level, mode) {
 
320
  renderTab(level);
321
  }
322
 
323
+ function cssVar(name) {
324
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
325
+ }
326
+
327
  function buildStackedBar(svgId, legendId, data, subtasks, experiments, normalize) {
328
+ const textColor = cssVar('--text');
329
+ const subtextColor = cssVar('--subtext');
330
+ const borderColor = cssVar('--border');
331
+
332
  const svgEl = document.getElementById(svgId);
333
  const W = svgEl.parentElement.clientWidth - 40;
334
  const H = 340;
 
381
  .call(d3.axisLeft(y).tickSize(-innerW).tickFormat('').ticks(5))
382
  .call(gg => {
383
  gg.select('.domain').remove();
384
+ gg.selectAll('line').attr('stroke', borderColor).attr('stroke-dasharray', '3,3');
385
  });
386
 
387
  // Stacked bars
 
402
  .attr('x', d => x(d.id) + x.bandwidth() / 2)
403
  .attr('y', d => d._total === 0 ? y(0) - 4 : y(d._displayTotal) - 5)
404
  .attr('text-anchor', 'middle')
405
+ .attr('fill', d => d._total === 0 ? borderColor : subtextColor)
406
+ .attr('font-size', '11')
407
  .text(d => {
408
  if (d._total === 0) return '✓ 0 failures';
409
  return normalize ? `n=${d._total}` : d._total;
 
417
  g.append('line')
418
  .attr('x1', xDiv).attr('x2', xDiv)
419
  .attr('y1', -22).attr('y2', innerH + 4)
420
+ .attr('stroke', borderColor).attr('stroke-width', 1).attr('stroke-dasharray', '4,3');
421
 
422
  g.append('text').attr('x', xDiv - 6).attr('y', -18).attr('text-anchor', 'end')
423
+ .attr('fill', '#f7934f').attr('font-size', '10').attr('letter-spacing', '0.06em').text('SERIES 1');
424
 
425
  if (s2First) {
426
  g.append('text').attr('x', xDiv + 6).attr('y', -18).attr('text-anchor', 'start')
427
+ .attr('fill', '#4dc98a').attr('font-size', '10').attr('letter-spacing', '0.06em').text('SERIES 2');
428
  }
429
  }
430
 
 
432
  g.append('g')
433
  .call(d3.axisLeft(y).ticks(5).tickSize(4).tickFormat(d => normalize ? d + '%' : d))
434
  .call(gg => {
435
+ gg.select('.domain').attr('stroke', borderColor);
436
+ gg.selectAll('text').attr('fill', subtextColor).attr('font-size', '11');
437
+ gg.selectAll('line').attr('stroke', borderColor);
438
  });
439
 
440
  g.append('g').attr('transform', `translate(0,${innerH})`)
441
  .call(d3.axisBottom(x).tickSize(0))
442
  .call(gg => {
443
+ gg.select('.domain').attr('stroke', borderColor);
444
  gg.selectAll('text')
445
  .attr('fill', d => {
446
  const a = experiments.find(a => a.id === d);
447
  return a?.series === 2 ? '#4dc98a' : '#f7934f';
448
  })
449
+ .attr('font-size', '11')
450
  .attr('transform', 'rotate(-40)')
451
  .attr('text-anchor', 'end')
452
  .attr('dx', '-0.5em')
 
456
  // Y axis label
457
  svg.append('text').attr('transform', 'rotate(-90)')
458
  .attr('x', -(margin.top + innerH / 2)).attr('y', 10).attr('text-anchor', 'middle')
459
+ .attr('fill', subtextColor).attr('font-size', '11')
460
  .text(normalize ? 'Failure distribution (%)' : 'Failed rollouts (n)');
461
 
462
  // Legend
 
475
  if (rendered[id]) return;
476
  rendered[id] = true;
477
  const normalize = modes[id] === 'pct';
478
+ const visExps = getVisibleExperiments();
479
+ if (id === 'l2') buildStackedBar('chart-l2', 'legend-l2', L2_FAILURES, SUBTASKS_L2, visExps, normalize);
480
+ if (id === 'l1') buildStackedBar('chart-l1', 'legend-l1', L1_FAILURES, SUBTASKS_L1, visExps, normalize);
481
  }
482
 
483
  function showTab(id) {
 
493
  window.showTab = showTab;
494
  window.setMode = setMode;
495
 
496
+ renderTab('l1');
497
 
498
  }
499
 
app/src/content/embeds/folding/l1-time-quality.html CHANGED
@@ -33,8 +33,8 @@
33
  </head>
34
  <body>
35
  <div class="legend">
36
- <div class="legend-item"><div class="legend-pip" style="background:#f7934f"></div>Series 2</div>
37
- <div class="legend-item"><div class="legend-pip" style="background:#4f8ef7"></div>Series 1</div>
38
  <div class="legend-item" style="margin-left:8px; font-size:10px; color:#555">
39
  Bubble size = Total SR
40
  </div>
 
33
  </head>
34
  <body>
35
  <div class="legend">
36
+ <div class="legend-item"><div class="legend-pip" style="background:#f7934f"></div>Fine-tuned (2.x)</div>
37
+ <div class="legend-item"><div class="legend-pip" style="background:#4f8ef7"></div>Initial (1.x)</div>
38
  <div class="legend-item" style="margin-left:8px; font-size:10px; color:#555">
39
  Bubble size = Total SR
40
  </div>
app/src/content/embeds/folding/loss-curves.html CHANGED
@@ -13,13 +13,13 @@
13
  * { box-sizing: border-box; margin: 0; padding: 0; }
14
  body { background: var(--bg); font-family: system-ui, -apple-system, sans-serif; color: var(--text); }
15
  #chart-container { position: relative; width: 100%; }
16
- .axis text { fill: #8b8fa8; font-size: 11px; }
17
  .axis path, .axis line { stroke: rgba(255,255,255,0.15); }
18
  .grid line { stroke: var(--grid); }
19
  .grid path { stroke: none; }
20
  .legend-wrap {
21
  display: flex; flex-wrap: wrap; gap: 6px 14px; justify-content: center;
22
- padding: 8px 0 0; font-size: 12px;
23
  }
24
  .legend-item {
25
  display: flex; align-items: center; gap: 5px; cursor: pointer;
@@ -33,7 +33,7 @@
33
  .tooltip-loss {
34
  position: absolute; background: #13152aee; border: 1px solid #333; border-radius: 8px;
35
  padding: 10px 14px; pointer-events: none; opacity: 0; transition: opacity .15s;
36
- z-index: 20; font-size: 12px; line-height: 1.6; box-shadow: 0 6px 24px rgba(0,0,0,.5);
37
  max-width: 280px;
38
  }
39
  .tooltip-loss strong { color: #e8eaf0; }
@@ -41,7 +41,7 @@
41
  .tooltip-loss .tt-swatch { width: 10px; height: 3px; border-radius: 1px; flex-shrink: 0; }
42
  .series-toggle { display: flex; gap: 0; justify-content: center; margin-bottom: 6px; }
43
  .series-btn {
44
- padding: 6px 18px; font-size: 12px; font-weight: 600; cursor: pointer; border: none;
45
  background: #1a1d2e; color: #8b8fa8; transition: all .2s;
46
  font-family: system-ui, sans-serif; border-bottom: 2px solid transparent;
47
  }
@@ -54,8 +54,8 @@
54
  <body>
55
  <div class="series-toggle">
56
  <button class="series-btn active" data-filter="all">All Runs</button>
57
- <button class="series-btn" data-filter="s1">Series 1</button>
58
- <button class="series-btn" data-filter="s2">Series 2</button>
59
  </div>
60
  <div id="chart-container">
61
  <svg id="loss-svg"></svg>
@@ -68,8 +68,8 @@ const RUNS = [
68
  { key: "ablation1_1_2", label: "1.1 π0", series: "s1", color: "#ef5350" },
69
  { key: "ablation1_2_2", label: "1.2 π0.5", series: "s1", color: "#f59e0b" },
70
  { key: "ablation1_3_17_q", label: "1.3 Relative", series: "s1", color: "#10b981" },
71
- { key: "ablation1-4", label: "1.4 RABC low", series: "s1", color: "#3b82f6" },
72
- { key: "ablation1-5_9", label: "1.5 RABC high", series: "s1", color: "#8b5cf6" },
73
  { key: "ablation1-7_2", label: "1.7 Rel+RABC", series: "s1", color: "#ec4899" },
74
  { key: "ablation2-1_100k_q", label: "2.1 HQ", series: "s2", color: "#14b8a6" },
75
  { key: "ablation2-2_100k", label: "2.2 HQ+RABC+Rel", series: "s2", color: "#6366f1" },
@@ -123,9 +123,23 @@ const svgEl = d3.select("#loss-svg");
123
  const tt = document.getElementById("tt");
124
  const legendEl = document.getElementById("legend");
125
 
126
- let activeFilter = "all";
 
 
 
127
  const hidden = new Set();
128
 
 
 
 
 
 
 
 
 
 
 
 
129
  function render() {
130
  svgEl.selectAll("*").remove();
131
  const W = container.clientWidth || 800;
@@ -166,9 +180,9 @@ function render() {
166
 
167
  // Axis labels
168
  g.append("text").attr("x", w / 2).attr("y", h + 32).attr("text-anchor", "middle")
169
- .attr("fill", "#8b8fa8").attr("font-size", 11).text("Training Steps");
170
  g.append("text").attr("x", -h / 2).attr("y", -40).attr("text-anchor", "middle")
171
- .attr("transform", "rotate(-90)").attr("fill", "#8b8fa8").attr("font-size", 11).text("Loss");
172
 
173
  // Lines
174
  const line = d3.line().x(d => x(d.step)).y(d => y(d.loss)).curve(d3.curveMonotoneX);
 
13
  * { box-sizing: border-box; margin: 0; padding: 0; }
14
  body { background: var(--bg); font-family: system-ui, -apple-system, sans-serif; color: var(--text); }
15
  #chart-container { position: relative; width: 100%; }
16
+ .axis text { fill: #8b8fa8; font-size: 13px; }
17
  .axis path, .axis line { stroke: rgba(255,255,255,0.15); }
18
  .grid line { stroke: var(--grid); }
19
  .grid path { stroke: none; }
20
  .legend-wrap {
21
  display: flex; flex-wrap: wrap; gap: 6px 14px; justify-content: center;
22
+ padding: 8px 0 0; font-size: 13px;
23
  }
24
  .legend-item {
25
  display: flex; align-items: center; gap: 5px; cursor: pointer;
 
33
  .tooltip-loss {
34
  position: absolute; background: #13152aee; border: 1px solid #333; border-radius: 8px;
35
  padding: 10px 14px; pointer-events: none; opacity: 0; transition: opacity .15s;
36
+ z-index: 20; font-size: 13px; line-height: 1.6; box-shadow: 0 6px 24px rgba(0,0,0,.5);
37
  max-width: 280px;
38
  }
39
  .tooltip-loss strong { color: #e8eaf0; }
 
41
  .tooltip-loss .tt-swatch { width: 10px; height: 3px; border-radius: 1px; flex-shrink: 0; }
42
  .series-toggle { display: flex; gap: 0; justify-content: center; margin-bottom: 6px; }
43
  .series-btn {
44
+ padding: 7px 20px; font-size: 14px; font-weight: 600; cursor: pointer; border: none;
45
  background: #1a1d2e; color: #8b8fa8; transition: all .2s;
46
  font-family: system-ui, sans-serif; border-bottom: 2px solid transparent;
47
  }
 
54
  <body>
55
  <div class="series-toggle">
56
  <button class="series-btn active" data-filter="all">All Runs</button>
57
+ <button class="series-btn" data-filter="s1">Initial (1.x)</button>
58
+ <button class="series-btn" data-filter="s2">Fine-tuned (2.x)</button>
59
  </div>
60
  <div id="chart-container">
61
  <svg id="loss-svg"></svg>
 
68
  { key: "ablation1_1_2", label: "1.1 π0", series: "s1", color: "#ef5350" },
69
  { key: "ablation1_2_2", label: "1.2 π0.5", series: "s1", color: "#f59e0b" },
70
  { key: "ablation1_3_17_q", label: "1.3 Relative", series: "s1", color: "#10b981" },
71
+ { key: "ablation1-4", label: "1.4 RABC low", series: "s1", color: "#3b82f6" },
72
+ { key: "ablation1-5_9", label: "1.5 RABC high", series: "s1", color: "#8b5cf6" },
73
  { key: "ablation1-7_2", label: "1.7 Rel+RABC", series: "s1", color: "#ec4899" },
74
  { key: "ablation2-1_100k_q", label: "2.1 HQ", series: "s2", color: "#14b8a6" },
75
  { key: "ablation2-2_100k", label: "2.2 HQ+RABC+Rel", series: "s2", color: "#6366f1" },
 
123
  const tt = document.getElementById("tt");
124
  const legendEl = document.getElementById("legend");
125
 
126
+ const configEl = container.closest("[data-config]");
127
+ const chartConfig = configEl ? JSON.parse(configEl.dataset.config || "{}") : {};
128
+
129
+ let activeFilter = chartConfig.initialFilter || "all";
130
  const hidden = new Set();
131
 
132
+ if (chartConfig.initialRuns) {
133
+ RUNS.forEach(r => { if (!chartConfig.initialRuns.includes(r.key)) hidden.add(r.key); });
134
+ }
135
+ if (chartConfig.hideToggle) {
136
+ const toggle = container.closest("body")?.querySelector(".series-toggle");
137
+ if (toggle) toggle.style.display = "none";
138
+ }
139
+ document.querySelectorAll(".series-btn").forEach(b => {
140
+ b.classList.toggle("active", b.dataset.filter === activeFilter);
141
+ });
142
+
143
  function render() {
144
  svgEl.selectAll("*").remove();
145
  const W = container.clientWidth || 800;
 
180
 
181
  // Axis labels
182
  g.append("text").attr("x", w / 2).attr("y", h + 32).attr("text-anchor", "middle")
183
+ .attr("fill", "#8b8fa8").attr("font-size", 13).text("Training Steps");
184
  g.append("text").attr("x", -h / 2).attr("y", -40).attr("text-anchor", "middle")
185
+ .attr("transform", "rotate(-90)").attr("fill", "#8b8fa8").attr("font-size", 13).text("Loss");
186
 
187
  // Lines
188
  const line = d3.line().x(d => x(d.step)).y(d => y(d.loss)).curve(d3.curveMonotoneX);
app/src/content/embeds/folding/pi05-architecture.html CHANGED
@@ -82,8 +82,8 @@
82
  // Responsive scaling
83
  const scale = Math.min(1, W / 800);
84
  const fontSize = Math.round(12 * scale);
85
- const smallFont = Math.round(10 * scale);
86
- const titleFont = Math.round(13 * scale);
87
 
88
  // ---- SECTION POSITIONS (top to bottom) ----
89
  const inputY = pad;
@@ -230,7 +230,7 @@
230
  drawArrow(g, W / 2, vlaY + vlaH, W / 2, actionY);
231
 
232
  // ========== ACTION EXPERT (FLOW MATCHING) ==========
233
- drawSection(g, pad, actionY, inputW, actionH, actionBg, actionBorder, 'ACTION EXPERT — FLOW MATCHING (300M)', 'center');
234
 
235
  // Noise -> vector field -> actions visualization
236
  const flowY = actionY + 28 * scale;
 
82
  // Responsive scaling
83
  const scale = Math.min(1, W / 800);
84
  const fontSize = Math.round(12 * scale);
85
+ const smallFont = Math.round(12 * scale);
86
+ const titleFont = Math.round(15 * scale);
87
 
88
  // ---- SECTION POSITIONS (top to bottom) ----
89
  const inputY = pad;
 
230
  drawArrow(g, W / 2, vlaY + vlaH, W / 2, actionY);
231
 
232
  // ========== ACTION EXPERT (FLOW MATCHING) ==========
233
+ drawSection(g, pad, actionY, inputW, actionH, actionBg, actionBorder, 'ACTION EXPERT: FLOW MATCHING (300M)', 'center');
234
 
235
  // Noise -> vector field -> actions visualization
236
  const flowY = actionY + 28 * scale;
app/src/content/embeds/folding/pi05-sarm-architecture.html CHANGED
@@ -11,14 +11,14 @@
11
  .tooltip-arch {
12
  position: absolute; background: #13152a; border: 1px solid #333; border-radius: 10px;
13
  padding: 14px 18px; pointer-events: none; opacity: 0; transition: opacity .2s;
14
- z-index: 20; max-width: 320px; box-shadow: 0 8px 32px rgba(0,0,0,.6); font-size: 13px; line-height: 1.5;
15
  }
16
- .tooltip-arch strong { display: block; margin-bottom: 6px; font-size: 14px; color: #e8eaf0; }
17
- .tooltip-arch .tt-detail { color: #8b8fa8; font-size: 12px; }
18
- .tooltip-arch code { background: #252540; padding: 1px 5px; border-radius: 3px; font-size: 11px; color: #a78bfa; }
19
  .tab-bar { display: flex; gap: 0; justify-content: center; margin-bottom: 0; }
20
  .tab-btn {
21
- padding: 8px 20px; font-size: 13px; font-weight: 600; cursor: pointer; border: none;
22
  background: #1a1d2e; color: #8b8fa8; transition: all .2s; border-bottom: 2px solid transparent;
23
  font-family: system-ui, sans-serif;
24
  }
@@ -34,16 +34,32 @@
34
  <button class="tab-btn" data-tab="pi05">π0.5 Detail</button>
35
  <button class="tab-btn" data-tab="sarm">SARM Detail</button>
36
  </div>
37
- <div id="arch-container">
38
- <svg id="arch-svg"></svg>
39
- <div class="tooltip-arch" id="tt"></div>
40
  </div>
41
  <script>
42
  function _initArchViz() {
43
- const svg = d3.select("#arch-svg");
44
- const container = document.getElementById("arch-container");
45
- const tt = d3.select("#tt");
46
- let currentTab = "system";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  function isLight() {
49
  try {
@@ -75,6 +91,7 @@ const DARK = {
75
  scoreBox: "#1a1a2e", scoreText: "#e8eaf0",
76
  ttBg: "#13152a", ttBorder: "#333", ttText: "#e8eaf0", ttSub: "#8b8fa8", ttCode: "#252540", ttCodeText: "#a78bfa",
77
  tabBg: "#1a1d2e", tabText: "#8b8fa8", tabActiveBg: "#252540", tabActiveText: "#e8eaf0",
 
78
  };
79
 
80
  const LIGHT = {
@@ -99,13 +116,14 @@ const LIGHT = {
99
  scoreBox: "#f9fafb", scoreText: "#1a1a2e",
100
  ttBg: "#fff", ttBorder: "#e2e4e9", ttText: "#1a1a2e", ttSub: "#6b7280", ttCode: "#f3f4f6", ttCodeText: "#6d28d9",
101
  tabBg: "#f3f4f6", tabText: "#6b7280", tabActiveBg: "#fff", tabActiveText: "#1a1a2e",
 
102
  };
103
 
104
  function T() { return isLight() ? LIGHT : DARK; }
105
 
106
  function applyTabTheme() {
107
  var t = T();
108
- document.querySelectorAll(".tab-btn").forEach(function(btn) {
109
  if (btn.classList.contains("active")) {
110
  btn.style.background = t.tabActiveBg;
111
  btn.style.color = t.tabActiveText;
@@ -114,7 +132,7 @@ function applyTabTheme() {
114
  btn.style.color = t.tabText;
115
  }
116
  });
117
- var tip = document.getElementById("tt");
118
  if (tip) {
119
  tip.style.background = t.ttBg;
120
  tip.style.borderColor = t.ttBorder;
@@ -128,9 +146,9 @@ const tooltips = {
128
  siglip: { title: "SigLIP Vision Encoder (400M)", detail: "Converts camera images to patch embeddings. Frozen during fine-tuning for stability. Outputs visual tokens for the VLM backbone." },
129
  gemma_vlm: { title: "Gemma VLM Backbone (2B)", detail: "Pre-trained language model that processes visual tokens, language tokens, and proprioceptive state. Shares attention across all modalities." },
130
  subtask: { title: "Subtask Prediction", detail: "Auto-regressive text decoding produces a high-level subtask like <code>\"pick up left sleeve\"</code>. Acts as chain-of-thought for the action expert." },
131
- action_expert: { title: "Action Expert (300M)", detail: "Smaller Gemma model dedicated to flow matching. Denoises Gaussian noise into continuous 16D actions (30 steps at 30 Hz). Conditioned on VLM backbone output." },
132
  flow: { title: "Flow Matching", detail: "Generative approach that denoises Gaussian noise into continuous actions. Enables multi-modal action distributions for complex manipulation." },
133
- actions: { title: "Continuous Actions", detail: "30-step action chunk at 30 Hz: target joint angles + gripper commands for 2 arms (16D). Re-planned every <code>execution_horizon</code> steps via RTC." },
134
  rtc: { title: "Real-Time Chunking (RTC)", detail: "Generates the next chunk while executing the current one. Freezes actions guaranteed to execute and inpaints the rest for smooth asynchronous execution." },
135
  stage_tfm: { title: "Stage Transformer", detail: "Classifies the current frame into a stage/subtask. Uses causal attention over <code>[visual, language, state]</code> tokens. Outputs stage logits for sparse or dense schemes." },
136
  subtask_tfm: { title: "Subtask Transformer", detail: "Predicts within-stage progress τ ∈ [0,1] conditioned on the stage prior (one-hot). Teacher forcing: 75% ground-truth, 25% predicted stage." },
@@ -159,6 +177,7 @@ function showTT(event, key) {
159
  tt.style("left", Math.min(ex + 14, bx.width - ttW - 10) + "px").style("top", Math.max(ey - 80, 8) + "px");
160
  }
161
  function hideTT() { tt.style("opacity", 0); }
 
162
 
163
  function box(g, x, y, w, h, fill, stroke, rx) {
164
  return g.append("rect").attr("x",x).attr("y",y).attr("width",w).attr("height",h)
@@ -201,6 +220,15 @@ function hoverBox(g, x, y, w, h, key) {
201
  .on("mousemove", e => showTT(e, key)).on("mouseleave", hideTT);
202
  }
203
 
 
 
 
 
 
 
 
 
 
204
  function renderSystem() {
205
  var t = T();
206
  const W = container.clientWidth || 800;
@@ -211,13 +239,13 @@ function renderSystem() {
211
  const obsY = 14;
212
  const obsH = 62*s;
213
  box(g, pad, obsY, W-pad*2, obsH, t.obsBox, t.obsBorder);
214
- label(g, W/2, obsY+16, "OBSERVATION", 11*s, t.obsLabel, "middle", 700);
215
  const items = ["📷 Base Cam","📷 L-Wrist","📷 R-Wrist","🦾 Joints (16D)","💬 Language"];
216
  const iw = (W-pad*2-20) / items.length;
217
  items.forEach((txt,i) => {
218
  const ix = pad+10+i*iw;
219
  box(g, ix, obsY+26, iw-6, 26*s, t.obsInner, t.obsInnerBorder, 5);
220
- label(g, ix+(iw-6)/2, obsY+26+16*s, txt, 9.5*s, t.obsInnerText);
221
  });
222
  hoverBox(g, pad, obsY, W-pad*2, obsH, "obs");
223
 
@@ -226,43 +254,43 @@ function renderSystem() {
226
  const piW = W*0.58;
227
  const piX = pad;
228
  box(g, piX, piY, piW, piH, t.piBox, t.piBorder);
229
- label(g, piX+piW/2, piY+18, "π0.5 VISION-LANGUAGE-ACTION MODEL", 12*s, t.piLabel, "middle", 700);
230
 
231
  const sigY = piY+32;
232
  const sigH = 36*s;
233
  box(g, piX+12, sigY, piW*0.35, sigH, t.sigBox, t.sigBorder, 6);
234
- label(g, piX+12+piW*0.175, sigY+14*s, "SigLIP (400M)", 10*s, t.sigLabel, "middle", 600);
235
- label(g, piX+12+piW*0.175, sigY+26*s, "Vision Encoder", 8.5*s, t.sigSub);
236
  hoverBox(g, piX+12, sigY, piW*0.35, sigH, "siglip");
237
 
238
  const gemY = sigY;
239
  const gemX = piX+12+piW*0.38;
240
  const gemW = piW*0.58;
241
  box(g, gemX, gemY, gemW, sigH, t.gemBox, t.gemBorder, 6);
242
- label(g, gemX+gemW/2, gemY+14*s, "Gemma VLM (2B)", 10*s, t.gemLabel, "middle", 600);
243
- label(g, gemX+gemW/2, gemY+26*s, "Language + Vision Backbone", 8.5*s, t.gemSub);
244
  hoverBox(g, gemX, gemY, gemW, sigH, "gemma_vlm");
245
  arrow(g, piX+12+piW*0.35, sigY+sigH/2, gemX, sigY+sigH/2, t.sigBorder);
246
 
247
  const subY = sigY+sigH+14*s;
248
  const subH = 30*s;
249
  box(g, piX+12, subY, piW*0.42, subH, t.subBox, t.subBorder, 6);
250
- label(g, piX+12+piW*0.21, subY+12*s, "⬆ Subtask: \"pick up left sleeve\"", 9.5*s, t.subLabel, "middle", 600);
251
- label(g, piX+12+piW*0.21, subY+24*s, "Auto-regressive text decoding", 8*s, t.subSub);
252
  hoverBox(g, piX+12, subY, piW*0.42, subH, "subtask");
253
  arrow(g, gemX+gemW/2, gemY+sigH, piX+12+piW*0.21, subY, t.sigBorder, true);
254
 
255
  const aeY = subY+subH+12*s;
256
  const aeH = 44*s;
257
  box(g, piX+12, aeY, piW-24, aeH, t.aeBox, t.aeBorder, 6);
258
- label(g, piX+piW/2, aeY+14*s, "ACTION EXPERT (300M) — Flow Matching", 10.5*s, t.aeLabel, "middle", 700);
259
 
260
  const nX = piX+24;
261
  const nW = 56*s;
262
  const nY2 = aeY+24*s;
263
  const nH2 = 16*s;
264
  box(g, nX, nY2, nW, nH2, t.noiseBox, t.noiseBorder, 4);
265
- label(g, nX+nW/2, nY2+11*s, "Noise", 8*s, t.noiseLabel);
266
  hoverBox(g, nX, nY2, nW, nH2, "noise");
267
 
268
  const wvX = nX+nW+10;
@@ -275,11 +303,11 @@ function renderSystem() {
275
 
276
  const oaX = piX+piW-24-68*s;
277
  box(g, oaX, nY2, 62*s, nH2, t.actionBox, t.actionBorder, 4);
278
- label(g, oaX+31*s, nY2+11*s, "30 × 16D", 8*s, t.actionLabel);
279
  hoverBox(g, piX+12, aeY, piW-24, aeH, "action_expert");
280
 
281
  arrow(g, gemX+gemW*0.3, gemY+sigH, gemX+gemW*0.3, aeY, t.sigBorder, false);
282
- label(g, gemX+gemW*0.3+4, (gemY+sigH+aeY)/2, "VLM prefix", 7.5*s, t.sigBorder, "start");
283
 
284
  arrow(g, piX+12+piW*0.42, subY+subH/2, piX+piW-12, subY+subH/2, t.subBorder, true);
285
  curvedArrow(g, piX+piW-12, subY+subH/2, piX+piW-8, aeY-4, piX+piW-24, aeY, t.subBorder, true);
@@ -287,37 +315,37 @@ function renderSystem() {
287
  const rtcX = piX+piW-80*s;
288
  const rtcY2 = aeY+aeH+8*s;
289
  box(g, rtcX, rtcY2, 70*s, 22*s, t.rtcBox, t.rtcBorder, 5);
290
- label(g, rtcX+35*s, rtcY2+14*s, "🔄 RTC re-plan", 8.5*s, t.rtcLabel);
291
  hoverBox(g, rtcX, rtcY2, 70*s, 22*s, "rtc");
292
 
293
  const sX = piX+piW+16;
294
  const sW = W-sX-pad;
295
  const sH = piH;
296
  box(g, sX, piY, sW, sH, t.sarmBox, t.sarmBorder);
297
- label(g, sX+sW/2, piY+18, "SARM REWARD MODEL", 11*s, t.sarmLabel, "middle", 700);
298
- label(g, sX+sW/2, piY+34, "(training only)", 9*s, t.sarmSub, "middle", 600);
299
 
300
  const descY = piY+54;
301
- label(g, sX+sW/2, descY, "Scores every episode with a", 9.5*s, t.subtext);
302
- label(g, sX+sW/2, descY+16*s, "per-timestep quality signal [0, 1]", 9.5*s, t.subtext);
303
 
304
  const rwY = descY+32*s;
305
  const rwH = 34*s;
306
  box(g, sX+10, rwY, sW-20, rwH, t.rwBox, t.rwBorder, 6);
307
- label(g, sX+sW/2, rwY+14*s, "Per-timestep Reward", 10*s, t.rwLabel, "middle", 700);
308
- label(g, sX+sW/2, rwY+27*s, "0 = start · · · 1 = task complete", 8*s, t.rwSub);
309
  hoverBox(g, sX+10, rwY, sW-20, rwH, "sarm_reward");
310
 
311
  const wsY = rwY+rwH+14*s;
312
  const wsH = 34*s;
313
  box(g, sX+10, wsY, sW-20, wsH, t.wsBox, t.wsBorder, 6);
314
- label(g, sX+sW/2, wsY+14*s, "Weighted Sampling", 10*s, t.wsLabel, "middle", 700);
315
- label(g, sX+sW/2, wsY+27*s, "High-reward episodes contribute more", 8*s, t.wsSub);
316
  hoverBox(g, sX+10, wsY, sW-20, wsH, "rabc");
317
  arrow(g, sX+sW/2, rwY+rwH, sX+sW/2, wsY, t.rwBorder);
318
 
319
  curvedArrow(g, sX+10, wsY+wsH/2, sX-8, wsY+wsH/2+30*s, piX+piW/2, piY+piH+8, t.wsBorder, true);
320
- label(g, (sX+piX+piW/2)/2, piY+piH+22*s, "weighted sampling (training only)", 8*s, t.wsBorder);
321
  hoverBox(g, sX, piY, sW, sH, "sarm_box");
322
 
323
  arrow(g, W*0.3, obsY+obsH, piX+piW/2, piY, t.obsBorder);
@@ -326,8 +354,8 @@ function renderSystem() {
326
  const outY = piY+piH+16;
327
  const outH = 42*s;
328
  box(g, pad, outY, piW, outH, t.outBox, t.outBorder);
329
- label(g, piX+piW/2, outY+14*s, "ROBOT ACTIONS", 11*s, t.outLabel, "middle", 700);
330
- label(g, piX+piW/2, outY+30*s, "16D joint targets @ 30 Hz", 8.5*s, t.outSub);
331
  arrow(g, piX+piW/2, piY+piH, piX+piW/2, outY, t.outBorder);
332
  hoverBox(g, pad, outY, piW, outH, "actions");
333
 
@@ -339,96 +367,184 @@ function renderPi05() {
339
  var t = T();
340
  const W = container.clientWidth || 800;
341
  const g = svg.append("g");
342
- const s = Math.min(1.1, W / 720);
343
- const pad = 20;
344
- const cW = W-pad*2;
345
-
346
- const obsY = 10;
347
- const obsH = 56*s;
348
- box(g, pad, obsY, cW, obsH, t.obsBox, t.obsBorder);
349
- label(g, W/2, obsY+15, "OBSERVATION", 11*s, t.obsLabel, "middle", 700);
350
- const items = ["📷 Base Camera","📷 Left Wrist","📷 Right Wrist","🦾 Joints (16D)","💬 \"fold the shirt\""];
351
- const iw = (cW-20)/items.length;
352
- items.forEach((txt,i) => {
353
- box(g, pad+10+i*iw, obsY+26, iw-6, 22*s, t.obsInner, t.obsInnerBorder, 5);
354
- label(g, pad+10+i*iw+(iw-6)/2, obsY+40*s, txt, 9*s, t.obsInnerText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  });
356
- hoverBox(g, pad, obsY, cW, obsH, "obs");
357
- arrow(g, W/2, obsY+obsH, W/2, obsY+obsH+14, t.obsBorder);
358
-
359
- const sigY = obsY+obsH+18;
360
- const sigH = 40*s;
361
- box(g, pad, sigY, cW*0.35, sigH, t.sigBox, t.sigBorder);
362
- label(g, pad+cW*0.175, sigY+16*s, "SigLIP Vision Encoder", 10.5*s, t.sigLabel, "middle", 700);
363
- label(g, pad+cW*0.175, sigY+30*s, "400M params · frozen", 8.5*s, t.sigSub);
364
- hoverBox(g, pad, sigY, cW*0.35, sigH, "siglip");
365
-
366
- const gemX = pad+cW*0.38;
367
- const gemW = cW*0.62;
368
- box(g, gemX, sigY, gemW, sigH, t.gemBox, t.gemBorder);
369
- label(g, gemX+gemW/2, sigY+16*s, "Gemma VLM Backbone (2B)", 10.5*s, t.gemLabel, "middle", 700);
370
- label(g, gemX+gemW/2, sigY+30*s, "18 layers · 8 heads · 256 head_dim", 8.5*s, t.gemSub);
371
- hoverBox(g, gemX, sigY, gemW, sigH, "gemma_vlm");
372
- arrow(g, pad+cW*0.35, sigY+sigH/2, gemX, sigY+sigH/2, t.sigBorder);
373
-
374
- const subY = sigY+sigH+16;
375
- const subH = 38*s;
376
- const subW = cW*0.48;
377
- box(g, pad, subY, subW, subH, t.subBox, t.subBorder);
378
- label(g, pad+subW/2, subY+14*s, "HIGH-LEVEL: Subtask Prediction", 10*s, t.subLabel, "middle", 700);
379
- label(g, pad+subW/2, subY+28*s, "Auto-regressive → \"pick up left sleeve\"", 8.5*s, t.subSub);
380
- hoverBox(g, pad, subY, subW, subH, "subtask");
381
-
382
- const rtcX2 = pad+subW+14;
383
- box(g, rtcX2, subY, cW-subW-14, subH, t.rtcBox, t.rtcBorder);
384
- label(g, rtcX2+(cW-subW-14)/2, subY+14*s, "🔄 Real-Time Chunking (RTC)", 10*s, t.rtcLabel, "middle", 700);
385
- label(g, rtcX2+(cW-subW-14)/2, subY+28*s, "Generate next chunk while executing current", 8.5*s, t.sarmSub);
386
- hoverBox(g, rtcX2, subY, cW-subW-14, subH, "rtc");
387
- arrow(g, W/2, sigY+sigH, W/2, subY, t.sigBorder);
388
-
389
- const aeY = subY+subH+16;
390
- const aeH = 72*s;
391
- box(g, pad, aeY, cW, aeH, t.aeBox, t.aeBorder);
392
- label(g, W/2, aeY+16*s, "ACTION EXPERT — Flow Matching (300M)", 11*s, t.aeLabel, "middle", 700);
393
- hoverBox(g, pad, aeY, cW, aeH, "action_expert");
394
-
395
- const innerY = aeY+28*s;
396
- const innerH = 34*s;
397
- const noiseW = cW*0.22;
398
- box(g, pad+14, innerY, noiseW, innerH, t.noiseBox, t.noiseBorder, 6);
399
- label(g, pad+14+noiseW/2, innerY+14*s, "Gaussian Noise", 9*s, t.noiseLabel, "middle", 600);
400
- label(g, pad+14+noiseW/2, innerY+27*s, "sampled input", 7.5*s, t.noiseBorder);
401
- hoverBox(g, pad+14, innerY, noiseW, innerH, "noise");
402
-
403
- const dsX = pad+14+noiseW+14;
404
- const dsW = cW*0.32;
405
- box(g, dsX, innerY, dsW, innerH, t.flowBox, t.flowBorder, 6);
406
- label(g, dsX+dsW/2, innerY+14*s, "Flow Matching", 9*s, t.flowLabel, "middle", 600);
407
- label(g, dsX+dsW/2, innerY+27*s, "denoise → continuous actions", 7.5*s, t.flowSub);
408
- hoverBox(g, dsX, innerY, dsW, innerH, "flow");
409
- arrow(g, pad+14+noiseW, innerY+innerH/2, dsX, innerY+innerH/2, t.aeBorder);
410
-
411
- const ocX = dsX+dsW+14;
412
- const ocW = cW-ocX+pad-14;
413
- box(g, ocX, innerY, ocW, innerH, t.actionBox, t.actionBorder, 6);
414
- label(g, ocX+ocW/2, innerY+14*s, "30 Actions", 9*s, t.actionLabel, "middle", 600);
415
- label(g, ocX+ocW/2, innerY+27*s, "16D @ 30 Hz", 7.5*s, t.actionBorder);
416
- arrow(g, dsX+dsW, innerY+innerH/2, ocX, innerY+innerH/2, t.actionBorder);
417
- hoverBox(g, ocX, innerY, ocW, innerH, "actions");
418
-
419
- arrow(g, pad+subW/2, subY+subH, W/2-40*s, aeY, t.subBorder, true);
420
- arrow(g, gemX+gemW*0.7, sigY+sigH, W/2+60*s, aeY, t.sigBorder);
421
- label(g, W/2+64*s, (sigY+sigH+aeY)/2, "VLM prefix", 8.5*s, t.sigBorder, "start");
422
-
423
- const outY2 = aeY+aeH+16;
424
- const outH2 = 36*s;
425
- box(g, pad, outY2, cW, outH2, t.outBox, t.outBorder);
426
- label(g, W/2, outY2+14*s, "CONTINUOUS ROBOT ACTIONS", 11*s, t.outLabel, "middle", 700);
427
- label(g, W/2, outY2+28*s, "16D joint + gripper targets @ 30 Hz → PD controller", 8.5*s, t.outSub);
428
- arrow(g, W/2, aeY+aeH, W/2, outY2, t.outBorder);
429
- hoverBox(g, pad, outY2, cW, outH2, "actions");
430
-
431
- const H = outY2 + outH2 + 10;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  svg.attr("width",W).attr("height",H).attr("viewBox",`0 0 ${W} ${H}`);
433
  }
434
 
@@ -436,105 +552,121 @@ function renderSARM() {
436
  var t = T();
437
  const W = container.clientWidth || 800;
438
  const g = svg.append("g");
439
- const s = Math.min(1.1, W / 720);
440
- const pad = 20;
441
- const cW = W-pad*2;
442
-
443
- const obsY = 10;
444
- const obsH = 50*s;
445
- box(g, pad, obsY, cW, obsH, t.obsBox, t.obsBorder);
446
- label(g, W/2, obsY+15, "SARM INPUTS", 11*s, t.obsLabel, "middle", 700);
447
- const items = [
448
- {txt:"📷 8 Images (1s apart)", k:"embed_video"},
449
- {txt:"💬 Text Embeddings", k:"embed_text"},
450
- {txt:"🦾 State Features", k:"embed_state"}
451
- ];
452
- const iw = (cW-20)/items.length;
453
- items.forEach((d,i) => {
454
- box(g, pad+10+i*iw, obsY+24, iw-6, 20*s, t.obsInner, t.obsInnerBorder, 5);
455
- label(g, pad+10+i*iw+(iw-6)/2, obsY+36*s, d.txt, 9*s, t.obsInnerText);
456
- hoverBox(g, pad+10+i*iw, obsY+24, iw-6, 20*s, d.k);
457
- });
458
-
459
- const stY = obsY+obsH+22;
460
- const stH = 80*s;
461
- box(g, pad, stY, cW, stH, t.sarmBox, t.stageBorder);
462
- label(g, W/2, stY+16*s, "STAGE TRANSFORMER (training-time scoring)", 11*s, t.stageText, "middle", 700);
463
- hoverBox(g, pad, stY, cW, stH, "stage_tfm");
464
- arrow(g, W/2, obsY+obsH, W/2, stY, t.obsBorder);
465
-
466
- const innerStY = stY+28*s;
467
- const projW = cW*0.22;
468
- ["vis_proj","lang_proj","state_proj"].forEach((name,i) => {
469
- const px = pad+14+i*(projW+8);
470
- box(g, px, innerStY, projW, 18*s, t.stageInner, t.stageBorder, 4);
471
- label(g, px+projW/2, innerStY+12*s, name+" → d=512", 7.5*s, t.stageText);
472
- });
473
-
474
- const tfmBarY = innerStY+24*s;
475
- box(g, pad+14, tfmBarY, cW-28, 18*s, t.stageInner, t.stageBorder, 4);
476
- label(g, W/2, tfmBarY+12*s, "Causal Transformer Encoder (6 layers · 8 heads) → Fusion MLP → Stage Logits", 8.5*s, t.stageText);
477
-
478
- const soY = stY+stH+8;
479
- const soH = 28*s;
480
- box(g, pad+cW*0.3, soY, cW*0.4, soH, t.rtcBox, t.rtcBorder);
481
- label(g, W/2, soY+11*s, "Stage Classification", 10*s, t.rtcLabel, "middle", 700);
482
- label(g, W/2, soY+22*s, "softmax → argmax → one-hot prior", 8*s, t.sarmSub);
483
- arrow(g, W/2, stY+stH, W/2, soY, t.stageBorder);
484
-
485
- const tfY = soY-2;
486
- box(g, pad, tfY, cW*0.26, soH+4, t.tfBox, t.tfBorder, 6);
487
- label(g, pad+cW*0.13, tfY+12*s, "Teacher Forcing", 9*s, t.tfLabel, "middle", 600);
488
- label(g, pad+cW*0.13, tfY+24*s, "75% GT / 25% predicted", 7.5*s, t.tfSub);
489
- arrow(g, pad+cW*0.26, tfY+soH/2, pad+cW*0.3, soY+soH/2, t.tfBorder, true);
490
-
491
- const stfY = soY+soH+18;
492
- const stfH = 72*s;
493
- box(g, pad, stfY, cW, stfH, t.sarmBox, t.stageBorder);
494
- label(g, W/2, stfY+16*s, "SUBTASK TRANSFORMER", 12*s, t.stageText, "middle", 700);
495
- hoverBox(g, pad, stfY, cW, stfH, "subtask_tfm");
496
- arrow(g, W/2, soY+soH, W/2, stfY, t.rtcBorder);
497
- curvedArrow(g, W*0.25, obsY+obsH, W*0.15, stfY-10, pad+cW*0.1, stfY, t.obsBorder, true);
498
-
499
- const inner2Y = stfY+28*s;
500
- ["vis_proj","lang_proj","state_proj","stage_emb"].forEach((name,i) => {
501
- const pw = cW*0.19;
502
- const px = pad+10+i*(pw+6);
503
- box(g, px, inner2Y, pw, 18*s, t.stageInner, t.stageBorder, 4);
504
- label(g, px+pw/2, inner2Y+12*s, name, 7.5*s, t.stageText);
505
- });
506
-
507
- const tfm2Y = inner2Y+24*s;
508
- box(g, pad+14, tfm2Y, cW-28, 16*s, t.stageInner, t.stageBorder, 4);
509
- label(g, W/2, tfm2Y+11*s, "Causal Transformer (6L · 8H) → Fusion MLP → sigmoid → τ ∈ [0,1]", 8.5*s, t.stageText);
510
-
511
- const rwY = stfY+stfH+18;
512
- const rwH = 36*s;
513
- box(g, pad, rwY, cW, rwH, t.rwBox, t.rwBorder);
514
- label(g, W/2, rwY+14*s, "NORMALIZED REWARD = normalize(stage + τ)", 11*s, t.rwLabel, "middle", 700);
515
- label(g, W/2, rwY+28*s, "Weighted by temporal proportions per subtask → [0, 1]", 8.5*s, t.rwSub);
516
- arrow(g, W/2, stfY+stfH, W/2, rwY, t.stageBorder);
517
- hoverBox(g, pad, rwY, cW, rwH, "sarm_reward");
518
-
519
- const usesY = rwY+rwH+16;
520
- const useW2 = cW/2-8;
521
- [{txt:"Episode Scoring",sub:"Per-timestep quality signal [0,1]",c:t.aeBorder,k:"data_scoring"},
522
- {txt:"RABC Weighted Sampling",sub:"High-reward actions contribute more to loss",c:t.wsBorder,k:"rabc"}
523
- ].forEach((d,i) => {
524
- const ux = pad+i*(useW2+16);
525
- box(g, ux, usesY, useW2, 32*s, t.scoreBox, d.c, 6);
526
- label(g, ux+useW2/2, usesY+12*s, d.txt, 9.5*s, t.scoreText, "middle", 600);
527
- label(g, ux+useW2/2, usesY+25*s, d.sub, 7.5*s, d.c);
528
- hoverBox(g, ux, usesY, useW2, 32*s, d.k);
529
- });
530
- arrow(g, W/2-useW2*0.3, rwY+rwH, pad+useW2/2, usesY, t.rwBorder);
531
- arrow(g, W/2+useW2*0.3, rwY+rwH, pad+useW2+16+useW2/2, usesY, t.rwBorder);
532
-
533
- const H = usesY + 32*s + 10;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  svg.attr("width",W).attr("height",H).attr("viewBox",`0 0 ${W} ${H}`);
535
  }
536
 
537
  function render() {
 
538
  svg.selectAll("*").remove();
539
  applyTabTheme();
540
  if (currentTab === "system") renderSystem();
@@ -542,14 +674,16 @@ function render() {
542
  else if (currentTab === "sarm") renderSARM();
543
  }
544
 
545
- document.querySelectorAll(".tab-btn").forEach(btn => {
546
- btn.addEventListener("click", () => {
547
- document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
548
- btn.classList.add("active");
549
- currentTab = btn.dataset.tab;
550
- render();
 
 
551
  });
552
- });
553
 
554
  render();
555
  window.addEventListener("resize", render);
@@ -563,10 +697,10 @@ obs.observe(themeEl, { attributes: true, attributeFilter: ["data-theme"] });
563
  if (typeof d3 !== "undefined") {
564
  _initArchViz();
565
  } else {
566
- var s = document.createElement("script");
567
- s.src = "https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js";
568
- s.onload = _initArchViz;
569
- document.head.appendChild(s);
570
  }
571
  </script>
572
  </body>
 
11
  .tooltip-arch {
12
  position: absolute; background: #13152a; border: 1px solid #333; border-radius: 10px;
13
  padding: 14px 18px; pointer-events: none; opacity: 0; transition: opacity .2s;
14
+ z-index: 20; max-width: 320px; box-shadow: 0 8px 32px rgba(0,0,0,.6); font-size: 14px; line-height: 1.5;
15
  }
16
+ .tooltip-arch strong { display: block; margin-bottom: 6px; font-size: 15px; color: #e8eaf0; }
17
+ .tooltip-arch .tt-detail { color: #8b8fa8; font-size: 13px; }
18
+ .tooltip-arch code { background: #252540; padding: 1px 5px; border-radius: 3px; font-size: 12px; color: #a78bfa; }
19
  .tab-bar { display: flex; gap: 0; justify-content: center; margin-bottom: 0; }
20
  .tab-btn {
21
+ padding: 9px 22px; font-size: 15px; font-weight: 600; cursor: pointer; border: none;
22
  background: #1a1d2e; color: #8b8fa8; transition: all .2s; border-bottom: 2px solid transparent;
23
  font-family: system-ui, sans-serif;
24
  }
 
34
  <button class="tab-btn" data-tab="pi05">π0.5 Detail</button>
35
  <button class="tab-btn" data-tab="sarm">SARM Detail</button>
36
  </div>
37
+ <div class="arch-container">
38
+ <svg class="arch-svg"></svg>
39
+ <div class="tooltip-arch"></div>
40
  </div>
41
  <script>
42
  function _initArchViz() {
43
+ var root = document.querySelector(".arch-container:not([data-init])");
44
+ if (!root) return;
45
+ root.setAttribute("data-init", "1");
46
+ var tabBar = root.previousElementSibling;
47
+ if (!tabBar || !tabBar.classList.contains("tab-bar")) tabBar = null;
48
+
49
+ const svg = d3.select(root.querySelector(".arch-svg"));
50
+ const container = root;
51
+ const tt = d3.select(root.querySelector(".tooltip-arch"));
52
+ const configEl = container.closest("[data-config]");
53
+ const chartConfig = configEl ? JSON.parse(configEl.dataset.config || "{}") : {};
54
+ let currentTab = chartConfig.initialTab || "system";
55
+ if (chartConfig.hideTabBar && tabBar) {
56
+ tabBar.style.display = "none";
57
+ }
58
+ if (tabBar) {
59
+ tabBar.querySelectorAll(".tab-btn").forEach(b => {
60
+ b.classList.toggle("active", b.dataset.tab === currentTab);
61
+ });
62
+ }
63
 
64
  function isLight() {
65
  try {
 
91
  scoreBox: "#1a1a2e", scoreText: "#e8eaf0",
92
  ttBg: "#13152a", ttBorder: "#333", ttText: "#e8eaf0", ttSub: "#8b8fa8", ttCode: "#252540", ttCodeText: "#a78bfa",
93
  tabBg: "#1a1d2e", tabText: "#8b8fa8", tabActiveBg: "#252540", tabActiveText: "#e8eaf0",
94
+ bg: "#0f1117",
95
  };
96
 
97
  const LIGHT = {
 
116
  scoreBox: "#f9fafb", scoreText: "#1a1a2e",
117
  ttBg: "#fff", ttBorder: "#e2e4e9", ttText: "#1a1a2e", ttSub: "#6b7280", ttCode: "#f3f4f6", ttCodeText: "#6d28d9",
118
  tabBg: "#f3f4f6", tabText: "#6b7280", tabActiveBg: "#fff", tabActiveText: "#1a1a2e",
119
+ bg: "#ffffff",
120
  };
121
 
122
  function T() { return isLight() ? LIGHT : DARK; }
123
 
124
  function applyTabTheme() {
125
  var t = T();
126
+ if (tabBar) tabBar.querySelectorAll(".tab-btn").forEach(function(btn) {
127
  if (btn.classList.contains("active")) {
128
  btn.style.background = t.tabActiveBg;
129
  btn.style.color = t.tabActiveText;
 
132
  btn.style.color = t.tabText;
133
  }
134
  });
135
+ var tip = root.querySelector(".tooltip-arch");
136
  if (tip) {
137
  tip.style.background = t.ttBg;
138
  tip.style.borderColor = t.ttBorder;
 
146
  siglip: { title: "SigLIP Vision Encoder (400M)", detail: "Converts camera images to patch embeddings. Frozen during fine-tuning for stability. Outputs visual tokens for the VLM backbone." },
147
  gemma_vlm: { title: "Gemma VLM Backbone (2B)", detail: "Pre-trained language model that processes visual tokens, language tokens, and proprioceptive state. Shares attention across all modalities." },
148
  subtask: { title: "Subtask Prediction", detail: "Auto-regressive text decoding produces a high-level subtask like <code>\"pick up left sleeve\"</code>. Acts as chain-of-thought for the action expert." },
149
+ action_expert: { title: "Action Expert (300M)", detail: "Smaller Gemma model dedicated to flow matching. Denoises Gaussian noise into continuous 16D actions (30 steps) over <code>10 denoising steps</code>. Conditioned on VLM backbone output." },
150
  flow: { title: "Flow Matching", detail: "Generative approach that denoises Gaussian noise into continuous actions. Enables multi-modal action distributions for complex manipulation." },
151
+ actions: { title: "Continuous Actions", detail: "30-step action chunk: target joint angles + gripper commands for 2 arms (16D). Re-planned every <code>execution_horizon</code> steps via RTC." },
152
  rtc: { title: "Real-Time Chunking (RTC)", detail: "Generates the next chunk while executing the current one. Freezes actions guaranteed to execute and inpaints the rest for smooth asynchronous execution." },
153
  stage_tfm: { title: "Stage Transformer", detail: "Classifies the current frame into a stage/subtask. Uses causal attention over <code>[visual, language, state]</code> tokens. Outputs stage logits for sparse or dense schemes." },
154
  subtask_tfm: { title: "Subtask Transformer", detail: "Predicts within-stage progress τ ∈ [0,1] conditioned on the stage prior (one-hot). Teacher forcing: 75% ground-truth, 25% predicted stage." },
 
177
  tt.style("left", Math.min(ex + 14, bx.width - ttW - 10) + "px").style("top", Math.max(ey - 80, 8) + "px");
178
  }
179
  function hideTT() { tt.style("opacity", 0); }
180
+ var _pi05AnimId = null;
181
 
182
  function box(g, x, y, w, h, fill, stroke, rx) {
183
  return g.append("rect").attr("x",x).attr("y",y).attr("width",w).attr("height",h)
 
220
  .on("mousemove", e => showTT(e, key)).on("mouseleave", hideTT);
221
  }
222
 
223
+ function arrowLabel(g, x, y, text, size, color, anchor) {
224
+ return g.append("text").attr("x",x).attr("y",y).attr("text-anchor",anchor||"middle")
225
+ .attr("font-size",size).attr("fill",color||T().subtext)
226
+ .attr("font-weight",400).attr("font-family","system-ui, sans-serif")
227
+ .attr("paint-order","stroke fill")
228
+ .attr("stroke", T().bg).attr("stroke-width", 4).attr("stroke-linejoin","round")
229
+ .text(text);
230
+ }
231
+
232
  function renderSystem() {
233
  var t = T();
234
  const W = container.clientWidth || 800;
 
239
  const obsY = 14;
240
  const obsH = 62*s;
241
  box(g, pad, obsY, W-pad*2, obsH, t.obsBox, t.obsBorder);
242
+ label(g, W/2, obsY+16, "OBSERVATION", 13*s, t.obsLabel, "middle", 700);
243
  const items = ["📷 Base Cam","📷 L-Wrist","📷 R-Wrist","🦾 Joints (16D)","💬 Language"];
244
  const iw = (W-pad*2-20) / items.length;
245
  items.forEach((txt,i) => {
246
  const ix = pad+10+i*iw;
247
  box(g, ix, obsY+26, iw-6, 26*s, t.obsInner, t.obsInnerBorder, 5);
248
+ label(g, ix+(iw-6)/2, obsY+26+16*s, txt, 11*s, t.obsInnerText);
249
  });
250
  hoverBox(g, pad, obsY, W-pad*2, obsH, "obs");
251
 
 
254
  const piW = W*0.58;
255
  const piX = pad;
256
  box(g, piX, piY, piW, piH, t.piBox, t.piBorder);
257
+ label(g, piX+piW/2, piY+18, "π0.5 VISION-LANGUAGE-ACTION MODEL", 14*s, t.piLabel, "middle", 700);
258
 
259
  const sigY = piY+32;
260
  const sigH = 36*s;
261
  box(g, piX+12, sigY, piW*0.35, sigH, t.sigBox, t.sigBorder, 6);
262
+ label(g, piX+12+piW*0.175, sigY+14*s, "SigLIP (400M)", 12*s, t.sigLabel, "middle", 600);
263
+ label(g, piX+12+piW*0.175, sigY+26*s, "Vision Encoder", 10*s, t.sigSub);
264
  hoverBox(g, piX+12, sigY, piW*0.35, sigH, "siglip");
265
 
266
  const gemY = sigY;
267
  const gemX = piX+12+piW*0.38;
268
  const gemW = piW*0.58;
269
  box(g, gemX, gemY, gemW, sigH, t.gemBox, t.gemBorder, 6);
270
+ label(g, gemX+gemW/2, gemY+14*s, "Gemma VLM (2B)", 12*s, t.gemLabel, "middle", 600);
271
+ label(g, gemX+gemW/2, gemY+26*s, "Language + Vision Backbone", 10*s, t.gemSub);
272
  hoverBox(g, gemX, gemY, gemW, sigH, "gemma_vlm");
273
  arrow(g, piX+12+piW*0.35, sigY+sigH/2, gemX, sigY+sigH/2, t.sigBorder);
274
 
275
  const subY = sigY+sigH+14*s;
276
  const subH = 30*s;
277
  box(g, piX+12, subY, piW*0.42, subH, t.subBox, t.subBorder, 6);
278
+ label(g, piX+12+piW*0.21, subY+12*s, "⬆ Subtask: \"pick up left sleeve\"", 11*s, t.subLabel, "middle", 600);
279
+ label(g, piX+12+piW*0.21, subY+24*s, "Auto-regressive text decoding", 9.5*s, t.subSub);
280
  hoverBox(g, piX+12, subY, piW*0.42, subH, "subtask");
281
  arrow(g, gemX+gemW/2, gemY+sigH, piX+12+piW*0.21, subY, t.sigBorder, true);
282
 
283
  const aeY = subY+subH+12*s;
284
  const aeH = 44*s;
285
  box(g, piX+12, aeY, piW-24, aeH, t.aeBox, t.aeBorder, 6);
286
+ label(g, piX+piW/2, aeY+14*s, "ACTION EXPERT (300M): Flow Matching", 12.5*s, t.aeLabel, "middle", 700);
287
 
288
  const nX = piX+24;
289
  const nW = 56*s;
290
  const nY2 = aeY+24*s;
291
  const nH2 = 16*s;
292
  box(g, nX, nY2, nW, nH2, t.noiseBox, t.noiseBorder, 4);
293
+ label(g, nX+nW/2, nY2+11*s, "Noise", 9.5*s, t.noiseLabel);
294
  hoverBox(g, nX, nY2, nW, nH2, "noise");
295
 
296
  const wvX = nX+nW+10;
 
303
 
304
  const oaX = piX+piW-24-68*s;
305
  box(g, oaX, nY2, 62*s, nH2, t.actionBox, t.actionBorder, 4);
306
+ label(g, oaX+31*s, nY2+11*s, "30 × 16D", 9.5*s, t.actionLabel);
307
  hoverBox(g, piX+12, aeY, piW-24, aeH, "action_expert");
308
 
309
  arrow(g, gemX+gemW*0.3, gemY+sigH, gemX+gemW*0.3, aeY, t.sigBorder, false);
310
+ label(g, gemX+gemW*0.3+4, (gemY+sigH+aeY)/2, "VLM prefix", 9*s, t.sigBorder, "start");
311
 
312
  arrow(g, piX+12+piW*0.42, subY+subH/2, piX+piW-12, subY+subH/2, t.subBorder, true);
313
  curvedArrow(g, piX+piW-12, subY+subH/2, piX+piW-8, aeY-4, piX+piW-24, aeY, t.subBorder, true);
 
315
  const rtcX = piX+piW-80*s;
316
  const rtcY2 = aeY+aeH+8*s;
317
  box(g, rtcX, rtcY2, 70*s, 22*s, t.rtcBox, t.rtcBorder, 5);
318
+ label(g, rtcX+35*s, rtcY2+14*s, "🔄 RTC re-plan", 10*s, t.rtcLabel);
319
  hoverBox(g, rtcX, rtcY2, 70*s, 22*s, "rtc");
320
 
321
  const sX = piX+piW+16;
322
  const sW = W-sX-pad;
323
  const sH = piH;
324
  box(g, sX, piY, sW, sH, t.sarmBox, t.sarmBorder);
325
+ label(g, sX+sW/2, piY+18, "SARM REWARD MODEL", 13*s, t.sarmLabel, "middle", 700);
326
+ label(g, sX+sW/2, piY+34, "(training only)", 10.5*s, t.sarmSub, "middle", 600);
327
 
328
  const descY = piY+54;
329
+ label(g, sX+sW/2, descY, "Scores every episode with a", 11*s, t.subtext);
330
+ label(g, sX+sW/2, descY+16*s, "per-timestep quality signal [0, 1]", 11*s, t.subtext);
331
 
332
  const rwY = descY+32*s;
333
  const rwH = 34*s;
334
  box(g, sX+10, rwY, sW-20, rwH, t.rwBox, t.rwBorder, 6);
335
+ label(g, sX+sW/2, rwY+14*s, "Per-timestep Reward", 12*s, t.rwLabel, "middle", 700);
336
+ label(g, sX+sW/2, rwY+27*s, "0 = start · · · 1 = task complete", 9.5*s, t.rwSub);
337
  hoverBox(g, sX+10, rwY, sW-20, rwH, "sarm_reward");
338
 
339
  const wsY = rwY+rwH+14*s;
340
  const wsH = 34*s;
341
  box(g, sX+10, wsY, sW-20, wsH, t.wsBox, t.wsBorder, 6);
342
+ label(g, sX+sW/2, wsY+14*s, "Weighted Sampling", 12*s, t.wsLabel, "middle", 700);
343
+ label(g, sX+sW/2, wsY+27*s, "High-reward episodes contribute more", 9.5*s, t.wsSub);
344
  hoverBox(g, sX+10, wsY, sW-20, wsH, "rabc");
345
  arrow(g, sX+sW/2, rwY+rwH, sX+sW/2, wsY, t.rwBorder);
346
 
347
  curvedArrow(g, sX+10, wsY+wsH/2, sX-8, wsY+wsH/2+30*s, piX+piW/2, piY+piH+8, t.wsBorder, true);
348
+ label(g, (sX+piX+piW/2)/2, piY+piH+22*s, "weighted sampling (training only)", 9.5*s, t.wsBorder);
349
  hoverBox(g, sX, piY, sW, sH, "sarm_box");
350
 
351
  arrow(g, W*0.3, obsY+obsH, piX+piW/2, piY, t.obsBorder);
 
354
  const outY = piY+piH+16;
355
  const outH = 42*s;
356
  box(g, pad, outY, piW, outH, t.outBox, t.outBorder);
357
+ label(g, piX+piW/2, outY+14*s, "ROBOT ACTIONS", 13*s, t.outLabel, "middle", 700);
358
+ label(g, piX+piW/2, outY+30*s, "16D joint targets", 10*s, t.outSub);
359
  arrow(g, piX+piW/2, piY+piH, piX+piW/2, outY, t.outBorder);
360
  hoverBox(g, pad, outY, piW, outH, "actions");
361
 
 
367
  var t = T();
368
  const W = container.clientWidth || 800;
369
  const g = svg.append("g");
370
+ const s = Math.min(1.1, W / 740);
371
+ const pad = 16;
372
+ const rowH = 44*s;
373
+ const gap = 18*s;
374
+ const colInputW = W*0.155;
375
+ const colEncW = W*0.105;
376
+ const colVlmW = W*0.185;
377
+ const colAeW = W*0.22;
378
+ const colOutW = W*0.11;
379
+ const arrowGap = 14*s;
380
+ const fs = 12*s;
381
+ const fsSmall = 10*s;
382
+ const fsBold = 13*s;
383
+
384
+ const x0 = pad;
385
+ const x1 = x0+colInputW+arrowGap*2;
386
+ const x2 = x1+colEncW+arrowGap*2;
387
+ const x3 = x2+colVlmW+arrowGap*2;
388
+ const x4 = x3+colAeW+arrowGap*2;
389
+
390
+ const camBoxH = rowH*1.8;
391
+ const imgY = pad;
392
+ const stateY = imgY+camBoxH+gap;
393
+ const langY = stateY+rowH+gap;
394
+
395
+ // Camera images with real thumbnails
396
+ box(g, x0, imgY, colInputW, camBoxH, t.obsBox, t.obsBorder, 6);
397
+ label(g, x0+colInputW/2, imgY+11*s, "Camera Images", fsBold, t.obsLabel, "middle", 600);
398
+ var thumbGap = 3*s;
399
+ var thumbW = (colInputW - 14*s - thumbGap*2) / 3;
400
+ var thumbH = thumbW * 0.7;
401
+ var thumbY = imgY+20*s;
402
+ var cams = [{f:"cam_base.jpg",l:"base"},{f:"cam_left_wrist.jpg",l:"L wrist"},{f:"cam_right_wrist.jpg",l:"R wrist"}];
403
+ cams.forEach(function(cam,i) {
404
+ var tx = x0+7*s + i*(thumbW+thumbGap);
405
+ var cid = "tc_"+Math.random().toString(36).slice(2,8);
406
+ g.append("defs").append("clipPath").attr("id",cid)
407
+ .append("rect").attr("x",tx).attr("y",thumbY).attr("width",thumbW).attr("height",thumbH).attr("rx",3);
408
+ g.append("image").attr("href","/images/folding/"+cam.f)
409
+ .attr("x",tx).attr("y",thumbY).attr("width",thumbW).attr("height",thumbH)
410
+ .attr("preserveAspectRatio","xMidYMid slice").attr("clip-path","url(#"+cid+")");
411
+ g.append("rect").attr("x",tx).attr("y",thumbY).attr("width",thumbW).attr("height",thumbH)
412
+ .attr("rx",3).attr("fill","none").attr("stroke",t.obsInnerBorder).attr("stroke-width",1);
413
+ label(g, tx+thumbW/2, thumbY+thumbH+8*s, cam.l, 8*s, t.obsInnerText);
414
  });
415
+ hoverBox(g, x0, imgY, colInputW, camBoxH, "obs");
416
+
417
+ box(g, x0, stateY, colInputW, rowH, t.obsBox, t.obsBorder, 6);
418
+ label(g, x0+colInputW/2, stateY+13*s, "Joint Angles", fsBold, t.obsLabel, "middle", 600);
419
+ label(g, x0+colInputW/2, stateY+25*s, "16D (7 joints+grip ×2)", fsSmall, t.obsInnerText);
420
+ hoverBox(g, x0, stateY, colInputW, rowH, "obs");
421
+
422
+ box(g, x0, langY, colInputW, rowH, t.obsBox, t.obsBorder, 6);
423
+ label(g, x0+colInputW/2, langY+13*s, "Language Prompt", fsBold, t.obsLabel, "middle", 600);
424
+ label(g, x0+colInputW/2, langY+25*s, "\"fold the t-shirt\"", fsSmall, t.obsInnerText);
425
+ hoverBox(g, x0, langY, colInputW, rowH, "obs");
426
+
427
+ // SigLIP encoder (aligned with camera box)
428
+ box(g, x1, imgY, colEncW, camBoxH, t.sigBox, t.sigBorder, 6);
429
+ label(g, x1+colEncW/2, imgY+camBoxH/2-5*s, "SigLIP", fsBold, t.sigLabel, "middle", 600);
430
+ label(g, x1+colEncW/2, imgY+camBoxH/2+7*s, "400M · frozen", fsSmall, t.sigSub);
431
+ hoverBox(g, x1, imgY, colEncW, camBoxH, "siglip");
432
+
433
+ arrow(g, x0+colInputW, imgY+camBoxH/2, x1, imgY+camBoxH/2, t.obsBorder);
434
+ arrowLabel(g, (x0+colInputW+x1)/2, imgY+camBoxH/2-8*s, "pixels", fsSmall, t.subtext);
435
+
436
+ // VLM backbone
437
+ const vlmH = langY+rowH-imgY;
438
+ box(g, x2, imgY, colVlmW, vlmH, t.gemBox, t.gemBorder, 6);
439
+ label(g, x2+colVlmW/2, imgY+14*s, "Gemma VLM", fsBold, t.gemLabel, "middle", 700);
440
+ label(g, x2+colVlmW/2, imgY+26*s, "2B params", fsSmall, t.gemSub);
441
+ label(g, x2+colVlmW/2, imgY+vlmH/2+2*s, "Cross-attention over", fsSmall, t.subtext);
442
+ label(g, x2+colVlmW/2, imgY+vlmH/2+14*s, "all three modalities", fsSmall, t.subtext);
443
+ hoverBox(g, x2, imgY, colVlmW, vlmH, "gemma_vlm");
444
+
445
+ arrow(g, x1+colEncW, imgY+camBoxH/2, x2, imgY+camBoxH/2, t.sigBorder);
446
+ arrowLabel(g, (x1+colEncW+x2)/2, imgY+camBoxH/2-8*s, "visual tokens", fsSmall, t.subtext);
447
+
448
+ arrow(g, x0+colInputW, stateY+rowH/2, x2, stateY+rowH/2, t.obsBorder);
449
+ arrowLabel(g, (x0+colInputW+x2)/2, stateY+rowH/2-8*s, "state vector", fsSmall, t.subtext);
450
+
451
+ arrow(g, x0+colInputW, langY+rowH/2, x2, langY+rowH/2, t.obsBorder);
452
+ arrowLabel(g, (x0+colInputW+x2)/2, langY+rowH/2-8*s, "text tokens", fsSmall, t.subtext);
453
+
454
+ // Action Expert with flow matching animation
455
+ const aeH2 = vlmH;
456
+ box(g, x3, imgY, colAeW, aeH2, t.aeBox, t.aeBorder, 6);
457
+ label(g, x3+colAeW/2, imgY+14*s, "Action Expert", fsBold, t.aeLabel, "middle", 700);
458
+ label(g, x3+colAeW/2, imgY+26*s, "300M · flow matching", fsSmall, t.flowSub);
459
+
460
+ arrow(g, x2+colVlmW, imgY+vlmH/2, x3, imgY+aeH2/2, t.gemBorder);
461
+ arrowLabel(g, (x2+colVlmW+x3)/2, imgY+vlmH/2-8*s, "VLM features", fsSmall, t.subtext);
462
+
463
+ // Flow matching animation area
464
+ var aP = 6*s;
465
+ var aTop = imgY+36*s, aBot = imgY+aeH2-aP-12*s;
466
+ var aLeft = x3+aP, aRight = x3+colAeW-aP;
467
+ var aW = aRight-aLeft, aH = aBot-aTop, aCY = aTop+aH/2;
468
+
469
+ g.append("rect").attr("x",aLeft).attr("y",aTop).attr("width",aW).attr("height",aH)
470
+ .attr("rx",4).attr("fill",isLight()?"rgba(0,0,0,0.03)":"rgba(255,255,255,0.03)");
471
+
472
+ label(g, aLeft+8*s, aBot+10*s, "N(0, I)", 8*s, t.noiseLabel, "start");
473
+ label(g, aRight-8*s, aBot+10*s, "actions", 8*s, t.actionLabel, "end");
474
+ var stepText = g.append("text").attr("x",aLeft+aW/2).attr("y",aBot+10*s)
475
+ .attr("text-anchor","middle").attr("font-size",8*s)
476
+ .attr("fill",t.subtext).attr("font-family","system-ui, sans-serif").text("step 0/10");
477
+
478
+ // Seeded random for deterministic noise positions
479
+ var _seed = 42;
480
+ function sRand() { _seed=(_seed*16807)%2147483647; return(_seed-1)/2147483646; }
481
+ function gRand(m,sd) { var u1=sRand(),u2=sRand(); return m+sd*Math.sqrt(-2*Math.log(Math.max(u1,.001)))*Math.cos(6.2832*u2); }
482
+
483
+ var NP = 24;
484
+ var pts = [];
485
+ for (var i=0;i<NP;i++) {
486
+ var frac = i/(NP-1);
487
+ var tgtX = aLeft+8*s + frac*(aW-16*s);
488
+ var tgtY = aCY + Math.sin(frac*Math.PI*2.5+0.5)*aH*0.3 * (0.7+0.3*Math.cos(frac*Math.PI));
489
+ var nX = gRand(aLeft+aW*0.35, aW*0.18);
490
+ var nY = gRand(aCY, aH*0.25);
491
+ nX = Math.max(aLeft+3, Math.min(aRight-3, nX));
492
+ nY = Math.max(aTop+3, Math.min(aBot-3, nY));
493
+ tgtY = Math.max(aTop+3, Math.min(aBot-3, tgtY));
494
+ pts.push({nX:nX, nY:nY, tX:tgtX, tY:tgtY});
495
+ }
496
+
497
+ var dots = g.selectAll(".fdot").data(pts).enter()
498
+ .append("circle").attr("r",2.5*s).attr("class","fdot");
499
+
500
+ // Trajectory line (appears as dots organize)
501
+ var trajPath = g.append("path").attr("fill","none")
502
+ .attr("stroke",t.actionBorder).attr("stroke-width",1.2*s).attr("opacity",0);
503
+ var trajData = pts.map(function(p){return[p.tX,p.tY]});
504
+ var lineGen = d3.line().curve(d3.curveCatmullRom.alpha(0.5));
505
+ trajPath.attr("d", lineGen(trajData));
506
+
507
+ var noiseC = t.noiseBorder, actionC = t.actionBorder;
508
+ var numSteps = 10;
509
+ var stepDur = 280, pauseEnd = 1000, pauseStart = 600;
510
+ var cycle = pauseStart + numSteps*stepDur + pauseEnd;
511
+ var t0 = null;
512
+
513
+ function animLoop(ts) {
514
+ if(!t0) t0=ts;
515
+ var el = (ts-t0)%cycle;
516
+ var step, tau;
517
+ if (el < pauseStart) { step = 0; tau = 0; }
518
+ else if (el < pauseStart + numSteps*stepDur) {
519
+ step = Math.min(numSteps, Math.floor((el-pauseStart)/stepDur) + 1);
520
+ tau = step / numSteps;
521
+ } else { step = numSteps; tau = 1; }
522
+
523
+ dots.attr("cx",function(d){return d.nX+(d.tX-d.nX)*tau})
524
+ .attr("cy",function(d){return d.nY+(d.tY-d.nY)*tau})
525
+ .attr("fill", d3.interpolateRgb(noiseC, actionC)(tau))
526
+ .attr("opacity", 0.85);
527
+ trajPath.attr("opacity", tau*0.45);
528
+ stepText.text("step "+step+"/10");
529
+
530
+ _pi05AnimId = requestAnimationFrame(animLoop);
531
+ }
532
+ _pi05AnimId = requestAnimationFrame(animLoop);
533
+
534
+ hoverBox(g, x3, imgY, colAeW, aeH2, "action_expert");
535
+
536
+ // Output
537
+ box(g, x4, imgY, colOutW, aeH2, t.actionBox, t.actionBorder, 6);
538
+ label(g, x4+colOutW/2, imgY+aeH2/2-12*s, "30 Actions", fsBold, t.actionLabel, "middle", 700);
539
+ label(g, x4+colOutW/2, imgY+aeH2/2, "16D", fsSmall, t.actionBorder);
540
+ label(g, x4+colOutW/2, imgY+aeH2/2+14*s, "joint angles +", fsSmall, t.subtext);
541
+ label(g, x4+colOutW/2, imgY+aeH2/2+26*s, "gripper cmds", fsSmall, t.subtext);
542
+ hoverBox(g, x4, imgY, colOutW, aeH2, "actions");
543
+
544
+ arrow(g, x3+colAeW, imgY+aeH2/2, x4, imgY+aeH2/2, t.actionBorder);
545
+ arrowLabel(g, (x3+colAeW+x4)/2, imgY+aeH2/2-8*s, "actions", fsSmall, t.subtext);
546
+
547
+ const H = langY + rowH + pad;
548
  svg.attr("width",W).attr("height",H).attr("viewBox",`0 0 ${W} ${H}`);
549
  }
550
 
 
552
  var t = T();
553
  const W = container.clientWidth || 800;
554
  const g = svg.append("g");
555
+ const s = Math.min(1.1, W / 740);
556
+ const pad = 16;
557
+ const rowH = 42*s;
558
+ const gap = 16*s;
559
+ const arrowGap = 14*s;
560
+ const fs = 12*s;
561
+ const fsSmall = 10*s;
562
+ const fsBold = 13*s;
563
+
564
+ const colInW = W*0.14;
565
+ const colEncW = W*0.11;
566
+ const colStageW = W*0.175;
567
+ const colSubW = W*0.175;
568
+ const colOutW = W*0.14;
569
+
570
+ const x0 = pad;
571
+ const x1 = x0+colInW+arrowGap*2;
572
+ const x2 = x1+colEncW+arrowGap*2;
573
+ const x3 = x2+colStageW+arrowGap*2;
574
+ const x4 = x3+colSubW+arrowGap*2;
575
+
576
+ const row1Y = pad;
577
+ const row2Y = row1Y+rowH+gap;
578
+ const row3Y = row2Y+rowH+gap;
579
+
580
+ // inputs
581
+ box(g, x0, row1Y, colInW, rowH, t.obsBox, t.obsBorder, 6);
582
+ label(g, x0+colInW/2, row1Y+12*s, "8 RGB Frames", fsBold, t.obsLabel, "middle", 600);
583
+ label(g, x0+colInW/2, row1Y+24*s, "sampled 1s apart", fsSmall, t.obsInnerText);
584
+ hoverBox(g, x0, row1Y, colInW, rowH, "embed_video");
585
+
586
+ box(g, x0, row2Y, colInW, rowH, t.obsBox, t.obsBorder, 6);
587
+ label(g, x0+colInW/2, row2Y+12*s, "Task Description", fsBold, t.obsLabel, "middle", 600);
588
+ label(g, x0+colInW/2, row2Y+24*s, "\"fold the t-shirt\"", fsSmall, t.obsInnerText);
589
+ hoverBox(g, x0, row2Y, colInW, rowH, "embed_text");
590
+
591
+ box(g, x0, row3Y, colInW, rowH, t.obsBox, t.obsBorder, 6);
592
+ label(g, x0+colInW/2, row3Y+12*s, "Joint State", fsBold, t.obsLabel, "middle", 600);
593
+ label(g, x0+colInW/2, row3Y+24*s, "16D positions", fsSmall, t.obsInnerText);
594
+ hoverBox(g, x0, row3Y, colInW, rowH, "embed_state");
595
+
596
+ // CLIP encoder
597
+ const clipH = row2Y+rowH-row1Y;
598
+ box(g, x1, row1Y, colEncW, clipH, t.sigBox, t.sigBorder, 6);
599
+ label(g, x1+colEncW/2, row1Y+clipH/2-5*s, "CLIP", fsBold, t.sigLabel, "middle", 700);
600
+ label(g, x1+colEncW/2, row1Y+clipH/2+7*s, "frozen", fsSmall, t.sigSub);
601
+ hoverBox(g, x1, row1Y, colEncW, clipH, "clip");
602
+
603
+ arrow(g, x0+colInW, row1Y+rowH/2, x1, row1Y+rowH/2, t.obsBorder);
604
+ arrowLabel(g, (x0+colInW+x1)/2, row1Y+rowH/2-8*s, "pixels", fsSmall, t.subtext);
605
+
606
+ arrow(g, x0+colInW, row2Y+rowH/2, x1, row2Y+rowH/2, t.obsBorder);
607
+ arrowLabel(g, (x0+colInW+x1)/2, row2Y+rowH/2-8*s, "text", fsSmall, t.subtext);
608
+
609
+ // Stage Transformer
610
+ const stH2 = row3Y+rowH-row1Y;
611
+ box(g, x2, row1Y, colStageW, stH2, t.sarmBox, t.stageBorder, 6);
612
+ label(g, x2+colStageW/2, row1Y+14*s, "Stage", fsBold, t.stageText, "middle", 700);
613
+ label(g, x2+colStageW/2, row1Y+26*s, "Transformer", fsBold, t.stageText, "middle", 700);
614
+ label(g, x2+colStageW/2, row1Y+stH2/2+6*s, "Classifies which", fsSmall, t.subtext);
615
+ label(g, x2+colStageW/2, row1Y+stH2/2+18*s, "subtask the robot", fsSmall, t.subtext);
616
+ label(g, x2+colStageW/2, row1Y+stH2/2+30*s, "is currently in", fsSmall, t.subtext);
617
+ hoverBox(g, x2, row1Y, colStageW, stH2, "stage_tfm");
618
+
619
+ arrow(g, x1+colEncW, row1Y+clipH/2, x2, row1Y+clipH/2, t.sigBorder);
620
+ arrowLabel(g, (x1+colEncW+x2)/2, row1Y+clipH/2-8*s, "embeddings", fsSmall, t.subtext);
621
+
622
+ arrow(g, x0+colInW, row3Y+rowH/2, x2, row3Y+rowH/2, t.obsBorder);
623
+ arrowLabel(g, (x0+colInW+x2)/2, row3Y+rowH/2-8*s, "state vector", fsSmall, t.subtext);
624
+
625
+ // Subtask Transformer
626
+ box(g, x3, row1Y, colSubW, stH2, t.sarmBox, t.stageBorder, 6);
627
+ label(g, x3+colSubW/2, row1Y+14*s, "Subtask", fsBold, t.stageText, "middle", 700);
628
+ label(g, x3+colSubW/2, row1Y+26*s, "Transformer", fsBold, t.stageText, "middle", 700);
629
+ label(g, x3+colSubW/2, row1Y+stH2/2+6*s, "Predicts progress", fsSmall, t.subtext);
630
+ label(g, x3+colSubW/2, row1Y+stH2/2+18*s, "within the stage", fsSmall, t.subtext);
631
+ label(g, x3+colSubW/2, row1Y+stH2/2+30*s, "τ ∈ [0, 1]", fsSmall, t.subtext);
632
+ hoverBox(g, x3, row1Y, colSubW, stH2, "subtask_tfm");
633
+
634
+ arrow(g, x2+colStageW, row1Y+stH2*0.3, x3, row1Y+stH2*0.3, t.stageBorder);
635
+ arrowLabel(g, (x2+colStageW+x3)/2, row1Y+stH2*0.3-8*s, "stage (one-hot)", fsSmall, t.subtext);
636
+
637
+ // feed inputs into subtask too
638
+ curvedArrow(g, x1+colEncW/2, row1Y+clipH, x1+colEncW/2, row3Y+rowH+16*s, x3+colSubW*0.3, row1Y+stH2, t.obsBorder, true);
639
+ arrowLabel(g, x1+colEncW/2-2*s, row3Y+rowH+10*s, "same inputs", fsSmall, t.subtext, "end");
640
+
641
+ // output
642
+ box(g, x4, row1Y, colOutW, stH2, t.rwBox, t.rwBorder, 6);
643
+ label(g, x4+colOutW/2, row1Y+stH2/2-20*s, "Progress", fsBold, t.rwLabel, "middle", 700);
644
+ label(g, x4+colOutW/2, row1Y+stH2/2-6*s, "Score", fsBold, t.rwLabel, "middle", 700);
645
+ label(g, x4+colOutW/2, row1Y+stH2/2+10*s, "stage + τ", fs, t.rwSub);
646
+ label(g, x4+colOutW/2, row1Y+stH2/2+24*s, "normalized", fs, t.rwSub);
647
+ label(g, x4+colOutW/2, row1Y+stH2/2+36*s, "to [0, 1]", fs, t.rwSub);
648
+ hoverBox(g, x4, row1Y, colOutW, stH2, "sarm_reward");
649
+
650
+ arrow(g, x3+colSubW, row1Y+stH2*0.5, x4, row1Y+stH2*0.5, t.stageBorder);
651
+ arrowLabel(g, (x3+colSubW+x4)/2, row1Y+stH2*0.5-8*s, "τ value", fsSmall, t.subtext);
652
+
653
+ // uses row below
654
+ const usesY = row3Y+rowH+gap+8*s;
655
+ const useW = Math.min((W-pad*2)*0.5, 240*s);
656
+ const ux = W/2-useW/2;
657
+ box(g, ux, usesY, useW, 34*s, t.scoreBox, t.wsBorder, 6);
658
+ label(g, ux+useW/2, usesY+13*s, "RABC Training Weights", fsBold, t.scoreText, "middle", 600);
659
+ label(g, ux+useW/2, usesY+26*s, "Weight actions by progress delta", fsSmall, t.wsBorder);
660
+ hoverBox(g, ux, usesY, useW, 34*s, "rabc");
661
+ arrow(g, x4+colOutW/2, row1Y+stH2, ux+useW/2, usesY, t.rwBorder);
662
+ arrowLabel(g, (x4+colOutW/2+ux+useW/2)/2+12*s, row1Y+stH2+10*s, "used for ↓", fsSmall, t.subtext);
663
+
664
+ const H = usesY + 34*s + pad;
665
  svg.attr("width",W).attr("height",H).attr("viewBox",`0 0 ${W} ${H}`);
666
  }
667
 
668
  function render() {
669
+ if (_pi05AnimId) { cancelAnimationFrame(_pi05AnimId); _pi05AnimId = null; }
670
  svg.selectAll("*").remove();
671
  applyTabTheme();
672
  if (currentTab === "system") renderSystem();
 
674
  else if (currentTab === "sarm") renderSARM();
675
  }
676
 
677
+ if (tabBar) {
678
+ tabBar.querySelectorAll(".tab-btn").forEach(btn => {
679
+ btn.addEventListener("click", () => {
680
+ tabBar.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
681
+ btn.classList.add("active");
682
+ currentTab = btn.dataset.tab;
683
+ render();
684
+ });
685
  });
686
+ }
687
 
688
  render();
689
  window.addEventListener("resize", render);
 
697
  if (typeof d3 !== "undefined") {
698
  _initArchViz();
699
  } else {
700
+ var _s = document.createElement("script");
701
+ _s.src = "https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js";
702
+ _s.onload = _initArchViz;
703
+ document.head.appendChild(_s);
704
  }
705
  </script>
706
  </body>
app/src/content/embeds/folding/rabc-explainer.html ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <style>
7
+ :root { --bg: transparent; --text: #e8eaf0; --subtext: #8b8fa8; --grid: rgba(255,255,255,0.08); }
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { background: var(--bg); font-family: system-ui, sans-serif; color: var(--text); }
10
+ .controls {
11
+ display: flex; align-items: center; gap: 14px; justify-content: center;
12
+ margin: 0 0 4px; font-size: 14px; flex-wrap: wrap;
13
+ }
14
+ .controls label { color: var(--subtext); font-weight: 600; }
15
+ .controls input[type=range] {
16
+ width: 140px; accent-color: #6366f1; cursor: pointer;
17
+ }
18
+ .kval { color: #e8eaf0; font-weight: 700; min-width: 48px; }
19
+ .axis text { fill: #8b8fa8; font-size: 12px; }
20
+ .axis path, .axis line { stroke: rgba(255,255,255,0.15); }
21
+ .grid line { stroke: var(--grid); }
22
+ .grid path { stroke: none; }
23
+ .legend-row {
24
+ display: flex; gap: 16px; justify-content: center; font-size: 13px;
25
+ color: var(--subtext); margin-top: 6px; flex-wrap: wrap;
26
+ }
27
+ .legend-row span { display: flex; align-items: center; gap: 5px; }
28
+ .legend-swatch { width: 12px; height: 4px; border-radius: 2px; flex-shrink: 0; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <div class="controls">
33
+ <label>κ threshold:</label>
34
+ <input type="range" id="kappa-slider" min="0" max="0.05" step="0.001" value="0.015"/>
35
+ <span class="kval" id="kappa-val">0.015</span>
36
+ </div>
37
+ <svg id="rabc-svg"></svg>
38
+ <div class="legend-row">
39
+ <span><span class="legend-swatch" style="background:#22d3ee"></span>SARM progress</span>
40
+ <span><span class="legend-swatch" style="background:#10b981"></span>Full weight (δ &gt; κ)</span>
41
+ <span><span class="legend-swatch" style="background:#eab308"></span>Soft weight (0 ≤ δ ≤ κ)</span>
42
+ <span><span class="legend-swatch" style="background:#ef4444"></span>Zero weight (δ &lt; 0)</span>
43
+ </div>
44
+ <script>
45
+ function _initRABC() {
46
+
47
+ // Simulated SARM progress for a realistic folding episode
48
+ const N = 120;
49
+ const progressRaw = [];
50
+ const seed = [
51
+ 0,0.01,0.02,0.03,0.04,0.06,0.08,0.10,0.12,0.14,
52
+ 0.16,0.18,0.20,0.22,0.24,0.26,0.28,0.28,0.27,0.26,
53
+ 0.26,0.27,0.28,0.30,0.32,0.34,0.36,0.38,0.40,0.42,
54
+ 0.44,0.45,0.45,0.45,0.44,0.44,0.45,0.46,0.48,0.50,
55
+ 0.52,0.54,0.56,0.58,0.60,0.62,0.64,0.66,0.68,0.70,
56
+ 0.72,0.73,0.74,0.74,0.73,0.72,0.71,0.70,0.70,0.71,
57
+ 0.72,0.74,0.76,0.78,0.80,0.81,0.82,0.83,0.84,0.85,
58
+ 0.85,0.85,0.86,0.86,0.87,0.88,0.89,0.90,0.91,0.92,
59
+ 0.93,0.93,0.93,0.93,0.94,0.94,0.95,0.95,0.96,0.96,
60
+ 0.96,0.97,0.97,0.97,0.98,0.98,0.98,0.98,0.99,0.99,
61
+ 0.99,0.99,0.99,1.0,1.0,1.0,1.0,1.0,1.0,1.0,
62
+ 1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0
63
+ ];
64
+ for (let i = 0; i < N; i++) progressRaw.push(seed[i]);
65
+
66
+ const CHUNK = 5;
67
+
68
+ function computeDeltas(progress) {
69
+ const deltas = [];
70
+ for (let i = 0; i < progress.length; i++) {
71
+ const j = Math.min(i + CHUNK, progress.length - 1);
72
+ deltas.push(progress[j] - progress[i]);
73
+ }
74
+ return deltas;
75
+ }
76
+
77
+ function computeWeights(deltas, kappa) {
78
+ const posDeltas = deltas.filter(d => d >= 0);
79
+ let mu = posDeltas.length ? posDeltas.reduce((a,b)=>a+b,0)/posDeltas.length : 0;
80
+ mu = Math.max(mu, 0);
81
+ const variance = posDeltas.length > 1
82
+ ? posDeltas.reduce((s,d)=>s+(d-mu)*(d-mu),0)/(posDeltas.length-1) : 0.0001;
83
+ const sigma = Math.sqrt(variance);
84
+
85
+ return deltas.map(d => {
86
+ if (d > kappa) return 1.0;
87
+ if (d < 0) return 0.0;
88
+ const lo = mu - 2*sigma, hi = mu + 2*sigma;
89
+ const range = hi - lo;
90
+ if (range < 0.0001) return d >= 0 ? 1 : 0;
91
+ return Math.max(0, Math.min(1, (d - lo) / range));
92
+ });
93
+ }
94
+
95
+ const container = document.getElementById("rabc-svg").parentElement;
96
+ const svg = d3.select("#rabc-svg");
97
+
98
+ function render() {
99
+ svg.selectAll("*").remove();
100
+ const kappa = parseFloat(document.getElementById("kappa-slider").value);
101
+ document.getElementById("kappa-val").textContent = kappa.toFixed(3);
102
+
103
+ const deltas = computeDeltas(progressRaw);
104
+ const weights = computeWeights(deltas, kappa);
105
+
106
+ const W = container.clientWidth || 700;
107
+ const margin = { top: 12, right: 16, bottom: 56, left: 46 };
108
+ const totalH = Math.max(260, Math.min(360, W * 0.42));
109
+ const progressH = totalH * 0.55;
110
+ const gap = 24;
111
+ const weightH = totalH - progressH - gap;
112
+ const H = totalH + margin.top + margin.bottom;
113
+ const w = W - margin.left - margin.right;
114
+
115
+ svg.attr("width", W).attr("height", H);
116
+ const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
117
+
118
+ const x = d3.scaleLinear().domain([0, N-1]).range([0, w]);
119
+ const yP = d3.scaleLinear().domain([0, 1]).range([progressH, 0]);
120
+ const yW = d3.scaleLinear().domain([0, 1]).range([weightH, 0]);
121
+ const wG = g.append("g").attr("transform", `translate(0,${progressH + gap})`);
122
+
123
+ // Progress grid + axis
124
+ g.append("g").attr("class","grid")
125
+ .call(d3.axisLeft(yP).ticks(4).tickSize(-w).tickFormat(""));
126
+ g.append("g").attr("class","axis")
127
+ .call(d3.axisLeft(yP).ticks(4).tickFormat(d3.format(".1f")));
128
+ g.append("text").attr("x",-progressH/2).attr("y",-36).attr("transform","rotate(-90)")
129
+ .attr("text-anchor","middle").attr("fill","#8b8fa8").attr("font-size",12).text("SARM progress");
130
+
131
+ // Weight grid + axis
132
+ wG.append("g").attr("class","grid")
133
+ .call(d3.axisLeft(yW).ticks(3).tickSize(-w).tickFormat(""));
134
+ wG.append("g").attr("class","axis")
135
+ .call(d3.axisLeft(yW).ticks(3).tickFormat(d3.format(".1f")));
136
+ wG.append("text").attr("x",-weightH/2).attr("y",-36).attr("transform","rotate(-90)")
137
+ .attr("text-anchor","middle").attr("fill","#8b8fa8").attr("font-size",12).text("Training weight");
138
+
139
+ // Shared x axis
140
+ wG.append("g").attr("class","axis").attr("transform",`translate(0,${weightH})`)
141
+ .call(d3.axisBottom(x).ticks(6).tickFormat(d => d + ""));
142
+ wG.append("text").attr("x",w/2).attr("y",weightH+30).attr("text-anchor","middle")
143
+ .attr("fill","#8b8fa8").attr("font-size",12).text("Timestep");
144
+
145
+ // Color weight bars
146
+ const barW = Math.max(1, w / N - 0.5);
147
+ weights.forEach((wt, i) => {
148
+ const delta = deltas[i];
149
+ let col;
150
+ if (delta < 0) col = "#ef4444";
151
+ else if (delta > kappa) col = "#10b981";
152
+ else col = "#eab308";
153
+
154
+ wG.append("rect")
155
+ .attr("x", x(i) - barW/2).attr("y", yW(wt))
156
+ .attr("width", barW).attr("height", Math.max(0, weightH - yW(wt)))
157
+ .attr("fill", col).attr("opacity", 0.8);
158
+ });
159
+
160
+ // Progress curve background shading
161
+ for (let i = 0; i < N - 1; i++) {
162
+ const delta = deltas[i];
163
+ let col;
164
+ if (delta < 0) col = "#ef4444";
165
+ else if (delta > kappa) col = "#10b981";
166
+ else col = "#eab308";
167
+
168
+ g.append("rect")
169
+ .attr("x", x(i)).attr("y", 0)
170
+ .attr("width", Math.max(1, x(i+1) - x(i))).attr("height", progressH)
171
+ .attr("fill", col).attr("opacity", 0.07);
172
+ }
173
+
174
+ // Progress line
175
+ const line = d3.line().x((d,i) => x(i)).y(d => yP(d)).curve(d3.curveMonotoneX);
176
+ g.append("path").datum(progressRaw)
177
+ .attr("fill","none").attr("stroke","#22d3ee").attr("stroke-width",2.5).attr("opacity",0.9)
178
+ .attr("d", line);
179
+
180
+ }
181
+
182
+ render();
183
+ document.getElementById("kappa-slider").addEventListener("input", render);
184
+ window.addEventListener("resize", render);
185
+ }
186
+
187
+ if (typeof d3 !== "undefined") { _initRABC(); }
188
+ else { var s = document.createElement("script"); s.src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"; s.onload=_initRABC; document.head.appendChild(s); }
189
+ </script>
190
+ </body>
191
+ </html>
app/src/content/embeds/folding/rtc-explainer.html ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <style>
7
+ :root { --bg: transparent; --text: #e8eaf0; --subtext: #8b8fa8; }
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { background: var(--bg); font-family: system-ui, sans-serif; color: var(--text); }
10
+ .controls {
11
+ display: flex; align-items: center; gap: 10px; justify-content: center;
12
+ margin: 0 0 4px; font-size: 14px; flex-wrap: wrap;
13
+ }
14
+ .mode-toggle {
15
+ display: flex; border-radius: 6px; overflow: hidden; border: 1px solid #333;
16
+ }
17
+ .mode-btn {
18
+ padding: 6px 16px; font-size: 13px; font-weight: 600; cursor: pointer;
19
+ border: none; background: #1a1d2e; color: #666;
20
+ transition: all .2s; font-family: inherit;
21
+ }
22
+ .mode-btn.active { background: #252540; color: #e8eaf0; }
23
+ .mode-btn:hover:not(.active) { background: #1f2238; color: #999; }
24
+ .play-btn {
25
+ width: 26px; height: 26px; border-radius: 50%; border: 1px solid #444;
26
+ background: #1a1d2e; color: #e8eaf0; cursor: pointer; font-size: 11px;
27
+ display: flex; align-items: center; justify-content: center; font-family: inherit;
28
+ transition: background .2s;
29
+ }
30
+ .play-btn:hover { background: #252540; }
31
+ .time-display { font-weight: 700; font-size: 14px; min-width: 70px; text-align: right; }
32
+ .legend-row {
33
+ display: flex; gap: 14px; justify-content: center; font-size: 12px;
34
+ color: #8b8fa8; margin-top: 4px; flex-wrap: wrap;
35
+ }
36
+ .legend-row span { display: flex; align-items: center; gap: 4px; }
37
+ .legend-swatch { width: 12px; height: 8px; border-radius: 2px; flex-shrink: 0; }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div class="controls">
42
+ <div class="mode-toggle">
43
+ <button class="mode-btn active" data-mode="sync">Synchronous</button>
44
+ <button class="mode-btn" data-mode="rtc">RTC</button>
45
+ </div>
46
+ <button class="play-btn" id="play-btn">▶</button>
47
+ <input type="range" id="speed-slider" min="0.5" max="3" step="0.5" value="1" style="width:60px;accent-color:#6366f1;cursor:pointer;" title="Playback speed"/>
48
+ <span class="time-display" id="time-display"></span>
49
+ </div>
50
+ <svg id="rtc-svg"></svg>
51
+ <div class="legend-row" id="legend-row">
52
+ <span><span class="legend-swatch" style="background:#7c7fff"></span>Inference</span>
53
+ <span><span class="legend-swatch" style="background:#22d3ee"></span>Executing</span>
54
+ <span id="legend-idle"><span class="legend-swatch" style="background:transparent;border:1px dashed #555"></span>Idle</span>
55
+ </div>
56
+ <script>
57
+ function _initRTC() {
58
+
59
+ const CHUNK = 10, INF = 3, N_CHUNKS = 4;
60
+
61
+ function buildSync() {
62
+ const inf = [], exec = [], idle = [];
63
+ let t = 0;
64
+ for (let i = 0; i < N_CHUNKS; i++) {
65
+ inf.push({ s: t, e: t + INF, i: i });
66
+ idle.push({ s: t, e: t + INF });
67
+ t += INF;
68
+ exec.push({ s: t, e: t + CHUNK, i: i, lock: 0 });
69
+ t += CHUNK;
70
+ }
71
+ return { inf, exec, idle, T: t };
72
+ }
73
+
74
+ function buildRTC() {
75
+ const inf = [], exec = [], idle = [];
76
+ inf.push({ s: 0, e: INF, i: 0 });
77
+ idle.push({ s: 0, e: INF });
78
+ let t = INF;
79
+ for (let i = 0; i < N_CHUNKS; i++) {
80
+ exec.push({ s: t, e: t + CHUNK, i: i, lock: i > 0 ? INF : 0 });
81
+ if (i + 1 < N_CHUNKS) inf.push({ s: t, e: t + INF, i: i + 1 });
82
+ t += CHUNK - (i + 1 < N_CHUNKS ? INF : 0);
83
+ }
84
+ return { inf, exec, idle, T: exec[exec.length - 1].e };
85
+ }
86
+
87
+ const SYNC = buildSync(), RTC_ = buildRTC();
88
+ let mode = "sync", playing = false, playT = 0, speed = 1, animId = null, lastFrame = null;
89
+
90
+ const container = document.getElementById("rtc-svg").parentElement;
91
+ const svg = d3.select("#rtc-svg");
92
+ const playBtn = document.getElementById("play-btn");
93
+ const speedSlider = document.getElementById("speed-slider");
94
+ const timeDisp = document.getElementById("time-display");
95
+ const legendIdle = document.getElementById("legend-idle");
96
+ const legendRow = document.getElementById("legend-row");
97
+
98
+ const infPalette = ["#7c7fff", "#9590ff", "#7c7fff", "#9590ff"];
99
+ const execPalette = ["#22d3ee", "#14b8a6", "#06b6d4", "#0ea5e9"];
100
+ const lockColor = "#0c5e6f";
101
+
102
+ function data() { return mode === "sync" ? SYNC : RTC_; }
103
+
104
+ function render() {
105
+ svg.selectAll("*").remove();
106
+ const d = data();
107
+ const W = container.clientWidth || 600;
108
+ const ml = 72, mr = 12, mt = 6, mb = 24;
109
+ const laneH = 32, gap = 20;
110
+ const w = W - ml - mr;
111
+ const H = mt + laneH * 2 + gap + mb;
112
+
113
+ svg.attr("width", W).attr("height", H).attr("viewBox", `0 0 ${W} ${H}`);
114
+ const g = svg.append("g").attr("transform", `translate(${ml},${mt})`);
115
+ const maxT = Math.max(SYNC.T, RTC_.T);
116
+ const x = d3.scaleLinear().domain([0, maxT]).range([0, w]);
117
+ const iy = 0, ey = laneH + gap;
118
+
119
+ g.append("rect").attr("x", 0).attr("y", iy).attr("width", w).attr("height", laneH)
120
+ .attr("fill", "#ffffff").attr("opacity", 0.02).attr("rx", 4);
121
+ g.append("rect").attr("x", 0).attr("y", ey).attr("width", w).attr("height", laneH)
122
+ .attr("fill", "#ffffff").attr("opacity", 0.02).attr("rx", 4);
123
+
124
+ g.append("text").attr("x", -10).attr("y", iy + laneH / 2)
125
+ .attr("text-anchor", "end").attr("dominant-baseline", "middle")
126
+ .attr("fill", "#8b8fa8").attr("font-size", 12).attr("font-weight", 500).text("Predict");
127
+ g.append("text").attr("x", -10).attr("y", ey + laneH / 2)
128
+ .attr("text-anchor", "end").attr("dominant-baseline", "middle")
129
+ .attr("fill", "#8b8fa8").attr("font-size", 12).attr("font-weight", 500).text("Execute");
130
+
131
+ d.idle.forEach(id => {
132
+ const bx = x(id.s), bw = x(id.e) - x(id.s);
133
+ g.append("rect").attr("x", bx + 1).attr("y", ey + 3).attr("width", Math.max(0, bw - 2)).attr("height", laneH - 6)
134
+ .attr("fill", "none").attr("stroke", "#444").attr("stroke-width", 1)
135
+ .attr("stroke-dasharray", "4,3").attr("rx", 3);
136
+ });
137
+
138
+ d.inf.forEach(b => {
139
+ const bx = x(b.s), bw = x(b.e) - x(b.s);
140
+ const col = infPalette[b.i % infPalette.length];
141
+ g.append("rect").attr("x", bx).attr("y", iy + 3).attr("width", bw).attr("height", laneH - 6)
142
+ .attr("fill", col).attr("opacity", 0.9).attr("rx", 4);
143
+
144
+ const connX = bx + bw / 2;
145
+ const execBlock = d.exec.find(e => e.i === b.i);
146
+ if (execBlock) {
147
+ const targetX = x(execBlock.s) + (x(execBlock.e) - x(execBlock.s)) / 2;
148
+ g.append("line")
149
+ .attr("x1", connX).attr("y1", iy + laneH - 2)
150
+ .attr("x2", targetX).attr("y2", ey + 2)
151
+ .attr("stroke", col).attr("stroke-width", 1)
152
+ .attr("stroke-dasharray", "2,3").attr("opacity", 0.3);
153
+ }
154
+ });
155
+
156
+ d.exec.forEach(b => {
157
+ const bx = x(b.s), bw = x(b.e) - x(b.s);
158
+ const col = execPalette[b.i % execPalette.length];
159
+
160
+ if (b.lock > 0) {
161
+ const lw = x(b.s + b.lock) - bx;
162
+ g.append("rect").attr("x", bx).attr("y", ey + 3).attr("width", bw).attr("height", laneH - 6)
163
+ .attr("fill", col).attr("opacity", 0.85).attr("rx", 4);
164
+ g.append("rect").attr("x", bx).attr("y", ey + 3).attr("width", lw).attr("height", laneH - 6)
165
+ .attr("fill", lockColor).attr("opacity", 0.95).attr("rx", 4);
166
+ for (let sx = bx + 3; sx < bx + lw - 1; sx += 5) {
167
+ g.append("line").attr("x1", sx).attr("y1", ey + 3)
168
+ .attr("x2", sx + (laneH - 6) * 0.4).attr("y2", ey + laneH - 3)
169
+ .attr("stroke", "#1a8a9e").attr("stroke-width", 0.8).attr("opacity", 0.5);
170
+ }
171
+ g.append("line").attr("x1", bx + lw).attr("y1", ey + 5)
172
+ .attr("x2", bx + lw).attr("y2", ey + laneH - 5)
173
+ .attr("stroke", "#fff").attr("stroke-width", 0.5).attr("opacity", 0.3);
174
+ } else {
175
+ g.append("rect").attr("x", bx).attr("y", ey + 3).attr("width", bw).attr("height", laneH - 6)
176
+ .attr("fill", col).attr("opacity", 0.85).attr("rx", 4);
177
+ }
178
+
179
+ if (bw > 30) {
180
+ g.append("text").attr("x", bx + bw / 2).attr("y", ey + laneH / 2)
181
+ .attr("text-anchor", "middle").attr("dominant-baseline", "middle")
182
+ .attr("fill", "#fff").attr("font-size", 12).attr("font-weight", 600)
183
+ .attr("opacity", 0.9).text("C" + (b.i + 1));
184
+ }
185
+ });
186
+
187
+ const ticks = [];
188
+ const step = maxT > 30 ? 10 : 5;
189
+ for (let t = 0; t <= maxT; t += step) ticks.push(t);
190
+ ticks.forEach(t => {
191
+ const tx = x(t);
192
+ g.append("line").attr("x1", tx).attr("y1", ey + laneH + 2).attr("x2", tx).attr("y2", ey + laneH + 5)
193
+ .attr("stroke", "#555").attr("stroke-width", 0.5);
194
+ g.append("text").attr("x", tx).attr("y", ey + laneH + 14)
195
+ .attr("text-anchor", "middle").attr("fill", "#666").attr("font-size", 11)
196
+ .text(Math.round(t * 33) + "ms");
197
+ });
198
+
199
+ const playhead = g.append("line")
200
+ .attr("x1", x(playT)).attr("x2", x(playT))
201
+ .attr("y1", -2).attr("y2", ey + laneH + 2)
202
+ .attr("stroke", "#f87171").attr("stroke-width", 1.5).attr("opacity", playing || playT > 0 ? 0.9 : 0);
203
+ g.append("circle").attr("cx", x(playT)).attr("cy", -2).attr("r", 3)
204
+ .attr("fill", "#f87171").attr("opacity", playing || playT > 0 ? 0.9 : 0).attr("class", "ph-dot");
205
+
206
+ const totalMs = Math.round(d.T * 33);
207
+ const syncMs = Math.round(SYNC.T * 33);
208
+ if (mode === "rtc") {
209
+ const pct = Math.round((syncMs - totalMs) / syncMs * 100);
210
+ timeDisp.innerHTML = `<span style="color:#10b981">${totalMs}ms</span> <span style="color:#666">(${pct}% faster)</span>`;
211
+ } else {
212
+ timeDisp.innerHTML = `<span style="color:#666">${totalMs}ms</span>`;
213
+ }
214
+
215
+ if (mode === "rtc") {
216
+ legendIdle.style.display = "none";
217
+ if (!document.getElementById("legend-locked")) {
218
+ const sp = document.createElement("span");
219
+ sp.id = "legend-locked";
220
+ sp.innerHTML = `<span class="legend-swatch" style="background:${lockColor}"></span>Committed`;
221
+ legendRow.appendChild(sp);
222
+ }
223
+ } else {
224
+ legendIdle.style.display = "";
225
+ const lk = document.getElementById("legend-locked");
226
+ if (lk) lk.remove();
227
+ }
228
+
229
+ window._ph = { line: playhead, dot: g.select(".ph-dot"), x };
230
+ }
231
+
232
+ function animate(ts) {
233
+ if (!playing) return;
234
+ if (!lastFrame) lastFrame = ts;
235
+ const dt = (ts - lastFrame) / 1000;
236
+ lastFrame = ts;
237
+ playT += dt * speed * 8;
238
+ if (playT > data().T) { playT = 0; lastFrame = null; }
239
+ if (window._ph) {
240
+ const px = window._ph.x(playT);
241
+ window._ph.line.attr("x1", px).attr("x2", px).attr("opacity", 0.9);
242
+ window._ph.dot.attr("cx", px).attr("opacity", 0.9);
243
+ }
244
+ animId = requestAnimationFrame(animate);
245
+ }
246
+
247
+ playBtn.addEventListener("click", () => {
248
+ playing = !playing;
249
+ playBtn.textContent = playing ? "⏸" : "▶";
250
+ if (playing) { lastFrame = null; animId = requestAnimationFrame(animate); }
251
+ else cancelAnimationFrame(animId);
252
+ });
253
+ speedSlider.addEventListener("input", () => {
254
+ speed = parseFloat(speedSlider.value);
255
+ });
256
+
257
+ document.querySelectorAll(".mode-btn").forEach(btn => {
258
+ btn.addEventListener("click", () => {
259
+ document.querySelectorAll(".mode-btn").forEach(b => b.classList.remove("active"));
260
+ btn.classList.add("active");
261
+ mode = btn.dataset.mode;
262
+ playT = 0;
263
+ render();
264
+ });
265
+ });
266
+
267
+ render();
268
+ window.addEventListener("resize", render);
269
+ }
270
+
271
+ if (typeof d3 !== "undefined") { _initRTC(); }
272
+ else { var s = document.createElement("script"); s.src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"; s.onload=_initRTC; document.head.appendChild(s); }
273
+ </script>
274
+ </body>
275
+ </html>
app/src/content/embeds/folding/statistical-analysis.html CHANGED
@@ -9,26 +9,26 @@ body{background:transparent;font-family:system-ui,sans-serif;color:#e8eaf0}
9
  .wrap{max-width:980px;margin:0 auto;padding:20px 20px 36px}
10
 
11
  .card{background:#1a1d27;border:1px solid #2a2d3a;border-radius:6px;overflow:visible;margin-bottom:12px}
12
- .card-head{padding:9px 14px;border-bottom:1px solid #2a2d3a;font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#8b8fa8;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}
13
  .chart-area{padding:16px 16px 10px}
14
  svg text{font-family:system-ui,sans-serif}
15
 
16
- .insight{background:#12151f;border:1px solid #2a2d3a;border-left:3px solid #f7934f;border-radius:4px;padding:9px 13px;margin-bottom:12px;font-size:10px;color:#8b8fa8;line-height:1.7}
17
  .insight strong{color:#e8eaf0}
18
  .insight.blue{border-left-color:#60a5fa}
19
  .insight.green{border-left-color:#4dc98a}
20
  .insight.purple{border-left-color:#818cf8}
21
 
22
  .legend{display:flex;gap:12px;flex-wrap:wrap;padding:8px 14px;border-top:1px solid #2a2d3a}
23
- .li{display:flex;align-items:center;gap:6px;font-size:10px;color:#8b8fa8}
24
  .lsw{width:14px;height:3px;border-radius:1px}
25
 
26
- .ctrl-btn{padding:4px 10px;background:#1a1d27;border:1px solid #2a2d3a;border-radius:4px;color:#8b8fa8;font-family:inherit;font-size:9px;cursor:pointer;letter-spacing:.04em;transition:all .15s}
27
  .ctrl-btn:hover,.ctrl-btn.active{color:#e8eaf0;border-color:#4a4d5a;background:#252835}
28
 
29
- .note{font-size:10px;color:#555e7a;margin-top:10px;line-height:1.6}
30
  @media(prefers-color-scheme:dark){.note{color:#c8cad8}}
31
- .tooltip{position:fixed;background:#252835;border:1px solid #3a3d4a;border-radius:4px;padding:8px 11px;font-size:9px;color:#e8eaf0;pointer-events:none;opacity:0;transition:opacity .1s;z-index:100;line-height:1.7;max-width:240px}
32
  </style>
33
  </head>
34
  <body>
@@ -38,7 +38,7 @@ svg text{font-family:system-ui,sans-serif}
38
  </div>
39
  <div class="card">
40
  <div class="card-head">
41
- <span>Posterior success rate distributions — Beta(1+k, 1+n-k)</span>
42
  <div style="display:flex;gap:6px">
43
  <button class="ctrl-btn active" id="v-total" onclick="setVLevel('total')">Total</button>
44
  <button class="ctrl-btn" id="v-l1" onclick="setVLevel('L1')">Level 1</button>
@@ -47,8 +47,8 @@ svg text{font-family:system-ui,sans-serif}
47
  </div>
48
  <div class="chart-area" style="overflow:visible"><svg id="svg-violin" width="100%" height="500" style="overflow:visible"></svg></div>
49
  <div class="legend">
50
- <div class="li"><div class="lsw" style="background:#f7934f"></div>Series 1</div>
51
- <div class="li"><div class="lsw" style="background:#4dc98a"></div>Series 2</div>
52
  <div class="li" style="font-size:10px;color:#8b8fa8">Bold letter = CLD group (STEP, Bonferroni α=0.10/55, n<sub>max</sub>=50)</div>
53
  </div>
54
  </div>
@@ -78,7 +78,7 @@ const DATA = {
78
  // CLD assignments (STEP sequential test, two-sided, Bonferroni α=0.10/55, n_max=50)
79
  const CLD = {
80
  total: {'2.5 HQ+RABC+Rel★':'a','2.2 HQ+RABC+Rel':'ab','1.1 π0':'bc','1.7 Rel+RABC':'bc','2.1 HQ':'bc','1.3 Relative':'bc','1.2 π0.5':'cd','2.4 HQ chunk45':'cd','1.4 RABC low':'cd','2.3 HQ+mirror':'cd','1.5 RABC high':'d'},
81
- L1: {'2.2 HQ+RABC+Rel':'a','2.5 HQ+RABC+Rel★':'a','1.1 π0':'a','1.7 Rel+RABC':'a','1.3 Relative':'a','2.1 HQ':'a','1.2 π0.5':'ab','2.4 HQ chunk45':'ab','1.4 RABC low':'ab','1.5 RABC high':'b','2.3 HQ+mirror':'b'},
82
  L2: {'2.5 HQ+RABC+Rel★':'a','2.2 HQ+RABC+Rel':'ab','2.1 HQ':'ab','2.3 HQ+mirror':'ab','1.1 π0':'b','1.2 π0.5':'b','1.3 Relative':'b','1.4 RABC low':'b','1.5 RABC high':'b','1.7 Rel+RABC':'b','2.4 HQ chunk45':'b'},
83
  };
84
 
@@ -154,7 +154,7 @@ function drawViolin(){
154
  g.append('line').attr('x1',0).attr('x2',iW).attr('y1',y(0.5)).attr('y2',y(0.5))
155
  .attr('stroke','#3a3d4a').attr('stroke-dasharray','4,3').attr('opacity',.6);
156
  g.append('text').attr('x',iW-2).attr('y',y(0.5)-4).attr('text-anchor','end')
157
- .attr('fill','#3a3d4a').attr('font-size',8).text('50%');
158
 
159
  const tooltip=document.getElementById('tooltip');
160
 
@@ -199,11 +199,11 @@ function drawViolin(){
199
  // CLD letter
200
  const cldLetter=(CLD[vLevel]||CLD.total)[expId]||'?';
201
  g.append('text').attr('x',cx).attr('y',-8).attr('text-anchor','middle')
202
- .attr('fill',c).attr('font-size',11).attr('font-weight','bold').text(cldLetter);
203
 
204
  // SR fraction label
205
  g.append('text').attr('x',cx).attr('y',-22).attr('text-anchor','middle')
206
- .attr('fill',SUB).attr('font-size',9).text(`${k}/${n}`);
207
 
208
  // Hover
209
  const hitbox=g.append('rect').attr('x',bx).attr('y',0).attr('width',bw).attr('height',iH)
@@ -221,11 +221,11 @@ function drawViolin(){
221
  // Axes
222
  g.append('g').attr('transform',`translate(0,${iH})`)
223
  .call(d3.axisBottom(x).tickSize(0))
224
- .call(gg=>{gg.select('.domain').attr('stroke',BORDER);gg.selectAll('text').attr('fill',d=>seriesColor(DATA[d].series)).attr('font-size',9).attr('transform','rotate(-40)').attr('text-anchor','end').attr('dx','-0.5em').attr('dy','0.3em')});
225
  g.append('g').call(d3.axisLeft(y).ticks(5).tickFormat(d=>Math.round(d*100)+'%').tickSize(3))
226
- .call(gg=>{gg.select('.domain').attr('stroke',BORDER);gg.selectAll('text').attr('fill',SUB).attr('font-size',9);gg.selectAll('line').attr('stroke',BORDER)});
227
  g.append('text').attr('transform','rotate(-90)').attr('x',-iH/2).attr('y',-30).attr('text-anchor','middle')
228
- .attr('fill',SUB).attr('font-size',9).text('Posterior success rate');
229
  }
230
 
231
  window.setVLevel = setVLevel;
 
9
  .wrap{max-width:980px;margin:0 auto;padding:20px 20px 36px}
10
 
11
  .card{background:#1a1d27;border:1px solid #2a2d3a;border-radius:6px;overflow:visible;margin-bottom:12px}
12
+ .card-head{padding:9px 14px;border-bottom:1px solid #2a2d3a;font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:#8b8fa8;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}
13
  .chart-area{padding:16px 16px 10px}
14
  svg text{font-family:system-ui,sans-serif}
15
 
16
+ .insight{background:#12151f;border:1px solid #2a2d3a;border-left:3px solid #f7934f;border-radius:4px;padding:9px 13px;margin-bottom:12px;font-size:12px;color:#8b8fa8;line-height:1.7}
17
  .insight strong{color:#e8eaf0}
18
  .insight.blue{border-left-color:#60a5fa}
19
  .insight.green{border-left-color:#4dc98a}
20
  .insight.purple{border-left-color:#818cf8}
21
 
22
  .legend{display:flex;gap:12px;flex-wrap:wrap;padding:8px 14px;border-top:1px solid #2a2d3a}
23
+ .li{display:flex;align-items:center;gap:6px;font-size:12px;color:#8b8fa8}
24
  .lsw{width:14px;height:3px;border-radius:1px}
25
 
26
+ .ctrl-btn{padding:5px 12px;background:#1a1d27;border:1px solid #2a2d3a;border-radius:4px;color:#8b8fa8;font-family:inherit;font-size:11px;cursor:pointer;letter-spacing:.04em;transition:all .15s}
27
  .ctrl-btn:hover,.ctrl-btn.active{color:#e8eaf0;border-color:#4a4d5a;background:#252835}
28
 
29
+ .note{font-size:12px;color:#555e7a;margin-top:10px;line-height:1.6}
30
  @media(prefers-color-scheme:dark){.note{color:#c8cad8}}
31
+ .tooltip{position:fixed;background:#252835;border:1px solid #3a3d4a;border-radius:4px;padding:8px 11px;font-size:11px;color:#e8eaf0;pointer-events:none;opacity:0;transition:opacity .1s;z-index:100;line-height:1.7;max-width:240px}
32
  </style>
33
  </head>
34
  <body>
 
38
  </div>
39
  <div class="card">
40
  <div class="card-head">
41
+ <span>Posterior success rate distributions: Beta(1+k, 1+n-k)</span>
42
  <div style="display:flex;gap:6px">
43
  <button class="ctrl-btn active" id="v-total" onclick="setVLevel('total')">Total</button>
44
  <button class="ctrl-btn" id="v-l1" onclick="setVLevel('L1')">Level 1</button>
 
47
  </div>
48
  <div class="chart-area" style="overflow:visible"><svg id="svg-violin" width="100%" height="500" style="overflow:visible"></svg></div>
49
  <div class="legend">
50
+ <div class="li"><div class="lsw" style="background:#f7934f"></div>Initial (1.x)</div>
51
+ <div class="li"><div class="lsw" style="background:#4dc98a"></div>Fine-tuned (2.x)</div>
52
  <div class="li" style="font-size:10px;color:#8b8fa8">Bold letter = CLD group (STEP, Bonferroni α=0.10/55, n<sub>max</sub>=50)</div>
53
  </div>
54
  </div>
 
78
  // CLD assignments (STEP sequential test, two-sided, Bonferroni α=0.10/55, n_max=50)
79
  const CLD = {
80
  total: {'2.5 HQ+RABC+Rel★':'a','2.2 HQ+RABC+Rel':'ab','1.1 π0':'bc','1.7 Rel+RABC':'bc','2.1 HQ':'bc','1.3 Relative':'bc','1.2 π0.5':'cd','2.4 HQ chunk45':'cd','1.4 RABC low':'cd','2.3 HQ+mirror':'cd','1.5 RABC high':'d'},
81
+ L1: {'2.2 HQ+RABC+Rel':'a','2.5 HQ+RABC+Rel★':'a','1.1 π0':'a','1.7 Rel+RABC':'a','1.3 Relative':'a','2.1 HQ':'a','1.2 π0.5':'ab','2.4 HQ chunk45':'ab','1.4 RABC low':'ab','2.3 HQ+mirror':'b','1.5 RABC high':'b'},
82
  L2: {'2.5 HQ+RABC+Rel★':'a','2.2 HQ+RABC+Rel':'ab','2.1 HQ':'ab','2.3 HQ+mirror':'ab','1.1 π0':'b','1.2 π0.5':'b','1.3 Relative':'b','1.4 RABC low':'b','1.5 RABC high':'b','1.7 Rel+RABC':'b','2.4 HQ chunk45':'b'},
83
  };
84
 
 
154
  g.append('line').attr('x1',0).attr('x2',iW).attr('y1',y(0.5)).attr('y2',y(0.5))
155
  .attr('stroke','#3a3d4a').attr('stroke-dasharray','4,3').attr('opacity',.6);
156
  g.append('text').attr('x',iW-2).attr('y',y(0.5)-4).attr('text-anchor','end')
157
+ .attr('fill','#3a3d4a').attr('font-size',10).text('50%');
158
 
159
  const tooltip=document.getElementById('tooltip');
160
 
 
199
  // CLD letter
200
  const cldLetter=(CLD[vLevel]||CLD.total)[expId]||'?';
201
  g.append('text').attr('x',cx).attr('y',-8).attr('text-anchor','middle')
202
+ .attr('fill',c).attr('font-size',13).attr('font-weight','bold').text(cldLetter);
203
 
204
  // SR fraction label
205
  g.append('text').attr('x',cx).attr('y',-22).attr('text-anchor','middle')
206
+ .attr('fill',SUB).attr('font-size',11).text(`${k}/${n}`);
207
 
208
  // Hover
209
  const hitbox=g.append('rect').attr('x',bx).attr('y',0).attr('width',bw).attr('height',iH)
 
221
  // Axes
222
  g.append('g').attr('transform',`translate(0,${iH})`)
223
  .call(d3.axisBottom(x).tickSize(0))
224
+ .call(gg=>{gg.select('.domain').attr('stroke',BORDER);gg.selectAll('text').attr('fill',d=>seriesColor(DATA[d].series)).attr('font-size',11).attr('transform','rotate(-40)').attr('text-anchor','end').attr('dx','-0.5em').attr('dy','0.3em')});
225
  g.append('g').call(d3.axisLeft(y).ticks(5).tickFormat(d=>Math.round(d*100)+'%').tickSize(3))
226
+ .call(gg=>{gg.select('.domain').attr('stroke',BORDER);gg.selectAll('text').attr('fill',SUB).attr('font-size',11);gg.selectAll('line').attr('stroke',BORDER)});
227
  g.append('text').attr('transform','rotate(-90)').attr('x',-iH/2).attr('y',-30).attr('text-anchor','middle')
228
+ .attr('fill',SUB).attr('font-size',11).text('Posterior success rate');
229
  }
230
 
231
  window.setVLevel = setVLevel;
app/src/content/embeds/folding/subtask-heatmap.html CHANGED
@@ -16,86 +16,186 @@
16
  .tooltip strong { display: block; margin-bottom: 5px; }
17
  .tooltip-row { display: flex; justify-content: space-between; gap: 12px; margin-top: 3px; font-size: 12px; color: var(--subtext); }
18
  .tooltip-row span:last-child { color: var(--text); font-weight: 600; }
19
- .legend-bar { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 11px; color: var(--subtext); justify-content: center; }
20
  .legend-gradient { height: 10px; width: 180px; border-radius: 5px; flex-shrink: 0; }
21
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  </style>
23
  </head>
24
  <body>
 
 
 
25
  <div style="position:relative">
26
  <svg id="hm-chart"></svg>
27
  <div class="tooltip" id="hm-tooltip"></div>
28
  </div>
29
- <div class="legend-bar">
30
- <span>Fast (0s)</span>
31
  <canvas id="lgd" class="legend-gradient" width="180" height="10"></canvas>
32
- <span>Slow (≥120s)</span>
33
  </div>
34
  <script>
35
  function _initSubtaskHeatmap() {
36
  const rawData = [
37
- {label:"1.1 π0",series:"1",total_sr:40, times:[null, 19.2, 42.22, 14.33, 19.88, 27.25]},
38
- {label:"1.2 π0.5",series:"1",total_sr:20, times:[50, 39.27, 41.5, 12.3, 13.75, 10.75]},
39
- {label:"1.3 Relative",series:"1",total_sr:35, times:[null, 19.5, 44.2, 14.8, 30.33, 22.14]},
40
- {label:"1.4 RABC low",series:"1",total_sr:15, times:[null, 20.8, 36.62, 10.0, 18.8, 12.67]},
41
- {label:"1.5 RABC high",series:"1",total_sr:0, times:[240, 21.4, 100.0, null, null, null ]},
42
- {label:"1.7 Rel+RABC",series:"1",total_sr:40, times:[157.5,19.33, 32.64, 8.9, 11.0, 23.38]},
43
- {label:"2.1 HQ",series:"2",total_sr:40, times:[77.5, 11.08, 21.09, 5.45, 5.5, 11.5 ]},
44
- {label:"2.2 HQ+RABC+Rel",series:"2",total_sr:75, times:[34.33,6.25, 12.31, 3.75, 5.31, 8.93 ]},
45
- {label:"2.3 HQ+mirror",series:"2",total_sr:5, times:[49, 14.0, 23.71, 17.5, 11.0, 4.0 ]},
46
- {label:"2.4 HQ chunk45",series:"2",total_sr:20, times:[120, 10.09, 41.18, 7.89, 7.33, 10.0 ]},
47
- {label:"2.5 HQ+RABC+Rel★",series:"2",total_sr:90, times:[62.25,8.28, 12.0, 5.28, 5.22, 6.83 ]},
48
  ];
49
 
50
- // Sort rows: best → worst by total_sr (heatmap: top = best)
51
- const data = [...rawData].sort((a,b) => b.total_sr - a.total_sr);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  const subtasks = ["Unfold","Fold 1","Fold 2","Fold 3","Fold 4","Rotation"];
53
  const seriesColor = s => s === "2" ? "#f7934f" : "#4f8ef7";
54
 
55
- // Color: green (fast) → yellow → red (slow), capped at 120s
56
- const colorScale = d3.scaleSequential().domain([0, 120])
57
- .interpolator(t => {
58
- const stops = [[0,[46,200,138]],[0.4,[247,211,79]],[1,[220,60,60]]];
59
- for (let i=0; i<stops.length-1; i++) {
60
- const [t0,c0]=stops[i],[t1,c1]=stops[i+1];
61
- if (t<=t1) {
62
- const f=(t-t0)/(t1-t0);
63
- return `rgb(${Math.round(c0[0]+(c1[0]-c0[0])*f)},${Math.round(c0[1]+(c1[1]-c0[1])*f)},${Math.round(c0[2]+(c1[2]-c0[2])*f)})`;
64
- }
 
 
 
 
65
  }
66
- return "rgb(220,60,60)";
67
- });
 
 
 
 
68
 
69
- // Draw legend gradient
70
- const canvas = document.getElementById("lgd");
71
- const ctx = canvas.getContext("2d");
72
- for (let i=0; i<180; i++) { ctx.fillStyle=colorScale(i/180*120); ctx.fillRect(i,0,1,10); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  const margin = {top:12, right:16, bottom:36, left:120};
75
  const svg = d3.select("#hm-chart");
76
  const container = svg.node().parentElement;
77
  const tooltip = d3.select("#hm-tooltip");
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  function render() {
80
  svg.selectAll("*").remove();
 
81
  const W = container.clientWidth;
82
- const cellW = Math.floor((W - margin.left - margin.right) / subtasks.length);
 
83
  const cellH = Math.max(34, Math.min(44, cellW * 0.7));
84
  const H = data.length * cellH + margin.top + margin.bottom;
85
  svg.attr("width",W).attr("height",H);
86
  const g = svg.append("g").attr("transform",`translate(${margin.left},${margin.top})`);
87
  const gridW = cellW * subtasks.length;
88
  const gridH = cellH * data.length;
 
89
 
90
- // Column labels
91
  g.selectAll(".col-lbl").data(subtasks).join("text")
92
  .attr("x",(_,i)=>i*cellW+cellW/2).attr("y",gridH+22)
93
- .attr("text-anchor","middle").attr("fill","#8b8fa8").attr("font-size",11)
94
  .text(d=>d);
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  // Row labels + series stripe
97
  data.forEach((d, ri) => {
98
- // Series colour stripe on the left
99
  g.append("rect")
100
  .attr("x",-margin.left+2).attr("y",ri*cellH+2)
101
  .attr("width",4).attr("height",cellH-4).attr("rx",2)
@@ -103,76 +203,117 @@ function render() {
103
 
104
  g.append("text")
105
  .attr("x",-8).attr("y",ri*cellH+cellH/2)
106
- .attr("text-anchor","end").attr("fill","#e8eaf0").attr("font-size",10).attr("font-weight","500")
107
  .text(d.label);
108
 
109
  g.append("text")
110
- .attr("x",-8).attr("y",ri*cellH+cellH/2+11)
111
- .attr("text-anchor","end").attr("fill","#8b8fa8").attr("font-size",8)
112
  .text(d.total_sr+"% SR");
113
  });
114
 
115
- // Cells
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  data.forEach((d, ri) => {
117
  d.times.forEach((val, ci) => {
118
- const cellG = g.append("g").attr("transform",`translate(${ci*cellW},${ri*cellH})`);
119
  if (val === null) {
120
- cellG.append("rect").attr("width",cellW-3).attr("height",cellH-3).attr("rx",4)
121
- .attr("fill","#1a1d27").attr("stroke","#2a2d3a");
122
- cellG.append("text").attr("x",cellW/2-1.5).attr("y",cellH/2+3)
123
- .attr("text-anchor","middle").attr("fill","#3a3d4a").attr("font-size",10).text("—");
124
  } else {
125
- const col = colorScale(Math.min(val,120));
126
- cellG.append("rect").attr("width",cellW-3).attr("height",cellH-3).attr("rx",4)
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  .attr("fill",col).style("cursor","pointer")
128
  .on("mousemove",function(event){
129
- tooltip.style("opacity",1).html(`
130
- <strong>Experiment ${d.label} — ${subtasks[ci]}</strong>
131
- <div class="tooltip-row"><span>Avg time</span><span>${val.toFixed(1)}s</span></div>
132
- <div class="tooltip-row"><span>Total SR</span><span>${d.total_sr}%</span></div>
133
- <div class="tooltip-row"><span>Series</span><span>${d.series}</span></div>
134
- `);
135
  const bx=container.getBoundingClientRect();
136
  const ex=event.clientX-bx.left, ey=event.clientY-bx.top;
137
- tooltip.style("left",Math.min(ex+12,W-170)+"px").style("top",Math.max(ey-70,0)+"px");
138
  })
139
  .on("mouseleave",()=>tooltip.style("opacity",0));
140
-
141
- const lum = d3.hsl(col).l;
142
- cellG.append("text")
143
  .attr("x",cellW/2-1.5).attr("y",cellH/2+4)
144
  .attr("text-anchor","middle")
145
  .attr("fill", lum > 0.52 ? "#1a1d27" : "#e8eaf0")
146
- .attr("font-size",Math.max(8,Math.min(12,cellW*0.21))).attr("font-weight","600")
147
- .text(val>=100 ? Math.round(val)+"s" : val.toFixed(1)+"s");
148
  }
149
  });
150
  });
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  // Sorted annotation
153
- g.append("text").attr("x",gridW).attr("y",-4).attr("text-anchor","end")
154
- .attr("fill","#8b8fa8").attr("font-size",9)
155
- .text("↓ sorted: best → worst");
 
156
  }
157
 
158
  render();
159
  window.addEventListener("resize", render);
160
 
161
- const EXPERIMENTS = {
162
- "1.1 π0": { desc:"π0 · all data · 200k steps · MEAN_STD", note:"Base pi0 policy trained from scratch on the full dataset." },
163
- "1.2 π0.5": { desc:"π0.5 · all data · 200k steps · MEAN_STD", note:"Upgraded to pi0.5 architecture, same data and steps." },
164
- "1.3 Relative": { desc:"π0.5 · all data · 200k steps · Relative Actions · QUANTILES", note:"Adds Relative Actions on top of 1.2 — actions expressed relative to current state." },
165
- "1.4 RABC low": { desc:"π0.5 · all data · 200k steps · RABC κ=0.01", note:"Selective Action Reward Model with low κ (≈ mean threshold, not very selective)." },
166
- "1.5 RABC high": { desc:"π0.5 · all data · 200k steps · RABC κ=0.0215", note:"SARM with κ = mean + ½ std — more selective filtering than 1.4." },
167
- "1.7 Rel+RABC": { desc:"π0.5 · all data · 200k steps · Relative Actions + RABC κ=0.0215 · QUANTILES", note:"Best of Series 1. Base checkpoint for 2.5." },
168
- "2.1 HQ": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3", note:"Fine-tunes 1.3 on curated high-quality data only." },
169
- "2.2 HQ+RABC+Rel": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3 + RABC κ=0.0265 + Relative Actions", note:"Adds RABC on high-quality fine-tune from 1.3." },
170
- "2.3 HQ+mirror": { desc:"π0.5 · HQ + mirrored · 100k steps · fine-tune from 1.3 + Relative Actions + mirroring", note:"Augments the high-quality dataset with mirrored trajectories." },
171
- "2.4 HQ chunk45": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3 · chunk=45", note:"Explores chunked action prediction (chunk=50, RTC size=50, execution horizon=35)." },
172
- "2.5 HQ+RABC+Rel★": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.7 + RABC κ=0.0265 + Relative Actions (best)", note:"Top performer. Best overall result." },
173
- };
174
-
175
-
176
  }
177
 
178
  if (typeof d3 !== "undefined") {
 
16
  .tooltip strong { display: block; margin-bottom: 5px; }
17
  .tooltip-row { display: flex; justify-content: space-between; gap: 12px; margin-top: 3px; font-size: 12px; color: var(--subtext); }
18
  .tooltip-row span:last-child { color: var(--text); font-weight: 600; }
19
+ .legend-bar { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 13px; color: var(--subtext); justify-content: center; }
20
  .legend-gradient { height: 10px; width: 180px; border-radius: 5px; flex-shrink: 0; }
21
+ .hm-toggle { display: flex; gap: 0; justify-content: center; margin-bottom: 6px; }
22
+ .hm-btn {
23
+ padding: 7px 20px; font-size: 14px; font-weight: 600; cursor: pointer; border: none;
24
+ background: #1a1d2e; color: #8b8fa8; transition: all .2s;
25
+ font-family: system-ui, sans-serif; border-bottom: 2px solid transparent;
26
+ }
27
+ .hm-btn:first-child { border-radius: 6px 0 0 0; }
28
+ .hm-btn:last-child { border-radius: 0 6px 0 0; }
29
+ .hm-btn.active { background: #252540; color: #e8eaf0; border-bottom-color: #6366f1; }
30
+ .hm-btn:hover:not(.active) { background: #1f2238; color: #bbb; }
31
+ .filter-toggle {
32
+ font-size: 12px; color: #8b8fa8; cursor: pointer; border: 1px solid #2a2d3a;
33
+ background: #1a1d27; padding: 4px 12px; border-radius: 14px; transition: all .15s;
34
+ user-select: none; display: inline-flex; align-items: center; gap: 4px;
35
+ }
36
+ .filter-toggle:hover { border-color: #555; color: #e8eaf0; }
37
+ .filter-toggle.showing-all { background: rgba(248,147,79,0.12); border-color: #f7934f; color: #f7934f; }
38
+ .hm-controls { display: flex; justify-content: center; gap: 8px; margin-bottom: 6px; align-items: center; }
39
  </style>
40
  </head>
41
  <body>
42
+ <div class="hm-controls">
43
+ <button class="filter-toggle" id="hm-filter-btn" onclick="window._hmToggleFilter()">Show all 11</button>
44
+ </div>
45
  <div style="position:relative">
46
  <svg id="hm-chart"></svg>
47
  <div class="tooltip" id="hm-tooltip"></div>
48
  </div>
49
+ <div class="legend-bar" id="lgd-bar">
50
+ <span id="lgd-lo">Fast (0s)</span>
51
  <canvas id="lgd" class="legend-gradient" width="180" height="10"></canvas>
52
+ <span id="lgd-hi">Slow (≥120s)</span>
53
  </div>
54
  <script>
55
  function _initSubtaskHeatmap() {
56
  const rawData = [
57
+ {label:"1.1 π0",series:"1",total_sr:40, times:[null, 19.2, 42.22, 14.33, 19.88, 27.25], l1time:121.5, quality:2.70},
58
+ {label:"1.2 π0.5",series:"1",total_sr:20, times:[50, 39.27, 41.5, 12.3, 13.75, 10.75], l1time:90.75, quality:2.50},
59
+ {label:"1.3 Relative",series:"1",total_sr:35, times:[null, 19.5, 44.2, 14.8, 30.33, 22.14], l1time:113.86,quality:2.80},
60
+ {label:"1.4 RABC low",series:"1",total_sr:15, times:[null, 20.8, 36.62, 10.0, 18.8, 12.67], l1time:78.33, quality:2.20},
61
+ {label:"1.5 RABC high",series:"1",total_sr:0, times:[240, 21.4, 100.0, null, null, null ], l1time:null, quality:1.00},
62
+ {label:"1.7 Rel+RABC",series:"1",total_sr:40, times:[157.5,19.33, 32.64, 8.9, 11.0, 23.38], l1time:99.5, quality:2.30},
63
+ {label:"2.1 HQ",series:"2",total_sr:40, times:[77.5, 11.08, 21.09, 5.45, 5.5, 11.5 ], l1time:57.57, quality:2.80},
64
+ {label:"2.2 HQ+RABC+Rel",series:"2",total_sr:75, times:[34.33,6.25, 12.31, 3.75, 5.31, 8.93 ], l1time:43.2, quality:3.30},
65
+ {label:"2.3 HQ+mirror",series:"2",total_sr:5, times:[49, 14.0, 23.71, 17.5, 11.0, 4.0 ], l1time:null, quality:1.00},
66
+ {label:"2.4 HQ chunk45",series:"2",total_sr:20, times:[120, 10.09, 41.18, 7.89, 7.33, 10.0 ], l1time:72.5, quality:1.80},
67
+ {label:"2.5 HQ+RABC+Rel★",series:"2",total_sr:90, times:[62.25,8.28, 12.0, 5.28, 5.22, 6.83 ], l1time:40.8, quality:4.10},
68
  ];
69
 
70
+ const HIDDEN_BY_DEFAULT = new Set(["1.4 RABC low","1.5 RABC high","2.3 HQ+mirror","2.4 HQ chunk45"]);
71
+ let hmShowAll = false;
72
+
73
+ window._hmToggleFilter = function() {
74
+ hmShowAll = !hmShowAll;
75
+ const btn = document.getElementById("hm-filter-btn");
76
+ btn.textContent = hmShowAll ? "Key experiments" : "Show all 11";
77
+ btn.classList.toggle("showing-all", hmShowAll);
78
+ render();
79
+ };
80
+
81
+ function getFilteredData() {
82
+ const src = hmShowAll ? rawData : rawData.filter(d => !HIDDEN_BY_DEFAULT.has(d.label));
83
+ return [...src].sort((a,b) => {
84
+ if (a.l1time === null && b.l1time === null) return 0;
85
+ if (a.l1time === null) return 1;
86
+ if (b.l1time === null) return -1;
87
+ return a.l1time - b.l1time;
88
+ });
89
+ }
90
  const subtasks = ["Unfold","Fold 1","Fold 2","Fold 3","Fold 4","Rotation"];
91
  const seriesColor = s => s === "2" ? "#f7934f" : "#4f8ef7";
92
 
93
+ let mode = "rel";
94
+
95
+ // Compute per-subtask means over ALL experiments (ignoring nulls)
96
+ const subtaskMeans = subtasks.map((_, ci) => {
97
+ const vals = rawData.map(d => d.times[ci]).filter(v => v !== null);
98
+ return vals.length ? vals.reduce((a,b) => a+b, 0) / vals.length : 1;
99
+ });
100
+
101
+ function lerpColor(stops, t) {
102
+ for (let i=0; i<stops.length-1; i++) {
103
+ const [t0,c0]=stops[i],[t1,c1]=stops[i+1];
104
+ if (t<=t1) {
105
+ const f=(t-t0)/(t1-t0);
106
+ return `rgb(${Math.round(c0[0]+(c1[0]-c0[0])*f)},${Math.round(c0[1]+(c1[1]-c0[1])*f)},${Math.round(c0[2]+(c1[2]-c0[2])*f)})`;
107
  }
108
+ }
109
+ return `rgb(${stops[stops.length-1][1].join(",")})`;
110
+ }
111
+
112
+ // Absolute: green → yellow → red, capped at 120s
113
+ const absColor = v => lerpColor([[0,[46,200,138]],[0.4,[247,211,79]],[1,[220,60,60]]], Math.min(v,120)/120);
114
 
115
+ // Relative: green (0.2x) → neutral (1x) → red (3x), diverging around mean
116
+ const relColor = ratio => {
117
+ const t = Math.max(0, Math.min(1, (ratio - 0.2) / (3 - 0.2)));
118
+ return lerpColor([[0,[46,200,138]],[0.286,[230,230,210]],[1,[220,60,60]]], t);
119
+ };
120
+
121
+ // Quality: red (1.0) → yellow (2.5) → green (4.5)
122
+ const qualColor = v => {
123
+ const t = Math.max(0, Math.min(1, (v - 1.0) / (4.5 - 1.0)));
124
+ return lerpColor([[0,[220,60,60]],[0.43,[247,211,79]],[1,[46,200,138]]], t);
125
+ };
126
+
127
+ function drawLegend() {
128
+ const canvas = document.getElementById("lgd");
129
+ const ctx = canvas.getContext("2d");
130
+ for (let i=0; i<180; i++) {
131
+ const t = i/179;
132
+ ctx.fillStyle = mode === "abs" ? absColor(t*120) : relColor(0.2 + t*2.8);
133
+ ctx.fillRect(i,0,1,10);
134
+ }
135
+ document.getElementById("lgd-lo").textContent = mode === "abs" ? "Fast (0s)" : "Fast (0.2×)";
136
+ document.getElementById("lgd-hi").textContent = mode === "abs" ? "Slow (≥120s)" : "Slow (≥3×)";
137
+ }
138
+ drawLegend();
139
 
140
  const margin = {top:12, right:16, bottom:36, left:120};
141
  const svg = d3.select("#hm-chart");
142
  const container = svg.node().parentElement;
143
  const tooltip = d3.select("#hm-tooltip");
144
 
145
+ const EXPERIMENTS = {
146
+ "1.1 π0": { note:"Base pi0 policy trained from scratch on the full dataset." },
147
+ "1.2 π0.5": { note:"Upgraded to pi0.5 architecture, same data and steps." },
148
+ "1.3 Relative": { note:"Adds Relative Actions, expressing actions relative to current state." },
149
+ "1.4 RABC low": { note:"SARM with low κ (≈ mean threshold, not very selective)." },
150
+ "1.5 RABC high": { note:"SARM with κ = mean + ½ std, more selective filtering than 1.4." },
151
+ "1.7 Rel+RABC": { note:"Best of initial training. Base checkpoint for 2.5." },
152
+ "2.1 HQ": { note:"Fine-tunes 1.3 on curated high-quality data only." },
153
+ "2.2 HQ+RABC+Rel": { note:"Adds RABC on high-quality fine-tune from 1.3." },
154
+ "2.3 HQ+mirror": { note:"Augments the high-quality dataset with mirrored trajectories." },
155
+ "2.4 HQ chunk45": { note:"Larger action chunk size (45 vs default 30)." },
156
+ "2.5 HQ+RABC+Rel★": { note:"Top performer. Best overall result." },
157
+ };
158
+
159
+ const GAP = 10;
160
+ const sumCols = ["L1 Time","Quality"];
161
+
162
  function render() {
163
  svg.selectAll("*").remove();
164
+ const data = getFilteredData();
165
  const W = container.clientWidth;
166
+ const totalCols = subtasks.length + sumCols.length;
167
+ const cellW = Math.floor((W - margin.left - margin.right - GAP) / totalCols);
168
  const cellH = Math.max(34, Math.min(44, cellW * 0.7));
169
  const H = data.length * cellH + margin.top + margin.bottom;
170
  svg.attr("width",W).attr("height",H);
171
  const g = svg.append("g").attr("transform",`translate(${margin.left},${margin.top})`);
172
  const gridW = cellW * subtasks.length;
173
  const gridH = cellH * data.length;
174
+ const sumX = gridW + GAP;
175
 
176
+ // Subtask column labels
177
  g.selectAll(".col-lbl").data(subtasks).join("text")
178
  .attr("x",(_,i)=>i*cellW+cellW/2).attr("y",gridH+22)
179
+ .attr("text-anchor","middle").attr("fill","#8b8fa8").attr("font-size",13)
180
  .text(d=>d);
181
 
182
+ // Summary column labels (italic, slightly muted)
183
+ sumCols.forEach((label, i) => {
184
+ g.append("text")
185
+ .attr("x", sumX + i * cellW + cellW / 2).attr("y", gridH + 22)
186
+ .attr("text-anchor","middle").attr("fill","#6b6f88").attr("font-size",12)
187
+ .attr("font-style","italic")
188
+ .text(label);
189
+ });
190
+
191
+ // Vertical divider between subtask and summary columns
192
+ g.append("line")
193
+ .attr("x1", gridW + GAP / 2).attr("x2", gridW + GAP / 2)
194
+ .attr("y1", -2).attr("y2", gridH)
195
+ .attr("stroke","#2a2d3a").attr("stroke-width",1);
196
+
197
  // Row labels + series stripe
198
  data.forEach((d, ri) => {
 
199
  g.append("rect")
200
  .attr("x",-margin.left+2).attr("y",ri*cellH+2)
201
  .attr("width",4).attr("height",cellH-4).attr("rx",2)
 
203
 
204
  g.append("text")
205
  .attr("x",-8).attr("y",ri*cellH+cellH/2)
206
+ .attr("text-anchor","end").attr("fill","#e8eaf0").attr("font-size",12).attr("font-weight","500")
207
  .text(d.label);
208
 
209
  g.append("text")
210
+ .attr("x",-8).attr("y",ri*cellH+cellH/2+12)
211
+ .attr("text-anchor","end").attr("fill","#8b8fa8").attr("font-size",10)
212
  .text(d.total_sr+"% SR");
213
  });
214
 
215
+ // Helper: draw a single heatmap cell
216
+ function drawCell(parent, x, y, w, h, val, col, tooltipHtml) {
217
+ const cg = parent.append("g").attr("transform",`translate(${x},${y})`);
218
+ if (val === null) return;
219
+ const lum = d3.hsl(col).l;
220
+ cg.append("rect").attr("width",w-3).attr("height",h-3).attr("rx",4)
221
+ .attr("fill",col).style("cursor","pointer")
222
+ .on("mousemove",function(event){
223
+ tooltip.style("opacity",1).html(tooltipHtml);
224
+ const bx=container.getBoundingClientRect();
225
+ const ex=event.clientX-bx.left, ey=event.clientY-bx.top;
226
+ tooltip.style("left",Math.min(ex+12,W-220)+"px").style("top",Math.max(ey-70,0)+"px");
227
+ })
228
+ .on("mouseleave",()=>tooltip.style("opacity",0));
229
+ const fs = Math.max(10, Math.min(14, w * 0.24));
230
+ cg.append("text")
231
+ .attr("x",w/2-1.5).attr("y",h/2+4)
232
+ .attr("text-anchor","middle")
233
+ .attr("fill", lum > 0.52 ? "#1a1d27" : "#e8eaf0")
234
+ .attr("font-size", fs).attr("font-weight","600")
235
+ .text(typeof val === "string" ? val : (val >= 100 ? Math.round(val)+"s" : val.toFixed(1)+"s"));
236
+ }
237
+
238
+ // Subtask cells
239
  data.forEach((d, ri) => {
240
  d.times.forEach((val, ci) => {
 
241
  if (val === null) {
242
+ // skip — leave empty
 
 
 
243
  } else {
244
+ const ratio = val / subtaskMeans[ci];
245
+ const col = mode === "abs" ? absColor(val) : relColor(ratio);
246
+ const cellLabel = mode === "abs"
247
+ ? (val>=100 ? Math.round(val)+"s" : val.toFixed(1)+"s")
248
+ : ratio.toFixed(1)+"×";
249
+ const ttHtml = `
250
+ <strong>Experiment ${d.label}: ${subtasks[ci]}</strong>
251
+ <div class="tooltip-row"><span>Avg time</span><span>${val.toFixed(1)}s</span></div>
252
+ <div class="tooltip-row"><span>Subtask mean</span><span>${subtaskMeans[ci].toFixed(1)}s</span></div>
253
+ <div class="tooltip-row"><span>Relative</span><span>${ratio.toFixed(2)}×</span></div>
254
+ <div class="tooltip-row"><span>Total SR</span><span>${d.total_sr}%</span></div>
255
+ `;
256
+ const cg = g.append("g").attr("transform",`translate(${ci*cellW},${ri*cellH})`);
257
+ const lum = d3.hsl(col).l;
258
+ cg.append("rect").attr("width",cellW-3).attr("height",cellH-3).attr("rx",4)
259
  .attr("fill",col).style("cursor","pointer")
260
  .on("mousemove",function(event){
261
+ tooltip.style("opacity",1).html(ttHtml);
 
 
 
 
 
262
  const bx=container.getBoundingClientRect();
263
  const ex=event.clientX-bx.left, ey=event.clientY-bx.top;
264
+ tooltip.style("left",Math.min(ex+12,W-220)+"px").style("top",Math.max(ey-70,0)+"px");
265
  })
266
  .on("mouseleave",()=>tooltip.style("opacity",0));
267
+ const fs = Math.max(10, Math.min(14, cellW * 0.24));
268
+ cg.append("text")
 
269
  .attr("x",cellW/2-1.5).attr("y",cellH/2+4)
270
  .attr("text-anchor","middle")
271
  .attr("fill", lum > 0.52 ? "#1a1d27" : "#e8eaf0")
272
+ .attr("font-size", fs).attr("font-weight","600")
273
+ .text(cellLabel);
274
  }
275
  });
276
  });
277
 
278
+ // Summary columns: L1 Time and Quality
279
+ data.forEach((d, ri) => {
280
+ const note = (EXPERIMENTS[d.label] || {}).note || "";
281
+
282
+ // L1 Time cell
283
+ const l1 = d.l1time;
284
+ const l1Col = l1 !== null ? absColor(l1) : null;
285
+ const l1Label = l1 !== null ? (l1 >= 100 ? Math.round(l1) + "s" : l1.toFixed(1) + "s") : null;
286
+ const l1Tt = l1 !== null ? `
287
+ <strong>${d.label}: L1 Completion Time</strong>
288
+ <div class="tooltip-row"><span>L1 time</span><span>${l1.toFixed(1)}s</span></div>
289
+ <div class="tooltip-row"><span>Total SR</span><span>${d.total_sr}%</span></div>
290
+ ${note ? `<div style="margin-top:5px;font-size:10px;color:#555;border-top:1px solid #2a2d3a;padding-top:5px">${note}</div>` : ""}
291
+ ` : "";
292
+ drawCell(g, sumX, ri * cellH, cellW, cellH, l1 !== null ? l1Label : null, l1Col, l1Tt);
293
+
294
+ // Quality cell
295
+ const q = d.quality;
296
+ const qCol = qualColor(q);
297
+ const qLabel = q.toFixed(1);
298
+ const qTt = `
299
+ <strong>${d.label}: Fold Quality</strong>
300
+ <div class="tooltip-row"><span>Quality</span><span>${q.toFixed(2)} / 5</span></div>
301
+ <div class="tooltip-row"><span>Total SR</span><span>${d.total_sr}%</span></div>
302
+ ${note ? `<div style="margin-top:5px;font-size:10px;color:#555;border-top:1px solid #2a2d3a;padding-top:5px">${note}</div>` : ""}
303
+ `;
304
+ drawCell(g, sumX + cellW, ri * cellH, cellW, cellH, qLabel, qCol, qTt);
305
+ });
306
+
307
  // Sorted annotation
308
+ const totalW = gridW + GAP + cellW * sumCols.length;
309
+ g.append("text").attr("x",totalW).attr("y",-4).attr("text-anchor","end")
310
+ .attr("fill","#8b8fa8").attr("font-size",11)
311
+ .text("↓ sorted: fastest → slowest L1 time");
312
  }
313
 
314
  render();
315
  window.addEventListener("resize", render);
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  }
318
 
319
  if (typeof d3 !== "undefined") {
app/src/content/embeds/folding/success-rates.html CHANGED
@@ -13,10 +13,10 @@
13
  display: flex; align-items: center; justify-content: center;
14
  gap: 8px; flex-wrap: wrap; margin-bottom: 10px;
15
  }
16
- .ctrl-label { font-size: 11px; color: var(--subtext); }
17
  .toggle-btn {
18
  background: #1a1d27; border: 1.5px solid #2a2d3a; color: #8b8fa8;
19
- font-size: 12px; padding: 5px 14px; border-radius: 20px; cursor: pointer;
20
  transition: all .15s; user-select: none; font-weight: 500;
21
  }
22
  .toggle-btn:hover { border-color: #555; color: #e8eaf0; }
@@ -26,7 +26,7 @@
26
 
27
  /* ── Legend ── */
28
  .legend { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-bottom: 4px; }
29
- .legend-item { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--subtext); transition: opacity .2s; }
30
  .legend-item.dimmed { opacity: 0.22; }
31
  .legend-dot { width: 11px; height: 11px; border-radius: 2px; }
32
  .legend-pip { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
@@ -35,7 +35,7 @@
35
  .ci-note {
36
  background: rgba(255,255,255,0.03); border: 1px solid #2a2d3a;
37
  border-radius: 8px; padding: 9px 14px; margin-bottom: 10px;
38
- font-size: 11px; color: #8b8fa8; line-height: 1.6;
39
  display: flex; align-items: flex-start; gap: 10px;
40
  }
41
  .ci-note-icon { font-size: 14px; flex-shrink: 0; margin-top: 1px; }
@@ -55,7 +55,7 @@
55
  .tooltip-note { margin-top: 7px; padding-top: 7px; border-top: 1px solid #2a2d3a; font-size: 11px; color: #8b8fa8; line-height: 1.5; }
56
 
57
  /* ── Chart ── */
58
- .axis text { fill: var(--subtext); font-size: 12px; }
59
  .axis line, .axis path { stroke: var(--grid); }
60
  .grid line { stroke: var(--grid); stroke-dasharray: 3,3; }
61
  .bar { cursor: pointer; transition: opacity .12s; }
@@ -79,8 +79,8 @@
79
  <div class="legend-item" id="leg-l1"> <div class="legend-dot" style="background:#4dc98a"></div>Level 1</div>
80
  <div class="legend-item" id="leg-l2"> <div class="legend-dot" style="background:#f87171"></div>Level 2</div>
81
  <div class="legend-item">
82
- <div class="legend-pip" style="background:#f7934f"></div>&nbsp;Series 2
83
- &nbsp;<div class="legend-pip" style="background:#4f8ef7"></div>&nbsp;Series 1
84
  </div>
85
  </div>
86
 
@@ -104,9 +104,9 @@ const EXPERIMENTS = {
104
  "1.1 π0": { desc:"π0 · all data · 200k steps · MEAN_STD", note:"π0 base model fine-tuned on full dataset. Default normalization." },
105
  "1.2 π0.5": { desc:"π0.5 · all data · 200k steps · MEAN_STD", note:"Upgraded to π0.5 with MEAN_STD normalization." },
106
  "1.3 Relative": { desc:"π0.5 · all data · 200k steps · Relative Actions · QUANTILES", note:"Adds relative actions and switches to QUANTILES normalization." },
107
- "1.4 RABC low": { desc:"π0.5 · all data · 200k steps · RABC κ=0.01", note:"RABC with low κ — not very selective. MEAN_STD norm, no relative actions." },
108
- "1.5 RABC high": { desc:"π0.5 · all data · 200k steps · RABC κ=0.0215", note:"RABC with higher κ (mean + ½σ). MEAN_STD norm, no relative actions." },
109
- "1.7 Rel+RABC": { desc:"π0.5 · all data · 200k steps · Relative Actions + RABC κ=0.0215 · QUANTILES", note:"Best of Series 1. Base checkpoint for 2.5." },
110
  "2.1 HQ": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3", note:"Fine-tunes 1.3 on curated high-quality data. No RABC." },
111
  "2.2 HQ+RABC+Rel": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3 + RABC κ=0.0265 + Relative Actions", note:"Adds RABC on high-quality fine-tune from 1.3." },
112
  "2.3 HQ+mirror": { desc:"π0.5 · HQ + mirrored · 100k steps · fine-tune from 1.3 + Relative Actions + mirroring", note:"Augments HQ data with mirrored trajectories." },
@@ -248,7 +248,7 @@ function render() {
248
  // Axes
249
  g.append("g").attr("class","axis").attr("transform",`translate(0,${h})`).call(
250
  d3.axisBottom(x0).tickSize(0))
251
- .call(gg=>{gg.select(".domain").remove();gg.selectAll("text").attr("transform","rotate(-40)").attr("text-anchor","end").attr("dx","-0.5em").attr("dy","0.3em").attr("font-size",9)});
252
  g.append("g").attr("class","axis").call(
253
  d3.axisLeft(y).tickValues([0,25,50,75,100]).tickFormat(d=>d+"%").tickSize(0))
254
  .call(ax=>ax.select(".domain").remove())
@@ -349,7 +349,7 @@ function render() {
349
  const sk = active[sortKey] ? sortKey : ["total","l1","l2"].find(k => active[k]);
350
  const skLabel = {total:"total SR", l1:"Level 1 SR", l2:"Level 2 SR"};
351
  g.append("text").attr("x",w).attr("y",-12).attr("text-anchor","end")
352
- .attr("fill","#8b8fa8").attr("font-size",10)
353
  .text(`sorted: best → worst by ${skLabel[sk]}`);
354
  }
355
 
 
13
  display: flex; align-items: center; justify-content: center;
14
  gap: 8px; flex-wrap: wrap; margin-bottom: 10px;
15
  }
16
+ .ctrl-label { font-size: 13px; color: var(--subtext); }
17
  .toggle-btn {
18
  background: #1a1d27; border: 1.5px solid #2a2d3a; color: #8b8fa8;
19
+ font-size: 14px; padding: 6px 16px; border-radius: 20px; cursor: pointer;
20
  transition: all .15s; user-select: none; font-weight: 500;
21
  }
22
  .toggle-btn:hover { border-color: #555; color: #e8eaf0; }
 
26
 
27
  /* ── Legend ── */
28
  .legend { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-bottom: 4px; }
29
+ .legend-item { display: flex; align-items: center; gap: 5px; font-size: 13px; color: var(--subtext); transition: opacity .2s; }
30
  .legend-item.dimmed { opacity: 0.22; }
31
  .legend-dot { width: 11px; height: 11px; border-radius: 2px; }
32
  .legend-pip { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
 
35
  .ci-note {
36
  background: rgba(255,255,255,0.03); border: 1px solid #2a2d3a;
37
  border-radius: 8px; padding: 9px 14px; margin-bottom: 10px;
38
+ font-size: 13px; color: #8b8fa8; line-height: 1.6;
39
  display: flex; align-items: flex-start; gap: 10px;
40
  }
41
  .ci-note-icon { font-size: 14px; flex-shrink: 0; margin-top: 1px; }
 
55
  .tooltip-note { margin-top: 7px; padding-top: 7px; border-top: 1px solid #2a2d3a; font-size: 11px; color: #8b8fa8; line-height: 1.5; }
56
 
57
  /* ── Chart ── */
58
+ .axis text { fill: var(--subtext); font-size: 13px; }
59
  .axis line, .axis path { stroke: var(--grid); }
60
  .grid line { stroke: var(--grid); stroke-dasharray: 3,3; }
61
  .bar { cursor: pointer; transition: opacity .12s; }
 
79
  <div class="legend-item" id="leg-l1"> <div class="legend-dot" style="background:#4dc98a"></div>Level 1</div>
80
  <div class="legend-item" id="leg-l2"> <div class="legend-dot" style="background:#f87171"></div>Level 2</div>
81
  <div class="legend-item">
82
+ <div class="legend-pip" style="background:#f7934f"></div>&nbsp;Fine-tuned (2.x)
83
+ &nbsp;<div class="legend-pip" style="background:#4f8ef7"></div>&nbsp;Initial (1.x)
84
  </div>
85
  </div>
86
 
 
104
  "1.1 π0": { desc:"π0 · all data · 200k steps · MEAN_STD", note:"π0 base model fine-tuned on full dataset. Default normalization." },
105
  "1.2 π0.5": { desc:"π0.5 · all data · 200k steps · MEAN_STD", note:"Upgraded to π0.5 with MEAN_STD normalization." },
106
  "1.3 Relative": { desc:"π0.5 · all data · 200k steps · Relative Actions · QUANTILES", note:"Adds relative actions and switches to QUANTILES normalization." },
107
+ "1.4 RABC low": { desc:"π0.5 · all data · 200k steps · RABC κ=0.01", note:"RABC with low κ, not very selective. MEAN_STD norm, no relative actions." },
108
+ "1.5 RABC high": { desc:"π0.5 · all data · 200k steps · RABC κ=0.0215", note:"RABC with higher κ (mean + ½σ). MEAN_STD norm, no relative actions." },
109
+ "1.7 Rel+RABC": { desc:"π0.5 · all data · 200k steps · Relative Actions + RABC κ=0.0215 · QUANTILES", note:"Best of initial training. Base checkpoint for 2.5." },
110
  "2.1 HQ": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3", note:"Fine-tunes 1.3 on curated high-quality data. No RABC." },
111
  "2.2 HQ+RABC+Rel": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3 + RABC κ=0.0265 + Relative Actions", note:"Adds RABC on high-quality fine-tune from 1.3." },
112
  "2.3 HQ+mirror": { desc:"π0.5 · HQ + mirrored · 100k steps · fine-tune from 1.3 + Relative Actions + mirroring", note:"Augments HQ data with mirrored trajectories." },
 
248
  // Axes
249
  g.append("g").attr("class","axis").attr("transform",`translate(0,${h})`).call(
250
  d3.axisBottom(x0).tickSize(0))
251
+ .call(gg=>{gg.select(".domain").remove();gg.selectAll("text").attr("transform","rotate(-40)").attr("text-anchor","end").attr("dx","-0.5em").attr("dy","0.3em").attr("font-size",11)});
252
  g.append("g").attr("class","axis").call(
253
  d3.axisLeft(y).tickValues([0,25,50,75,100]).tickFormat(d=>d+"%").tickSize(0))
254
  .call(ax=>ax.select(".domain").remove())
 
349
  const sk = active[sortKey] ? sortKey : ["total","l1","l2"].find(k => active[k]);
350
  const skLabel = {total:"total SR", l1:"Level 1 SR", l2:"Level 2 SR"};
351
  g.append("text").attr("x",w).attr("y",-12).attr("text-anchor","end")
352
+ .attr("fill","#8b8fa8").attr("font-size",12)
353
  .text(`sorted: best → worst by ${skLabel[sk]}`);
354
  }
355
 
app/src/content/embeds/folding/total-score.html CHANGED
@@ -8,7 +8,7 @@
8
  * { box-sizing: border-box; margin: 0; padding: 0; }
9
  body { background: var(--bg); font-family: system-ui, sans-serif; color: var(--text); }
10
 
11
- .axis text { fill: var(--subtext); font-size: 11px; }
12
  .axis line, .axis path { stroke: var(--grid); }
13
  .grid line { stroke: var(--grid); stroke-dasharray: 3,3; }
14
  .tooltip {
@@ -76,11 +76,11 @@ function render() {
76
  // 50% reference line
77
  g.append("line").attr("stroke","#fbbf24").attr("stroke-dasharray","5,3").attr("stroke-width",1.5).attr("opacity",0.6)
78
  .attr("x1",0).attr("x2",w).attr("y1",y(50)).attr("y2",y(50));
79
- g.append("text").attr("x",w+3).attr("y",y(50)+4).attr("fill","#fbbf24").attr("font-size",9).text("50%");
80
 
81
  g.append("g").attr("class","axis").attr("transform",`translate(0,${h})`).call(
82
  d3.axisBottom(x).tickSize(0))
83
- .call(gg=>{gg.select(".domain").remove();gg.selectAll("text").attr("transform","rotate(-40)").attr("text-anchor","end").attr("dx","-0.5em").attr("dy","0.3em").attr("font-size",9)});
84
  g.append("g").attr("class","axis").call(
85
  d3.axisLeft(y).ticks(5).tickFormat(d=>d+"%").tickSize(0))
86
  .call(ax=>ax.select(".domain").remove())
@@ -144,11 +144,11 @@ function render() {
144
  g.append("line").attr("x1",bx).attr("x2",bx).attr("y1",by-16).attr("y2",-8)
145
  .attr("stroke","#4dc98a").attr("stroke-width",1).attr("stroke-dasharray","2,2").attr("opacity",0.5);
146
  g.append("text").attr("x",bx).attr("y",-12).attr("text-anchor","middle")
147
- .attr("fill","#4dc98a").attr("font-size",9).attr("font-weight","600").text("★ best");
148
  }
149
 
150
  g.append("text").attr("x",w).attr("y",-12).attr("text-anchor","end")
151
- .attr("fill","#8b8fa8").attr("font-size",10)
152
  .text("sorted: highest → lowest score %");
153
  }
154
 
@@ -158,10 +158,10 @@ window.addEventListener("resize", render);
158
  const EXPERIMENTS = {
159
  "1.1 π0": { desc:"π0 · all data · 200k steps · MEAN_STD", note:"Base pi0 policy trained from scratch on the full dataset." },
160
  "1.2 π0.5": { desc:"π0.5 · all data · 200k steps · MEAN_STD", note:"Upgraded to pi0.5 architecture, same data and steps." },
161
- "1.3 Relative": { desc:"π0.5 · all data · 200k steps · Relative Actions · QUANTILES", note:"Adds Relative Actions on top of 1.2 — actions expressed relative to current state." },
162
  "1.4 RABC low": { desc:"π0.5 · all data · 200k steps · RABC κ=0.01", note:"Selective Action Reward Model with low κ (≈ mean threshold, not very selective)." },
163
- "1.5 RABC high": { desc:"π0.5 · all data · 200k steps · RABC κ=0.0215", note:"SARM with κ = mean + ½ std — more selective filtering than 1.4." },
164
- "1.7 Rel+RABC": { desc:"π0.5 · all data · 200k steps · Relative Actions + RABC κ=0.0215 · QUANTILES", note:"Best of Series 1. Base checkpoint for 2.5." },
165
  "2.1 HQ": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3", note:"Fine-tunes 1.3 on curated high-quality data only." },
166
  "2.2 HQ+RABC+Rel": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3 + RABC κ=0.0265 + Relative Actions", note:"Adds RABC on high-quality fine-tune from 1.3." },
167
  "2.3 HQ+mirror": { desc:"π0.5 · HQ + mirrored · 100k steps · fine-tune from 1.3 + Relative Actions + mirroring", note:"Augments the high-quality dataset with mirrored trajectories." },
 
8
  * { box-sizing: border-box; margin: 0; padding: 0; }
9
  body { background: var(--bg); font-family: system-ui, sans-serif; color: var(--text); }
10
 
11
+ .axis text { fill: var(--subtext); font-size: 13px; }
12
  .axis line, .axis path { stroke: var(--grid); }
13
  .grid line { stroke: var(--grid); stroke-dasharray: 3,3; }
14
  .tooltip {
 
76
  // 50% reference line
77
  g.append("line").attr("stroke","#fbbf24").attr("stroke-dasharray","5,3").attr("stroke-width",1.5).attr("opacity",0.6)
78
  .attr("x1",0).attr("x2",w).attr("y1",y(50)).attr("y2",y(50));
79
+ g.append("text").attr("x",w+3).attr("y",y(50)+4).attr("fill","#fbbf24").attr("font-size",11).text("50%");
80
 
81
  g.append("g").attr("class","axis").attr("transform",`translate(0,${h})`).call(
82
  d3.axisBottom(x).tickSize(0))
83
+ .call(gg=>{gg.select(".domain").remove();gg.selectAll("text").attr("transform","rotate(-40)").attr("text-anchor","end").attr("dx","-0.5em").attr("dy","0.3em").attr("font-size",11)});
84
  g.append("g").attr("class","axis").call(
85
  d3.axisLeft(y).ticks(5).tickFormat(d=>d+"%").tickSize(0))
86
  .call(ax=>ax.select(".domain").remove())
 
144
  g.append("line").attr("x1",bx).attr("x2",bx).attr("y1",by-16).attr("y2",-8)
145
  .attr("stroke","#4dc98a").attr("stroke-width",1).attr("stroke-dasharray","2,2").attr("opacity",0.5);
146
  g.append("text").attr("x",bx).attr("y",-12).attr("text-anchor","middle")
147
+ .attr("fill","#4dc98a").attr("font-size",11).attr("font-weight","600").text("★ best");
148
  }
149
 
150
  g.append("text").attr("x",w).attr("y",-12).attr("text-anchor","end")
151
+ .attr("fill","#8b8fa8").attr("font-size",12)
152
  .text("sorted: highest → lowest score %");
153
  }
154
 
 
158
  const EXPERIMENTS = {
159
  "1.1 π0": { desc:"π0 · all data · 200k steps · MEAN_STD", note:"Base pi0 policy trained from scratch on the full dataset." },
160
  "1.2 π0.5": { desc:"π0.5 · all data · 200k steps · MEAN_STD", note:"Upgraded to pi0.5 architecture, same data and steps." },
161
+ "1.3 Relative": { desc:"π0.5 · all data · 200k steps · Relative Actions · QUANTILES", note:"Adds Relative Actions on top of 1.2, expressing actions relative to current state." },
162
  "1.4 RABC low": { desc:"π0.5 · all data · 200k steps · RABC κ=0.01", note:"Selective Action Reward Model with low κ (≈ mean threshold, not very selective)." },
163
+ "1.5 RABC high": { desc:"π0.5 · all data · 200k steps · RABC κ=0.0215", note:"SARM with κ = mean + ½ std, more selective filtering than 1.4." },
164
+ "1.7 Rel+RABC": { desc:"π0.5 · all data · 200k steps · Relative Actions + RABC κ=0.0215 · QUANTILES", note:"Best of initial training. Base checkpoint for 2.5." },
165
  "2.1 HQ": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3", note:"Fine-tunes 1.3 on curated high-quality data only." },
166
  "2.2 HQ+RABC+Rel": { desc:"π0.5 · HQ data · 100k steps · fine-tune from 1.3 + RABC κ=0.0265 + Relative Actions", note:"Adds RABC on high-quality fine-tune from 1.3." },
167
  "2.3 HQ+mirror": { desc:"π0.5 · HQ + mirrored · 100k steps · fine-tune from 1.3 + Relative Actions + mirroring", note:"Augments the high-quality dataset with mirrored trajectories." },
compute_cld.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Compute CLD (Compact Letter Display) letters and verify Beta posteriors
3
+ for all experiments using the TRI STEP sequential testing framework.
4
+
5
+ Parameters:
6
+ - global_confidence_level = 0.90 (α=0.10)
7
+ - n_max = 50
8
+ - shuffle = False (each rollout is independent)
9
+ - 11 experiments → C(11,2) = 55 pairwise comparisons → Bonferroni correction
10
+ """
11
+
12
+ import json
13
+ import numpy as np
14
+ from scipy import stats
15
+ from sequentialized_barnard_tests.tools.plotting import compare_success_and_get_cld
16
+
17
+ EXPERIMENTS = {
18
+ "1.1 π0": {"total": [8, 20], "L1": [8, 10], "L2": [0, 10]},
19
+ "1.2 π0.5": {"total": [4, 20], "L1": [4, 10], "L2": [0, 10]},
20
+ "1.3 Relative": {"total": [7, 20], "L1": [7, 10], "L2": [0, 10]},
21
+ "1.4 RABC low": {"total": [3, 20], "L1": [3, 10], "L2": [0, 10]},
22
+ "1.5 RABC high": {"total": [0, 20], "L1": [0, 10], "L2": [0, 10]},
23
+ "1.7 Rel+RABC": {"total": [8, 20], "L1": [8, 10], "L2": [0, 10]},
24
+ "2.1 HQ": {"total": [8, 20], "L1": [7, 10], "L2": [1, 10]},
25
+ "2.2 HQ+RABC+Rel": {"total": [15, 20], "L1": [10, 10], "L2": [5, 10]},
26
+ "2.3 HQ+mirror": {"total": [1, 20], "L1": [0, 10], "L2": [1, 10]},
27
+ "2.4 HQ chunk45": {"total": [4, 20], "L1": [4, 10], "L2": [0, 10]},
28
+ "2.5 HQ+RABC+Rel★": {"total": [18, 20], "L1": [10, 10], "L2": [8, 10]},
29
+ }
30
+
31
+ # What the HTML files use (percentages) — for cross-checking the round-trip
32
+ HTML_RAW_PCT = {
33
+ "1.1 π0": {"total": 40, "l1": 80, "l2": 0},
34
+ "1.2 π0.5": {"total": 20, "l1": 40, "l2": 0},
35
+ "1.3 Relative": {"total": 35, "l1": 70, "l2": 0},
36
+ "1.4 RABC low": {"total": 15, "l1": 30, "l2": 0},
37
+ "1.5 RABC high": {"total": 0, "l1": 0, "l2": 0},
38
+ "1.7 Rel+RABC": {"total": 40, "l1": 80, "l2": 0},
39
+ "2.1 HQ": {"total": 40, "l1": 70, "l2": 10},
40
+ "2.2 HQ+RABC+Rel": {"total": 75, "l1": 100, "l2": 50},
41
+ "2.3 HQ+mirror": {"total": 5, "l1": 0, "l2": 10},
42
+ "2.4 HQ chunk45": {"total": 20, "l1": 40, "l2": 0},
43
+ "2.5 HQ+RABC+Rel★": {"total": 90, "l1": 100, "l2": 80},
44
+ }
45
+
46
+ HTML_N = {"total": 20, "l1": 10, "l2": 10}
47
+
48
+ GLOBAL_CONFIDENCE = 0.90
49
+ N_MAX = 50
50
+ SHUFFLE = False
51
+
52
+ model_names = list(EXPERIMENTS.keys())
53
+
54
+
55
+ def draw_samples_from_beta_posterior(
56
+ success_array: np.ndarray,
57
+ rng: np.random.Generator,
58
+ num_samples: int = 10000,
59
+ alpha_prior: float = 1,
60
+ beta_prior: float = 1,
61
+ ) -> np.ndarray:
62
+ """TRI's exact function from their notebook."""
63
+ n_trials = len(success_array)
64
+ n_successes = np.sum(success_array)
65
+ n_failures = n_trials - n_successes
66
+ posterior = stats.beta(alpha_prior + n_successes, beta_prior + n_failures)
67
+ return posterior.rvs(num_samples, random_state=rng)
68
+
69
+
70
+ # ── 1. CLD letters ──────────────────────────────────────────────────────────
71
+ for level in ["total", "L1", "L2"]:
72
+ print(f"\n{'='*60}")
73
+ print(f" CLD — LEVEL: {level}")
74
+ print(f"{'='*60}")
75
+
76
+ success_arrays = []
77
+ for name in model_names:
78
+ k, n = EXPERIMENTS[name][level]
79
+ arr = np.array([True] * k + [False] * (n - k))
80
+ success_arrays.append(arr)
81
+
82
+ cld_dict = compare_success_and_get_cld(
83
+ model_names,
84
+ success_arrays,
85
+ GLOBAL_CONFIDENCE,
86
+ N_MAX,
87
+ SHUFFLE,
88
+ verbose=True,
89
+ )
90
+
91
+ print(f"\nJSON for HTML embed:")
92
+ json_obj = {name: cld_dict[name] for name in model_names}
93
+ print(json.dumps(json_obj, ensure_ascii=False))
94
+
95
+
96
+ # ── 2. Verify Beta posteriors ────────────────────────────────────────────────
97
+ print(f"\n\n{'#'*70}")
98
+ print(f" POSTERIOR VERIFICATION")
99
+ print(f" Prior: Beta(1,1) (uniform). Posterior: Beta(1+k, 1+n-k)")
100
+ print(f"{'#'*70}")
101
+
102
+ level_map = {"total": "total", "L1": "l1", "L2": "l2"}
103
+ rng = np.random.default_rng(42)
104
+ all_ok = True
105
+
106
+ for level in ["total", "L1", "L2"]:
107
+ html_key = level_map[level]
108
+ print(f"\n{'─'*60}")
109
+ print(f" Level: {level} (HTML key: '{html_key}', n={HTML_N[html_key]})")
110
+ print(f"{'─'*60}")
111
+ print(f" {'Experiment':<24s} {'k/n':>6s} {'α':>4s} {'β':>4s} {'Mean':>7s} {'90% CI':>16s} {'HTML%→k':>8s} {'Match':>5s}")
112
+
113
+ for name in model_names:
114
+ k, n = EXPERIMENTS[name][level]
115
+ alpha_post = 1 + k
116
+ beta_post = 1 + (n - k)
117
+ dist = stats.beta(alpha_post, beta_post)
118
+ mean = dist.mean()
119
+ ci_lo, ci_hi = dist.ppf(0.05), dist.ppf(0.95)
120
+
121
+ # Verify HTML percentage round-trip
122
+ html_pct = HTML_RAW_PCT[name][html_key]
123
+ html_n = HTML_N[html_key]
124
+ html_k = round(html_pct / 100 * html_n)
125
+ match = html_k == k
126
+
127
+ if not match:
128
+ all_ok = False
129
+
130
+ print(
131
+ f" {name:<24s} {k:>2d}/{n:<2d} {alpha_post:>4d} {beta_post:>4d} "
132
+ f"{mean*100:>6.1f}% [{ci_lo*100:>5.1f}% – {ci_hi*100:>5.1f}%] "
133
+ f"{html_pct}%→{html_k:>2d} {'✓' if match else '✗ MISMATCH'}"
134
+ )
135
+
136
+ # Also run TRI's draw_samples_from_beta_posterior for a spot-check
137
+ spot_name = "2.5 HQ+RABC+Rel★"
138
+ k_spot, n_spot = EXPERIMENTS[spot_name][level]
139
+ arr_spot = np.array([True] * k_spot + [False] * (n_spot - k_spot))
140
+ samples = draw_samples_from_beta_posterior(arr_spot, rng, num_samples=100_000)
141
+ print(f"\n Spot-check ({spot_name}, {level}):")
142
+ print(f" TRI samples: mean={np.mean(samples)*100:.1f}%, std={np.std(samples)*100:.1f}%")
143
+ alpha_s, beta_s = 1 + k_spot, 1 + (n_spot - k_spot)
144
+ analytic = stats.beta(alpha_s, beta_s)
145
+ print(f" Analytic: mean={analytic.mean()*100:.1f}%, std={analytic.std()*100:.1f}%")
146
+ print(f" HTML params: Beta({alpha_s}, {beta_s})")
147
+
148
+ print(f"\n{'='*60}")
149
+ if all_ok:
150
+ print(" ALL POSTERIORS VERIFIED ✓")
151
+ print(" - Beta(1+k, 1+n-k) with uniform prior Beta(1,1)")
152
+ print(" - HTML percentage→k round-trip: all match")
153
+ print(" - Matches TRI draw_samples_from_beta_posterior()")
154
+ else:
155
+ print(" ✗ SOME MISMATCHES FOUND — see above")
156
+ print(f"{'='*60}")
export_wandb_losses.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import wandb
2
+ import json
3
+
4
+ wandb.login()
5
+
6
+ api = wandb.Api()
7
+ project = "pepijn1999kooijmans-open-universiteit/shirt-folding"
8
+
9
+ TARGET_RUNS = [
10
+ "ablation2-4_100k_q",
11
+ "ablation2-5_0",
12
+ "ablation2-1_100k_q",
13
+ "ablation2-2_100k",
14
+ "ablation2-3_100k_q_it",
15
+ "ablation1-7_2",
16
+ "ablation1-5_9",
17
+ "ablation1_3_17_q",
18
+ "ablation1_2_2",
19
+ "ablation1_1_2",
20
+ "ablation1-4",
21
+ ]
22
+
23
+ runs = api.runs(project)
24
+ print(f"Found {len(runs)} total runs in project\n")
25
+
26
+ all_data = {}
27
+ for run in runs:
28
+ if run.name not in TARGET_RUNS:
29
+ continue
30
+ print(f"Pulling: {run.name} ({run.id})...")
31
+ try:
32
+ df = run.history(samples=2000, pandas=True)
33
+ except Exception as e:
34
+ print(f" Error: {e}")
35
+ continue
36
+
37
+ loss_keys = [c for c in df.columns if "loss" in c.lower()]
38
+ if not loss_keys:
39
+ print(f" No loss keys. Available: {df.columns.tolist()}")
40
+ continue
41
+
42
+ lk = loss_keys[0]
43
+ print(f" Using key: {lk}")
44
+ points = []
45
+ for _, row in df.iterrows():
46
+ v = row.get(lk)
47
+ if v is not None and str(v) != "nan":
48
+ points.append({"step": int(row["_step"]), "loss": float(v)})
49
+
50
+ all_data[run.name] = {"id": run.id, "name": run.name, "points": points}
51
+ print(f" Got {len(points)} points")
52
+
53
+ found = set(all_data.keys())
54
+ missing = [r for r in TARGET_RUNS if r not in found]
55
+ if missing:
56
+ print(f"\nWARNING: Could not find runs: {missing}")
57
+
58
+ out = "loss_curves.json"
59
+ with open(out, "w") as f:
60
+ json.dump(all_data, f, indent=2)
61
+
62
+ print(f"\nDone! Saved {len(all_data)} runs to {out}")
63
+ print(f"\nNow paste the contents of {out} into the RAW_DATA variable in:")
64
+ print(f" app/src/content/embeds/folding/loss-curves.html")
loss_curves.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e5c135761e5e9a42b01e47bcbd5ee4eb50827717fbebce203be2b9e04f8866e7
3
+ size 76562