jimmy60504 commited on
Commit
7fab05e
·
verified ·
1 Parent(s): 787c6a2

Update QML Classifier Explorer

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. ANALYSIS.md +204 -0
  3. ANSWERS.md +20 -0
  4. README.md +40 -4
  5. assets/preview_boundaries.png +3 -0
  6. assets/preview_datasets.png +0 -0
  7. assets/preview_ui_sketch.png +3 -0
  8. css/base.css +146 -0
  9. css/charts.css +238 -0
  10. css/components.css +490 -0
  11. css/overlays.css +194 -0
  12. data/runtime_source.template.json +8 -0
  13. data/viewer_data.template.json +29 -0
  14. data/viewer_manifest.template.json +29 -0
  15. index.html +258 -17
  16. js/app.js +423 -0
  17. js/charts.js +201 -0
  18. js/data.js +297 -0
  19. js/dom.js +88 -0
  20. js/overlays.js +164 -0
  21. runtime/chunks/q2-le2-lr2-e50_epoch_0000.json +0 -0
  22. runtime/chunks/q2-le2-lr2-e50_epoch_0001.json +0 -0
  23. runtime/chunks/q2-le2-lr2-e50_epoch_0002.json +0 -0
  24. runtime/chunks/q2-le2-lr2-e50_epoch_0003.json +0 -0
  25. runtime/chunks/q2-le2-lr2-e50_epoch_0004.json +0 -0
  26. runtime/chunks/q2-le2-lr2-e50_epoch_0005.json +0 -0
  27. runtime/chunks/q2-le2-lr2-e50_epoch_0006.json +0 -0
  28. runtime/chunks/q2-le2-lr2-e50_epoch_0007.json +0 -0
  29. runtime/chunks/q2-le2-lr2-e50_epoch_0008.json +0 -0
  30. runtime/chunks/q2-le2-lr2-e50_epoch_0009.json +0 -0
  31. runtime/chunks/q2-le2-lr2-e50_epoch_0010.json +0 -0
  32. runtime/chunks/q2-le2-lr2-e50_epoch_0011.json +0 -0
  33. runtime/chunks/q2-le2-lr2-e50_epoch_0012.json +0 -0
  34. runtime/chunks/q2-le2-lr2-e50_epoch_0013.json +0 -0
  35. runtime/chunks/q2-le2-lr2-e50_epoch_0014.json +0 -0
  36. runtime/chunks/q2-le2-lr2-e50_epoch_0015.json +0 -0
  37. runtime/chunks/q2-le2-lr2-e50_epoch_0016.json +0 -0
  38. runtime/chunks/q2-le2-lr2-e50_epoch_0017.json +0 -0
  39. runtime/chunks/q2-le2-lr2-e50_epoch_0018.json +0 -0
  40. runtime/chunks/q2-le2-lr2-e50_epoch_0019.json +0 -0
  41. runtime/chunks/q2-le2-lr2-e50_epoch_0020.json +0 -0
  42. runtime/chunks/q2-le2-lr2-e50_epoch_0021.json +0 -0
  43. runtime/chunks/q2-le2-lr2-e50_epoch_0022.json +0 -0
  44. runtime/chunks/q2-le2-lr2-e50_epoch_0023.json +0 -0
  45. runtime/chunks/q2-le2-lr2-e50_epoch_0024.json +0 -0
  46. runtime/chunks/q2-le2-lr2-e50_epoch_0025.json +0 -0
  47. runtime/chunks/q2-le2-lr2-e50_epoch_0026.json +0 -0
  48. runtime/chunks/q2-le2-lr2-e50_epoch_0027.json +0 -0
  49. runtime/chunks/q2-le2-lr2-e50_epoch_0028.json +0 -0
  50. runtime/chunks/q2-le2-lr2-e50_epoch_0029.json +0 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/preview_boundaries.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/preview_ui_sketch.png filter=lfs diff=lfs merge=lfs -text
ANALYSIS.md ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Problem 2 分析
2
+
3
+ ## 題目背景
4
+
5
+ 這題要比較三種主流的量子機器學習(QML)分類方法,在兩個二元分類資料集上的表現。題目的核心問題是:**量子電路要怎麼「看」輸入資料?** 三種方法對這個問題的回答完全不同。
6
+
7
+ - **Explicit**:一開始 encode 一次,之後全靠 variational block 去學
8
+ - **Kernel**:完全不訓練,用量子電路定義一個 inner product,然後丟給 classical SVM
9
+ - **Reuploading**:每一層都重新把資料 encode 進去,讓量子態隨資料一起演化
10
+
11
+ 這三種方法的比較是現在 QML 領域真正在研究的核心問題之一,不是 toy demo。
12
+
13
+ ---
14
+
15
+ ## 資料集
16
+
17
+ 兩個資料集都是 2D 二元分類,200 個點,70/30 train/test split,所有特徵都經過 `StandardScaler` 正規化。
18
+
19
+ ### Circle
20
+
21
+ ```
22
+ Label = 1 if ‖x‖ < r, where r = √(2/π) ≈ 0.798
23
+ ```
24
+
25
+ 在 `[−1, 1]²` 上均勻取樣。這個半徑不是隨便選的:圓的面積 = πr² = 2,正方形面積 = 4,所以剛好一半點在圓內,一半在外,**類別完全平衡**。
26
+
27
+ 邊界是一條完美的圓弧,是光滑的非線性邊界,但在概念上非常乾淨。對量子模型來說,這是個幾何測試:你的表示空間能不能把「遠離原點」跟「靠近原點」分開?
28
+
29
+ ### Moons
30
+
31
+ sklearn 的 `make_moons(noise=0.1)`。兩個半月形互相交叉,邊界是非凸的,沒有辦法直接用一條曲線分開,需要兩個獨立的局部決策。這對所有方法都是更硬的挑戰。
32
+
33
+ ---
34
+
35
+ ## 三個方法
36
+
37
+ ### 方法一:Explicit Quantum Model
38
+
39
+ **架構概念**:先把資料 encode 進量子態,再用一組 variational block 去轉動,最後量測。
40
+
41
+ **電路(L 層)**:
42
+
43
+ ```
44
+ q0: ─[RX(x₀)]──[RY(θ)]─[RZ(θ)]─●──[RY(θ)]─[RZ(θ)]─●── ··· ─⟨Z₀⟩
45
+ │ │
46
+ q1: ─[RX(x₁)]──[RY(θ)]─[RZ(θ)]─X──[RY(θ)]─[RZ(θ)]─X── ···
47
+ ↑ encode └──────── layer 1 ─────────┘└──── layer 2 ────┘
48
+ once
49
+ ```
50
+
51
+ - **Encoding**:固定在第 0 層之前,`RX(x₀)` 和 `RX(x₁)` 各做一次
52
+ - **每層**:每個 qubit 做 `RY(θ)` + `RZ(θ)`,然後一個 `CNOT(0→1)` 負責 qubit 間糾纏
53
+ - **輸出**:量測 `⟨Z₀⟩`,映射為 class-1 的機率:`p = (⟨Z₀⟩ + 1) / 2`
54
+ - **Optimizer**:Adam,BCE loss
55
+ - **可訓練參數**:`4L`(L 層 × 2 qubits × 2 角度)
56
+
57
+ **關鍵限制**:資料只在最前面進入量子電路一次,之後 variational block 完全不再「看到」原始資料。這代表整個電路的表達能力受到初始 encoding 的 Hilbert space 嚴格限制。電路能學的邊界形狀,完全取決於 `RX(x₀), RX(x₁)` 初始化後的量子態空間。
58
+
59
+ ---
60
+
61
+ ### 方法二:Implicit Quantum Kernel (IQK)
62
+
63
+ **架構概念**:不訓練量子電路,而是用電路定義一個 **kernel function**,讓兩點 xᵢ, xⱼ 的「量子相似度」決定分類邊界。Classical SVM 負責找最優超平面。
64
+
65
+ **Feature map S(x)**:
66
+
67
+ ```
68
+ q0: ─[H]─[RZ(x₀)]──────────────
69
+
70
+ q1: ─[H]─[RZ(x₁)]─[CNOT]─[RZ(x₀·x₁)]─
71
+ ```
72
+
73
+ `H ⊗ H` 先把兩個 qubit 放進均勻疊加態,`RZ` 旋轉對應到資料的每個維度,`CNOT + RZ(x₀·x₁)` 加入交叉項,讓 kernel 能捕捉 x₀ 和 x₁ 的乘積關係。
74
+
75
+ **Kernel 定義**:
76
+
77
+ ```
78
+ k(xᵢ, xⱼ) = |⟨0 | S†(xᵢ) S(xⱼ) | 0⟩|²
79
+ = Pr( measure |00⟩ after S†(xᵢ)·S(xⱼ) )
80
+ ```
81
+
82
+ 把 S(xⱼ) 先做完,再做 S(xᵢ) 的反轉,量測回到 `|00⟩` 的機率。兩點「越相似」,機率越高,kernel 值越大。
83
+
84
+ **訓練流程**:
85
+ 1. 算出訓練集完整的 `n×n` kernel matrix(需要 n² 次電路評估)
86
+ 2. 把這個 matrix 丟給 `SVC(kernel="precomputed")` 做訓練
87
+ 3. Inference 時只對測試點跟 **support vectors** 計算 kernel(不是全部訓練點)
88
+
89
+ **可訓練參數**:**0 個量子參數**。整個 feature map 是固定的,不做任何梯度更新。模型容量來自 SVM 的 dual coefficients,其數量等於 support vector 的數量。
90
+
91
+ **複雜度指標**:`count_model_complexity()` 回傳 kernel evaluations 的次數(訓練時 n²,推論時 n × n_sv)。
92
+
93
+ ---
94
+
95
+ ### 方法三:Data Reuploading Classifier
96
+
97
+ **架構概念**:每一層都重新把資料 encode 進去,讓資料和量子態深度交織。每次 encode 的 scale 和 bias 都是可學的,讓模型能自動決定「這個 encoding 的重要性」。
98
+
99
+ **電路(L 層)**:
100
+
101
+ ```
102
+ q0: ─[RX(x₀·s₀⁽¹⁾+b₀⁽¹⁾)]─[RY(θ)]─[RZ(θ)]─●─[RX(x₀·s₀⁽²⁾+b₀⁽²⁾)]─ ··· ─⟨Z₀⟩
103
+
104
+ q1: ─[RX(x₁·s₁⁽¹⁾+b₁⁽¹⁾)]─[RY(θ)]─[RZ(θ)]─X─[RX(x₁·s₁⁽²⁾+b₁⁽²⁾)]─ ···
105
+ └────────────��─── layer 1 ────────────────┘└──── layer 2 ────────
106
+ ```
107
+
108
+ - **每層 encode**:`RX(xᵢ · scale[l,i] + bias[l,i])`,scale 和 bias 都是可訓練的
109
+ - **每層旋轉**:同 explicit,`RY(θ)` + `RZ(θ)` per qubit
110
+ - **每層糾纏**:`CNOT(0→1)`
111
+ - **輸出**:`⟨Z₀⟩` → 機率(同 explicit)
112
+ - **Optimizer**:Adam,BCE loss
113
+ - **可訓練參數**:`8L`(L 層 × 2 qubits × (1 scale + 1 bias + 2 rotations))
114
+
115
+ **為什麼 reuploading 更強?**
116
+
117
+ 在 Explicit 模型裡,資料只出現一次,相當於電路只能使用固定頻率成分。Reuploading 每層都重新 inject,理論上等價於一個 truncated Fourier series:**層數越多,可以近似的頻率越高,決策邊界可以越複雜**。
118
+
119
+ Pérez-Salinas et al. (2020) 證明了單一 qubit 的 data reuploading 是 universal approximator,前提是層數夠多。
120
+
121
+ ---
122
+
123
+ ## 三種方法的本質差異
124
+
125
+ | | Explicit | Kernel | Reuploading |
126
+ |---|---|---|---|
127
+ | **資料進入電路** | 一次(固定 encoding) | 隱式(定義相似度) | 每層一次 |
128
+ | **訓練方式** | 梯度下降 (Adam) | 無量子訓練(SVM) | 梯度下降 (Adam) |
129
+ | **可訓練量子參數** | 4L | 0 | 8L |
130
+ | **表達能力隨 L** | 有限增長 | 固定(取決於 feature map) | Fourier 頻率隨 L 線性增加 |
131
+ | **訓練複雜度** | O(n·L) per epoch | O(n²) 一次性 | O(n·L) per epoch |
132
+ | **推論複雜度** | O(L) per point | O(n_sv) per point | O(L) per point |
133
+
134
+ ---
135
+
136
+ ## 看 viewer 時的注意點
137
+
138
+ **Slider**:拖動 slider 可以看訓練過程中決策邊界是怎麼形成的。Explicit 和 Reuploading 會隨 epoch 演化,Kernel 的邊界是固定的(只在 epoch 0 計算一次,之後複用)。
139
+
140
+ **6 個 panel 的對比邏輯**:
141
+ - 同一 row(同個 dataset):三種方法在相同資料上的表現差異
142
+ - 同一 column(同個方法):同一方法面對不同幾何結構的適應能力
143
+
144
+ **Circle vs Moons 的預期差異**:Circle 的邊界光滑,相對容易;Moons 非凸,需要更高的表達能力。特別值得觀察 Kernel 在 Moons 上的邊界能不能捕捉非凸結構,以及 Explicit 在層數不夠時是否會失敗。
145
+
146
+ **Reuploading 的早期行為**:scale 初始化為 1、bias 初始化為 0,早期 epoch 的邊界可能很不穩定,後期才收斂成有意義的形狀。這個過程和 Explicit 的演化方式會明顯不同。
147
+
148
+ ---
149
+
150
+ ## 結果討論
151
+
152
+ ### 數字總覽
153
+
154
+ | Run | LE | LR | Explicit Circle | Explicit Moons | Kernel Circle | Kernel Moons | Reup Circle | Reup Moons |
155
+ |---|---|---|---|---|---|---|---|---|
156
+ | q2-le2-lr2 | 2 | 2 | 70.0% | 83.3% | 96.7% | 85.0% | 78.3% | 93.3% |
157
+ | q2-le4-lr4 | 4 | 4 | 75.0% | 83.3% | 96.7% | 85.0% | 91.7% | 98.3% |
158
+ | q2-le4-lr8 | 4 | 8 | 75.0% | 83.3% | 96.7% | 85.0% | 96.7% | **100%** |
159
+ | q2-le6-lr6 | 6 | 6 | 73.3% | 85.0% | 96.7% | 85.0% | 96.7% | 98.3% |
160
+
161
+ ---
162
+
163
+ ### Explicit:一次 encoding 的代價
164
+
165
+ Explicit 在所有 run 裡表現最差,且**幾乎不隨層數增加而改善**——Circle 從 L=2 的 70% 到 L=4 的 75%,之後就停在那裡;Moons 更是從頭到尾卡在 83–85%。
166
+
167
+ 這直接反映它的架構限制:資料在最前面用 `RX(x₀), RX(x₁)` encode 一次後,後面的 variational block 完全看不到原始資料,只能在初始量子態的 Hilbert space 裡轉來轉去。問題在於 `RX` 是獨立旋轉兩個 qubit,**沒有直接 encode `‖x‖² = x₀² + x₁²`**,而 Circle 的邊界正好是一個等半徑圓。這個資訊被 encoding 本身丟掉了,後面再多層都補不回來。
168
+
169
+ Moons 上 Explicit 能到 83–85%,比 Circle 好,是因為 `CNOT` 糾纏讓兩個 qubit 之間有互動,對局部結構稍微好一點。但非凸邊界對它來說仍然太難。
170
+
171
+ ---
172
+
173
+ ### Kernel:不訓練的底氣
174
+
175
+ Kernel 的數字在所有 run 都一樣(Circle 96.7%、Moons 85.0%),因為它的量子電路是固定的,feature map 和 kernel matrix 不隨 LE/LR 改變。
176
+
177
+ Circle 上 96.7% 很亮眼。ZZFeatureMap 用了 `H + RZ(xᵢ) + CNOT + RZ(x₀·x₁)` 的結構,cross term `x₀·x₁` 等價於引入 `cos(θ)sin(θ)` 型的非線性,再經過 kernel 的 inner product,能夠隱式地「感覺到」資料點離原點的距離,讓 SVM 在這個空間找到一個很好的超平面。
178
+
179
+ Moons 上卡在 85%。非凸邊界需要兩個獨立的局部決策,但固定的 kernel 只有一個 feature space,SVM 能用的支持向量有限,沒辦法做出兩個分離的決策區域。這是 **kernel expressibility 的天花板**,不是訓練問題。
180
+
181
+ **有趣的 reversal**:在 Circle 上,Kernel(96.7%)大勝小層數的 Reuploading(L=2 只有 78.3%)。這說明針對特定幾何結構設計的固定 feature map,可以比沒有學夠的 variational 模型更有效率。
182
+
183
+ ---
184
+
185
+ ### Reuploading:層數的力量
186
+
187
+ Reuploading 是三個方法裡唯一**隨層數有系統性提升**的:
188
+
189
+ - **Circle**:L=2 → 78.3%,L=4 → 91.7%,L=6/8 → 96.7%
190
+ - **Moons**:L=2 → 93.3%,L=4 → 98.3%,L=8 → **100%**
191
+
192
+ 這和理論預測完全一致。每多一層 reuploading,Fourier 近似可以用的最高頻率就增加一次,決策邊界可以描述的形狀就更複雜。Moons 在 L=8 達到 100%,代表 8 個 Fourier 頻率已經足夠描述這個非凸邊界。
193
+
194
+ 值得注意的是 LR=8 比 LR=6 在 Moons 上多了 1.7%(100% vs 98.3%),而在 Circle 上兩者持平(96.7%)。這說明 Circle 在 L=6 時已經學夠了,Moons 需要多一點頻率才能消除剩下的錯誤。
195
+
196
+ ---
197
+
198
+ ### 整體結論
199
+
200
+ 這三種方法的結果清楚地反映了各自的設計哲學:
201
+
202
+ - **Explicit 的天花板來自 encoding**,不是 variational 層數。在資訊進入電路的那一刻就決定了模型能學什麼。
203
+ - **Kernel 的強項是針對特定幾何結構的隱式表達**,但 feature map 的選擇決定上限,沒有辦法靠訓練突破。
204
+ - **Reuploading 是最靈活的**,代價是需要更多層(更多電路深度)來達到同樣的效果。在 NISQ 裝置上電路深度越深,noise 越嚴重,這個 trade-off 在真實硬體上會更明顯。
ANSWERS.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Problem 2 解題作答
2
+
3
+ _作答將在實驗完成後填入。_
4
+
5
+ ## (a) Fig. 6 重現(circle dataset)
6
+
7
+ ## (b) Decision boundaries(3×2 grid)
8
+
9
+ ## (c) 比較表
10
+
11
+ | Method | Dataset | Test Acc | Params / Kernel evals | Training time |
12
+ |--------|---------|----------|-----------------------|---------------|
13
+ | Explicit | Circle | — | — | — |
14
+ | Explicit | Moons | — | — | — |
15
+ | Implicit Kernel | Circle | — | — | — |
16
+ | Implicit Kernel | Moons | — | — | — |
17
+ | Data Reuploading | Circle | — | — | — |
18
+ | Data Reuploading | Moons | — | — | — |
19
+
20
+ ## (d) 討論
README.md CHANGED
@@ -1,10 +1,46 @@
1
  ---
2
  title: QML Classifier Explorer
3
- emoji: 🌍
4
- colorFrom: pink
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: QML Classifier Explorer
3
+ emoji: 🔬
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
+ # QML Classifier Explorer
11
+
12
+ Static Hugging Face viewer for **HW1 Problem 2** — comparing three quantum
13
+ machine learning classification methods on two datasets.
14
+
15
+ ## What this experiment is about
16
+
17
+ Three QML approaches are evaluated on binary classification:
18
+
19
+ | Method | Description |
20
+ |--------|-------------|
21
+ | **Explicit Quantum Model** | Encoding circuit S(x) + trainable W(θ) + measurement |
22
+ | **Implicit Quantum Kernel** | Fixed encoding kernel k(xi, xj) passed to SVM |
23
+ | **Data Reuploading** | Interleaved encoding and trainable layers (Ref. [4]) |
24
+
25
+ Two datasets:
26
+ - **Circle** — concentric ring structure (as used in Ref. [4])
27
+ - **Moons** — `sklearn.datasets.make_moons(noise=0.1, n_samples=200)`
28
+
29
+ ## Viewer contents
30
+
31
+ 1. **Decision boundary grid** — 3 methods × 2 datasets (6 plots)
32
+ 2. **Training curve** — accuracy and loss vs epoch, with step slider
33
+ 3. **Comparison table** — test accuracy, trainable parameters, training time
34
+
35
+ ## Generating runtime data
36
+
37
+ Run on `gx10`:
38
+
39
+ ```bash
40
+ # training + export
41
+ ssh gx10 "cd ~/quantum_computing && GX10_DOCKER_NETWORK=gx10-mlflow ./scripts/gx10_run_py.sh HW1/problem2/train.py --run-name q2-l4-e50 --tracking-uri http://gx10-mlflow-server:5001"
42
+ ```
43
+
44
+ The export populates `runtime/viewer_data.json` which the viewer picks up
45
+ automatically. Without a runtime export the viewer shows the template
46
+ placeholder.
assets/preview_boundaries.png ADDED

Git LFS Details

  • SHA256: f4482f22892166bbcd4596d5fdeee5abdf37d3c9372aae2dda8c21247bd0c5c3
  • Pointer size: 131 Bytes
  • Size of remote file: 186 kB
assets/preview_datasets.png ADDED
assets/preview_ui_sketch.png ADDED

Git LFS Details

  • SHA256: 65424191b513a36092343e0d3d4899ae578b5b3ab7eb8d5494795f744304c649
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
css/base.css ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #f4efe5;
3
+ --bg-accent: #e5f0e8;
4
+ --panel: rgba(255, 252, 245, 0.92);
5
+ --panel-strong: rgba(255, 248, 234, 0.98);
6
+ --text: #17211d;
7
+ --muted: #51615b;
8
+ --line: rgba(23, 33, 29, 0.12);
9
+ --brand: #0d8f71;
10
+ --brand-soft: rgba(13, 143, 113, 0.14);
11
+ --accent: #ef8354;
12
+ --shadow: 0 18px 60px rgba(20, 30, 28, 0.08);
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ height: 100vh;
22
+ overflow: hidden;
23
+ font-family: "IBM Plex Sans", sans-serif;
24
+ color: var(--text);
25
+ background:
26
+ radial-gradient(circle at top left, rgba(239, 131, 84, 0.2), transparent 28%),
27
+ radial-gradient(circle at top right, rgba(13, 143, 113, 0.18), transparent 24%),
28
+ linear-gradient(135deg, var(--bg) 0%, var(--bg-accent) 100%);
29
+ }
30
+
31
+ .page-shell {
32
+ display: grid;
33
+ grid-template-columns: minmax(300px, 360px) 1fr;
34
+ gap: 20px;
35
+ padding: 24px;
36
+ height: 100vh;
37
+ min-height: 0;
38
+ }
39
+
40
+ .control-rail,
41
+ .content-pane {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 20px;
45
+ min-height: 0;
46
+ }
47
+
48
+ .control-rail {
49
+ overflow-y: auto;
50
+ overflow-x: hidden;
51
+ padding-right: 4px;
52
+ }
53
+
54
+ .content-pane {
55
+ overflow: hidden;
56
+ }
57
+
58
+ .hero-card,
59
+ .panel-card,
60
+ .canvas-card {
61
+ border: 1px solid var(--line);
62
+ background: var(--panel);
63
+ backdrop-filter: blur(14px);
64
+ border-radius: 24px;
65
+ box-shadow: var(--shadow);
66
+ }
67
+
68
+ .hero-card {
69
+ padding: 14px 18px;
70
+ background: linear-gradient(160deg, rgba(255, 252, 245, 0.98), rgba(245, 255, 250, 0.94));
71
+ }
72
+
73
+ .panel-card {
74
+ padding: 20px;
75
+ }
76
+
77
+ .canvas-card {
78
+ padding: 22px;
79
+ min-height: 0;
80
+ }
81
+
82
+ .eyebrow {
83
+ margin: 0 0 4px;
84
+ font-size: 0.72rem;
85
+ font-weight: 700;
86
+ letter-spacing: 0.12em;
87
+ text-transform: uppercase;
88
+ color: var(--brand);
89
+ }
90
+
91
+ h1,
92
+ h2 {
93
+ margin: 0;
94
+ font-family: "Space Grotesk", sans-serif;
95
+ }
96
+
97
+ h1 {
98
+ font-size: clamp(1.35rem, 2vw, 1.8rem);
99
+ line-height: 1.05;
100
+ }
101
+
102
+ h2 {
103
+ font-size: 1.3rem;
104
+ }
105
+
106
+ .muted {
107
+ color: var(--muted);
108
+ }
109
+
110
+ .hero-card .muted {
111
+ margin: 4px 0 0;
112
+ font-size: 0.88rem;
113
+ line-height: 1.25;
114
+ }
115
+
116
+ /* ── Mobile layout ────────────────────────────────────────────────────────── */
117
+ @media (max-width: 768px) {
118
+ body {
119
+ height: auto;
120
+ min-height: 100vh;
121
+ overflow: auto;
122
+ }
123
+
124
+ .page-shell {
125
+ grid-template-columns: 1fr;
126
+ height: auto;
127
+ min-height: 0;
128
+ padding: 12px;
129
+ gap: 12px;
130
+ overflow: visible;
131
+ }
132
+
133
+ .control-rail,
134
+ .content-pane {
135
+ overflow: visible;
136
+ gap: 12px;
137
+ }
138
+
139
+ .canvas-card {
140
+ padding: 14px;
141
+ }
142
+
143
+ .panel-card {
144
+ padding: 14px;
145
+ }
146
+ }
css/charts.css ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ── canvas-card flex column (lets children fill height) ────────────────── */
2
+
3
+ .content-pane > .canvas-card {
4
+ display: flex;
5
+ flex-direction: column;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .content-pane > .canvas-card:first-child {
10
+ flex: 2 1 0;
11
+ min-height: 0;
12
+ }
13
+
14
+ .content-pane > .canvas-card:last-child {
15
+ flex: 1 1 0;
16
+ min-height: 0;
17
+ }
18
+
19
+ /* ── decision-boundary grid ─────────────────────────────────────────────── */
20
+
21
+ .boundary-grid {
22
+ display: grid;
23
+ grid-template-columns: repeat(3, minmax(0, 1fr));
24
+ grid-template-rows: repeat(2, minmax(0, 1fr));
25
+ gap: 10px;
26
+ flex: 1 1 auto;
27
+ min-height: 0;
28
+ overflow: hidden;
29
+ margin-top: 10px;
30
+ }
31
+
32
+ .boundary-card {
33
+ position: relative;
34
+ border: 1px solid var(--line);
35
+ border-radius: 14px;
36
+ background: rgba(255, 255, 255, 0.55);
37
+ overflow: hidden;
38
+ min-height: 0;
39
+ }
40
+
41
+ .boundary-header {
42
+ position: absolute;
43
+ top: 0;
44
+ left: 0;
45
+ right: 0;
46
+ z-index: 10;
47
+ display: flex;
48
+ align-items: center;
49
+ padding: 3px 8px;
50
+ font-size: 0.76rem;
51
+ font-weight: 700;
52
+ letter-spacing: 0.04em;
53
+ gap: 6px;
54
+ background: rgba(255, 255, 255, 0.72);
55
+ backdrop-filter: blur(4px);
56
+ border-radius: 13px 13px 0 0;
57
+ pointer-events: none;
58
+ }
59
+
60
+ .boundary-method-tag {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ height: 20px;
64
+ padding: 0 7px;
65
+ border-radius: 999px;
66
+ font-size: 0.68rem;
67
+ font-weight: 700;
68
+ letter-spacing: 0.06em;
69
+ text-transform: uppercase;
70
+ background: var(--brand-soft);
71
+ color: var(--brand);
72
+ }
73
+
74
+ .boundary-method-tag.tag-kernel {
75
+ background: rgba(239, 131, 84, 0.12);
76
+ color: #8a4b10;
77
+ }
78
+
79
+ .boundary-method-tag.tag-explicit {
80
+ background: rgba(91, 80, 200, 0.12);
81
+ color: #3a2f8a;
82
+ }
83
+
84
+ .boundary-plot {
85
+ position: absolute;
86
+ inset: 0;
87
+ width: 100%;
88
+ height: 100%;
89
+ overflow: hidden;
90
+ }
91
+
92
+ /* ── accuracy pill ──────────────────────────────────────────────────────── */
93
+
94
+ .acc-pill {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ height: 20px;
98
+ padding: 0 7px;
99
+ border-radius: 999px;
100
+ font-size: 0.76rem;
101
+ font-weight: 700;
102
+ font-variant-numeric: tabular-nums;
103
+ background: rgba(23, 33, 29, 0.07);
104
+ color: var(--text);
105
+ margin-left: auto;
106
+ }
107
+
108
+ /* ── loss / accuracy chart ──────────────────────────────────────────────── */
109
+
110
+ .chart-wrap {
111
+ flex: 1 1 auto;
112
+ min-height: 0;
113
+ position: relative;
114
+ display: flex;
115
+ flex-direction: column;
116
+ padding-top: 4px;
117
+ }
118
+
119
+ #loss-chart {
120
+ flex: 1 1 auto;
121
+ min-height: 0;
122
+ width: 100%;
123
+ border-radius: 18px;
124
+ border: 1px solid rgba(23, 33, 29, 0.08);
125
+ background:
126
+ linear-gradient(180deg, rgba(13, 143, 113, 0.05), rgba(13, 143, 113, 0)),
127
+ white;
128
+ }
129
+
130
+ .plot-surface {
131
+ width: 100%;
132
+ height: 100%;
133
+ }
134
+
135
+ .plot-surface-wide {
136
+ min-height: 160px;
137
+ flex: 1 1 auto;
138
+ }
139
+
140
+ .chart-empty {
141
+ position: absolute;
142
+ inset: 4px 0 0;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ padding: 24px;
147
+ font-size: 0.9rem;
148
+ color: var(--muted);
149
+ text-align: center;
150
+ border: 1px dashed rgba(23, 33, 29, 0.18);
151
+ border-radius: 18px;
152
+ background: rgba(255, 252, 245, 0.85);
153
+ }
154
+
155
+ .chart-empty[hidden] {
156
+ display: none !important;
157
+ }
158
+
159
+ /* ── boundary home buttons ──────────────────────────────────────────────── */
160
+
161
+ .boundary-home-row {
162
+ display: flex;
163
+ gap: 6px;
164
+ align-items: center;
165
+ margin-left: auto;
166
+ }
167
+
168
+ .home-btn {
169
+ padding: 3px 11px;
170
+ border: 1px solid var(--line);
171
+ border-radius: 999px;
172
+ background: white;
173
+ color: var(--muted);
174
+ font: inherit;
175
+ font-size: 0.76rem;
176
+ font-weight: 700;
177
+ cursor: pointer;
178
+ transition: background 120ms ease, color 120ms ease;
179
+ }
180
+
181
+ .home-btn:hover {
182
+ background: var(--brand-soft);
183
+ color: var(--brand);
184
+ }
185
+
186
+ /* ── dataset toggle ─────────────────────────────────────────────────────── */
187
+
188
+ .dataset-toggle {
189
+ display: inline-flex;
190
+ gap: 4px;
191
+ padding: 4px;
192
+ border-radius: 999px;
193
+ background: rgba(23, 33, 29, 0.07);
194
+ flex: 0 0 auto;
195
+ }
196
+
197
+ .dataset-toggle-btn {
198
+ padding: 4px 14px;
199
+ border: none;
200
+ border-radius: 999px;
201
+ background: transparent;
202
+ color: var(--muted);
203
+ font: inherit;
204
+ font-size: 0.82rem;
205
+ font-weight: 700;
206
+ cursor: pointer;
207
+ transition: background 160ms ease, color 160ms ease;
208
+ }
209
+
210
+ .dataset-toggle-btn.is-active {
211
+ background: white;
212
+ color: var(--brand);
213
+ box-shadow: 0 2px 8px rgba(20, 30, 28, 0.1);
214
+ }
215
+
216
+ /* ── responsive ─────────────────────────────────────────────────────────── */
217
+
218
+ @media (max-width: 1100px) {
219
+ body {
220
+ height: auto;
221
+ overflow: auto;
222
+ }
223
+
224
+ .page-shell {
225
+ grid-template-columns: 1fr;
226
+ height: auto;
227
+ }
228
+
229
+ .content-pane > .canvas-card:first-child,
230
+ .content-pane > .canvas-card:last-child {
231
+ flex: none;
232
+ min-height: 480px;
233
+ }
234
+
235
+ .boundary-grid {
236
+ grid-template-rows: repeat(3, 240px);
237
+ }
238
+ }
css/components.css ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .hero-title-row {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 10px;
5
+ }
6
+
7
+ .hero-info-wrap {
8
+ position: relative;
9
+ display: inline-flex;
10
+ align-items: center;
11
+ gap: 8px;
12
+ flex: 0 0 auto;
13
+ }
14
+
15
+ .hero-info-button {
16
+ display: inline-flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ width: 28px;
20
+ height: 28px;
21
+ padding: 0;
22
+ border: 1px solid rgba(23, 33, 29, 0.12);
23
+ border-radius: 999px;
24
+ background: rgba(255, 255, 255, 0.72);
25
+ color: var(--brand);
26
+ font-family: "Space Grotesk", sans-serif;
27
+ font-size: 0.95rem;
28
+ font-weight: 700;
29
+ cursor: pointer;
30
+ transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
31
+ flex: 0 0 auto;
32
+ animation: infoPulse 2.4s ease-in-out infinite;
33
+ }
34
+
35
+ .hero-info-button:hover {
36
+ background: rgba(13, 143, 113, 0.12);
37
+ transform: translateY(-1px);
38
+ box-shadow: 0 10px 22px rgba(20, 30, 28, 0.1);
39
+ }
40
+
41
+ .hero-info-button:focus-visible {
42
+ outline: 2px solid rgba(13, 143, 113, 0.35);
43
+ outline-offset: 2px;
44
+ }
45
+
46
+ .hero-info-button-secondary {
47
+ color: #8a4b0f;
48
+ border-color: rgba(138, 75, 15, 0.16);
49
+ background: rgba(255, 246, 230, 0.88);
50
+ animation: none;
51
+ }
52
+
53
+ .hero-info-button-secondary:hover {
54
+ background: rgba(232, 158, 33, 0.16);
55
+ }
56
+
57
+ .hero-info-button-secondary:focus-visible {
58
+ outline-color: rgba(232, 158, 33, 0.35);
59
+ }
60
+
61
+ .analysis-hint {
62
+ position: absolute;
63
+ top: calc(100% + 10px);
64
+ right: -6px;
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 8px;
68
+ padding: 8px 10px 8px 12px;
69
+ border-radius: 14px;
70
+ border: 1px solid rgba(23, 33, 29, 0.1);
71
+ background: rgba(255, 252, 245, 0.98);
72
+ color: var(--text);
73
+ box-shadow: 0 18px 36px rgba(20, 30, 28, 0.12);
74
+ white-space: nowrap;
75
+ z-index: 4;
76
+ animation: hintFloat 2.8s ease-in-out infinite;
77
+ }
78
+
79
+ .analysis-hint::before {
80
+ content: "";
81
+ position: absolute;
82
+ top: -6px;
83
+ right: 14px;
84
+ width: 12px;
85
+ height: 12px;
86
+ background: rgba(255, 252, 245, 0.98);
87
+ border-top: 1px solid rgba(23, 33, 29, 0.1);
88
+ border-left: 1px solid rgba(23, 33, 29, 0.1);
89
+ transform: rotate(45deg);
90
+ }
91
+
92
+ .analysis-hint-text {
93
+ position: relative;
94
+ z-index: 1;
95
+ font-size: 0.8rem;
96
+ font-weight: 600;
97
+ color: var(--muted);
98
+ }
99
+
100
+ .analysis-hint-close {
101
+ position: relative;
102
+ z-index: 1;
103
+ width: 20px;
104
+ height: 20px;
105
+ padding: 0;
106
+ border: 0;
107
+ border-radius: 999px;
108
+ background: rgba(23, 33, 29, 0.08);
109
+ color: var(--text);
110
+ font-size: 0.95rem;
111
+ line-height: 1;
112
+ cursor: pointer;
113
+ }
114
+
115
+ @keyframes infoPulse {
116
+
117
+ 0%,
118
+ 100% {
119
+ box-shadow: 0 0 0 0 rgba(13, 143, 113, 0.0);
120
+ transform: translateY(0);
121
+ }
122
+
123
+ 45% {
124
+ box-shadow: 0 0 0 8px rgba(13, 143, 113, 0.0), 0 0 0 0 rgba(13, 143, 113, 0.16);
125
+ transform: translateY(-1px);
126
+ }
127
+
128
+ 60% {
129
+ box-shadow: 0 0 0 10px rgba(13, 143, 113, 0.0), 0 0 0 0 rgba(13, 143, 113, 0.0);
130
+ }
131
+ }
132
+
133
+ @keyframes hintFloat {
134
+
135
+ 0%,
136
+ 100% {
137
+ transform: translateY(0);
138
+ }
139
+
140
+ 50% {
141
+ transform: translateY(-2px);
142
+ }
143
+ }
144
+
145
+ .panel-header,
146
+ .section-heading {
147
+ display: flex;
148
+ align-items: flex-start;
149
+ justify-content: space-between;
150
+ gap: 16px;
151
+ }
152
+
153
+ .status-pill {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ min-height: 32px;
158
+ padding: 0 12px;
159
+ border-radius: 999px;
160
+ background: var(--brand-soft);
161
+ color: var(--brand);
162
+ font-size: 0.84rem;
163
+ font-weight: 700;
164
+ }
165
+
166
+ .loading-panel {
167
+ display: grid;
168
+ gap: 8px;
169
+ margin: 12px 0 16px;
170
+ }
171
+
172
+ .loading-panel[hidden] {
173
+ display: none !important;
174
+ }
175
+
176
+ .loading-meta {
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: space-between;
180
+ gap: 12px;
181
+ font-size: 0.92rem;
182
+ }
183
+
184
+ .loading-meta strong {
185
+ font-size: 0.94rem;
186
+ }
187
+
188
+ .loading-track {
189
+ overflow: hidden;
190
+ width: 100%;
191
+ height: 10px;
192
+ border-radius: 999px;
193
+ background: rgba(23, 33, 29, 0.08);
194
+ }
195
+
196
+ .loading-bar {
197
+ width: 0%;
198
+ height: 100%;
199
+ border-radius: inherit;
200
+ background: linear-gradient(90deg, var(--brand) 0%, var(--accent) 100%);
201
+ transition: width 180ms ease;
202
+ }
203
+
204
+ .slider-label,
205
+ .meta-label {
206
+ display: block;
207
+ margin-bottom: 6px;
208
+ font-size: 0.8rem;
209
+ font-weight: 600;
210
+ color: var(--muted);
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.08em;
213
+ }
214
+
215
+ input[type="range"] {
216
+ width: 100%;
217
+ margin: 8px 0 12px;
218
+ accent-color: var(--brand);
219
+ }
220
+
221
+ .run-select {
222
+ width: 100%;
223
+ margin: 8px 0 12px;
224
+ padding: 12px 14px;
225
+ border: 1px solid rgba(23, 33, 29, 0.12);
226
+ border-radius: 14px;
227
+ background: rgba(255, 255, 255, 0.9);
228
+ color: var(--text);
229
+ font: inherit;
230
+ }
231
+
232
+ .hidden-control {
233
+ display: none !important;
234
+ }
235
+
236
+ [hidden] {
237
+ display: none !important;
238
+ }
239
+
240
+ .results-table-wrap {
241
+ overflow-x: auto;
242
+ overflow-y: auto;
243
+ margin: 8px 0 12px;
244
+ border: 1px solid rgba(23, 33, 29, 0.08);
245
+ border-radius: 16px;
246
+ background: rgba(255, 255, 255, 0.7);
247
+ max-height: 26vh;
248
+ }
249
+
250
+ .results-table {
251
+ width: 100%;
252
+ min-width: 100%;
253
+ border-collapse: collapse;
254
+ font-size: 0.8rem;
255
+ }
256
+
257
+ .results-table th,
258
+ .results-table td {
259
+ padding: 10px 10px;
260
+ text-align: left;
261
+ border-bottom: 1px solid rgba(23, 33, 29, 0.08);
262
+ white-space: nowrap;
263
+ }
264
+
265
+ .results-table th {
266
+ position: sticky;
267
+ top: 0;
268
+ z-index: 1;
269
+ background: rgba(255, 248, 234, 0.98);
270
+ font-size: 0.74rem;
271
+ letter-spacing: 0.06em;
272
+ text-transform: uppercase;
273
+ color: var(--muted);
274
+ }
275
+
276
+ .results-table tbody tr {
277
+ cursor: pointer;
278
+ transition: background 160ms ease, color 160ms ease;
279
+ }
280
+
281
+ .results-table tbody tr:hover {
282
+ background: rgba(13, 143, 113, 0.08);
283
+ }
284
+
285
+ .results-table tbody tr.is-selected {
286
+ background: rgba(13, 143, 113, 0.14);
287
+ }
288
+
289
+ .results-table td.metric-cell {
290
+ font-variant-numeric: tabular-nums;
291
+ }
292
+
293
+ .results-table td.metric-cell-strong {
294
+ font-weight: 700;
295
+ color: var(--brand);
296
+ }
297
+
298
+ .results-table td.run-cell {
299
+ max-width: 88px;
300
+ overflow: hidden;
301
+ text-overflow: ellipsis;
302
+ }
303
+
304
+ .step-meta {
305
+ display: grid;
306
+ grid-template-columns: repeat(2, minmax(0, 1fr));
307
+ gap: 12px;
308
+ margin-bottom: 12px;
309
+ }
310
+
311
+ .step-meta strong {
312
+ font-size: 1rem;
313
+ }
314
+
315
+ .timeline-controls {
316
+ display: grid;
317
+ gap: 4px;
318
+ margin-top: 2px;
319
+ flex: 0 0 auto;
320
+ }
321
+
322
+ .timeline-meta-row {
323
+ display: flex;
324
+ flex-wrap: wrap;
325
+ gap: 8px;
326
+ }
327
+
328
+ .timeline-meta-item {
329
+ display: inline-flex;
330
+ align-items: flex-start;
331
+ gap: 8px;
332
+ padding: 7px 9px;
333
+ border-radius: 14px;
334
+ background: rgba(255, 255, 255, 0.6);
335
+ border: 1px solid rgba(23, 33, 29, 0.08);
336
+ }
337
+
338
+ .timeline-meta-item strong {
339
+ font-size: 0.95rem;
340
+ }
341
+
342
+ .timeline-title-row {
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 10px;
346
+ flex-wrap: nowrap;
347
+ min-width: 0;
348
+ }
349
+
350
+ .timeline-slider-inline {
351
+ display: flex;
352
+ align-items: center;
353
+ flex: 0 0 200px;
354
+ min-width: 160px;
355
+ max-width: 200px;
356
+ }
357
+
358
+ .timeline-slider-inline input[type="range"] {
359
+ width: 100%;
360
+ margin: 0;
361
+ }
362
+
363
+ .timeline-title-pill {
364
+ display: inline-flex;
365
+ align-items: center;
366
+ min-height: 28px;
367
+ padding: 0 10px;
368
+ border-radius: 999px;
369
+ background: rgba(13, 143, 113, 0.1);
370
+ color: var(--brand);
371
+ font-size: 0.85rem;
372
+ font-weight: 700;
373
+ }
374
+
375
+ .timeline-title-metrics {
376
+ font-size: 0.88rem;
377
+ line-height: 1.25;
378
+ color: var(--muted);
379
+ }
380
+
381
+ .timeline-metrics-row {
382
+ display: inline-flex;
383
+ align-items: center;
384
+ gap: 8px;
385
+ flex: 0 1 auto;
386
+ min-width: 0;
387
+ flex-wrap: nowrap;
388
+ }
389
+
390
+ .timeline-title-metrics-status {
391
+ flex: 0 1 auto;
392
+ white-space: nowrap;
393
+ }
394
+
395
+ .timeline-metric-pill {
396
+ background: rgba(23, 33, 29, 0.06);
397
+ color: var(--text);
398
+ font-weight: 600;
399
+ white-space: nowrap;
400
+ }
401
+
402
+ .timeline-metric-pill-test {
403
+ background: rgba(239, 131, 84, 0.12);
404
+ color: #9a4d28;
405
+ }
406
+
407
+ .section-heading-compact {
408
+ margin-top: 2px;
409
+ }
410
+
411
+ .meta-list {
412
+ display: grid;
413
+ grid-template-columns: minmax(0, 1fr);
414
+ gap: 10px;
415
+ margin: 0;
416
+ }
417
+
418
+ .experiment-card {
419
+ display: flex;
420
+ flex-direction: column;
421
+ flex: 1 1 auto;
422
+ min-height: 0;
423
+ }
424
+
425
+ .experiment-reference-stack {
426
+ margin-bottom: 0;
427
+ }
428
+
429
+ .experiment-scroll {
430
+ display: grid;
431
+ gap: 16px;
432
+ overflow: auto;
433
+ flex: 1 1 auto;
434
+ min-height: 0;
435
+ padding-right: 4px;
436
+ }
437
+
438
+ .meta-list div {
439
+ display: grid;
440
+ gap: 4px;
441
+ padding: 12px 14px;
442
+ border-radius: 16px;
443
+ background: rgba(255, 255, 255, 0.55);
444
+ border: 1px solid rgba(23, 33, 29, 0.08);
445
+ }
446
+
447
+ .meta-list dt {
448
+ font-size: 0.8rem;
449
+ font-weight: 700;
450
+ color: var(--muted);
451
+ text-transform: uppercase;
452
+ letter-spacing: 0.08em;
453
+ }
454
+
455
+ .meta-list dd {
456
+ margin: 0;
457
+ font-size: 0.98rem;
458
+ line-height: 1.4;
459
+ }
460
+
461
+ .reference-stack {
462
+ display: grid;
463
+ gap: 14px;
464
+ }
465
+
466
+ .reference-figure {
467
+ margin: 0;
468
+ display: grid;
469
+ gap: 8px;
470
+ }
471
+
472
+ .reference-figure img {
473
+ width: 100%;
474
+ display: block;
475
+ border-radius: 18px;
476
+ border: 1px solid rgba(23, 33, 29, 0.08);
477
+ background: white;
478
+ cursor: zoom-in;
479
+ transition: transform 160ms ease, box-shadow 160ms ease;
480
+ }
481
+
482
+ .reference-figure img:hover {
483
+ transform: translateY(-1px);
484
+ box-shadow: 0 12px 28px rgba(20, 30, 28, 0.14);
485
+ }
486
+
487
+ .reference-figure figcaption {
488
+ font-size: 0.9rem;
489
+ color: var(--muted);
490
+ }
css/overlays.css ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .image-lightbox {
2
+ position: fixed;
3
+ inset: 0;
4
+ display: grid;
5
+ place-items: center;
6
+ padding: 24px;
7
+ z-index: 10010;
8
+ }
9
+
10
+ .image-lightbox[hidden] {
11
+ display: none !important;
12
+ }
13
+
14
+ .image-lightbox-backdrop {
15
+ position: absolute;
16
+ inset: 0;
17
+ background: rgba(23, 33, 29, 0.56);
18
+ backdrop-filter: blur(4px);
19
+ }
20
+
21
+ .image-lightbox-dialog {
22
+ position: relative;
23
+ z-index: 1;
24
+ display: grid;
25
+ gap: 10px;
26
+ width: min(92vw, 1120px);
27
+ max-height: calc(100vh - 48px);
28
+ padding: 18px 18px 14px;
29
+ border-radius: 24px;
30
+ border: 1px solid rgba(255, 255, 255, 0.2);
31
+ background: rgba(255, 252, 245, 0.98);
32
+ box-shadow: 0 32px 80px rgba(20, 30, 28, 0.35);
33
+ }
34
+
35
+ .image-lightbox-close {
36
+ position: absolute;
37
+ top: 10px;
38
+ right: 10px;
39
+ width: 36px;
40
+ height: 36px;
41
+ border: 0;
42
+ border-radius: 999px;
43
+ background: rgba(23, 33, 29, 0.08);
44
+ color: var(--text);
45
+ font-size: 1.5rem;
46
+ line-height: 1;
47
+ cursor: pointer;
48
+ }
49
+
50
+ .image-lightbox-image {
51
+ width: 100%;
52
+ height: auto;
53
+ max-height: calc(100vh - 132px);
54
+ object-fit: contain;
55
+ border-radius: 18px;
56
+ background: white;
57
+ }
58
+
59
+ .image-lightbox-caption {
60
+ margin: 0;
61
+ text-align: center;
62
+ color: var(--muted);
63
+ font-size: 0.92rem;
64
+ }
65
+
66
+ .analysis-modal {
67
+ position: fixed;
68
+ inset: 0;
69
+ display: grid;
70
+ place-items: center;
71
+ padding: 24px;
72
+ z-index: 10000;
73
+ }
74
+
75
+ .analysis-modal[hidden] {
76
+ display: none !important;
77
+ }
78
+
79
+ .analysis-modal-backdrop {
80
+ position: absolute;
81
+ inset: 0;
82
+ background: rgba(23, 33, 29, 0.56);
83
+ backdrop-filter: blur(6px);
84
+ }
85
+
86
+ .analysis-modal-dialog {
87
+ position: relative;
88
+ z-index: 1;
89
+ width: min(900px, calc(100vw - 48px));
90
+ max-height: calc(100vh - 48px);
91
+ border-radius: 28px;
92
+ border: 1px solid rgba(255, 255, 255, 0.24);
93
+ background: rgba(255, 252, 245, 0.98);
94
+ box-shadow: 0 32px 84px rgba(20, 30, 28, 0.34);
95
+ overflow: hidden;
96
+ }
97
+
98
+ .analysis-modal-close {
99
+ position: absolute;
100
+ top: 12px;
101
+ right: 12px;
102
+ width: 38px;
103
+ height: 38px;
104
+ border: 0;
105
+ border-radius: 999px;
106
+ background: rgba(23, 33, 29, 0.08);
107
+ color: var(--text);
108
+ font-size: 1.55rem;
109
+ line-height: 1;
110
+ cursor: pointer;
111
+ }
112
+
113
+ .analysis-modal-content {
114
+ overflow: auto;
115
+ max-height: calc(100vh - 48px);
116
+ padding: 28px 30px 28px;
117
+ }
118
+
119
+ .analysis-modal-label {
120
+ margin: 0 0 14px;
121
+ font-size: 0.78rem;
122
+ font-weight: 700;
123
+ letter-spacing: 0.08em;
124
+ text-transform: uppercase;
125
+ color: var(--muted);
126
+ }
127
+
128
+ .analysis-markdown {
129
+ color: var(--text);
130
+ line-height: 1.75;
131
+ }
132
+
133
+ .analysis-markdown h1,
134
+ .analysis-markdown h2,
135
+ .analysis-markdown h3 {
136
+ font-family: "Space Grotesk", sans-serif;
137
+ line-height: 1.15;
138
+ }
139
+
140
+ .analysis-markdown h1 {
141
+ margin: 0 0 12px;
142
+ font-size: 1.62rem;
143
+ }
144
+
145
+ .analysis-markdown h2 {
146
+ margin: 28px 0 10px;
147
+ font-size: 1.1rem;
148
+ padding-top: 18px;
149
+ border-top: 1px solid rgba(23, 33, 29, 0.08);
150
+ }
151
+
152
+ .analysis-markdown h2:first-child,
153
+ .analysis-markdown h1+h2 {
154
+ border-top: 0;
155
+ padding-top: 0;
156
+ }
157
+
158
+ .analysis-markdown p {
159
+ margin: 0 0 14px;
160
+ }
161
+
162
+ .analysis-markdown img {
163
+ display: block;
164
+ width: min(100%, 760px);
165
+ margin: 18px auto 14px;
166
+ border-radius: 18px;
167
+ border: 1px solid rgba(23, 33, 29, 0.1);
168
+ box-shadow: 0 12px 30px rgba(23, 33, 29, 0.08);
169
+ cursor: zoom-in;
170
+ transition: transform 160ms ease, box-shadow 160ms ease;
171
+ }
172
+
173
+ .analysis-markdown img:hover {
174
+ transform: translateY(-1px);
175
+ box-shadow: 0 16px 34px rgba(20, 30, 28, 0.14);
176
+ }
177
+
178
+ .analysis-markdown ul,
179
+ .analysis-markdown ol {
180
+ margin: 0 0 14px 1.2rem;
181
+ padding: 0;
182
+ }
183
+
184
+ .analysis-markdown li+li {
185
+ margin-top: 6px;
186
+ }
187
+
188
+ .analysis-markdown code {
189
+ padding: 0.12rem 0.36rem;
190
+ border-radius: 8px;
191
+ background: rgba(23, 33, 29, 0.06);
192
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
193
+ font-size: 0.92em;
194
+ }
data/runtime_source.template.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "local",
3
+ "runtime_root_url": "./",
4
+ "manifest_path": "./runtime/viewer_manifest.json",
5
+ "fallback_manifest_urls": [
6
+ "./data/viewer_manifest.template.json"
7
+ ]
8
+ }
data/viewer_data.template.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "QML Classifier Explorer",
3
+ "subtitle": "QCAA HW1 Problem 2 classification results",
4
+ "status": "template export",
5
+ "description": "The viewer first looks for a gx10-generated runtime export. If no runtime data exists, it falls back to this lightweight template.",
6
+ "experiment": {
7
+ "model": "PennyLane QML classifiers",
8
+ "task": "Binary classification (circle and moons datasets)",
9
+ "methods": [
10
+ "explicit",
11
+ "kernel",
12
+ "reuploading"
13
+ ],
14
+ "datasets": [
15
+ "circle",
16
+ "moons"
17
+ ],
18
+ "device": "configurable: default.qubit or lightning.qubit",
19
+ "note": "Run training on gx10 to populate hf_space_hw1_problem2/runtime/viewer_data.json."
20
+ },
21
+ "assets": {
22
+ "datasets_overview": "assets/preview_datasets.png",
23
+ "boundaries_grid": "assets/preview_boundaries.png"
24
+ },
25
+ "grid": {
26
+ "resolution": 50
27
+ },
28
+ "timeline_steps": []
29
+ }
data/viewer_manifest.template.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "QML Classifier Runs",
3
+ "default_run": "template",
4
+ "runs": [
5
+ {
6
+ "id": "template",
7
+ "label": "Template / No runtime export yet",
8
+ "path": "./data/viewer_data.template.json",
9
+ "steps": 0,
10
+ "method": "explicit + kernel + reuploading",
11
+ "dataset": "circle + moons",
12
+ "methods": [
13
+ "explicit",
14
+ "kernel",
15
+ "reuploading"
16
+ ],
17
+ "datasets": [
18
+ "circle",
19
+ "moons"
20
+ ],
21
+ "num_qubits": 2,
22
+ "num_layers": 4,
23
+ "num_layers_explicit": 4,
24
+ "num_layers_reuploading": 4,
25
+ "num_params": null,
26
+ "best_test_acc": null
27
+ }
28
+ ]
29
+ }
index.html CHANGED
@@ -1,19 +1,260 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>QML Classifier Explorer</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;700&display=swap"
12
+ rel="stylesheet" />
13
+ <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
14
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
15
+ <link rel="stylesheet" href="css/base.css" />
16
+ <link rel="stylesheet" href="css/components.css" />
17
+ <link rel="stylesheet" href="css/overlays.css" />
18
+ <link rel="stylesheet" href="css/charts.css" />
19
+ </head>
20
+
21
+ <body>
22
+ <div class="page-shell">
23
+
24
+ <!-- ── left rail ──────────────────────────────────────────────────── -->
25
+ <aside class="control-rail">
26
+
27
+ <div class="hero-card">
28
+ <p class="eyebrow">D11224001 Huang, Chun-Ming</p>
29
+ <div class="hero-title-row">
30
+ <h1 id="page-title">QML Classifier Explorer</h1>
31
+ <div class="hero-info-wrap">
32
+ <button id="analysis-open" class="hero-info-button" type="button"
33
+ aria-label="Open analysis notes" title="Open analysis notes">i</button>
34
+ <button id="answers-open" class="hero-info-button hero-info-button-secondary"
35
+ type="button" aria-label="Open assignment answers" title="Open assignment answers">?</button>
36
+ <div id="analysis-hint" class="analysis-hint" hidden>
37
+ <span class="analysis-hint-text">Tap here for notes</span>
38
+ <button id="analysis-hint-close" class="analysis-hint-close" type="button"
39
+ aria-label="Dismiss analysis hint">×</button>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <p id="page-subtitle" class="muted">HW1 Problem 2 results are loading…</p>
44
+ </div>
45
+
46
+ <!-- results / comparison table -->
47
+ <div class="panel-card">
48
+ <div class="panel-header">
49
+ <h2>Results</h2>
50
+ <span id="export-status" class="status-pill">loading</span>
51
+ </div>
52
+ <div id="loading-panel" class="loading-panel" hidden>
53
+ <div class="loading-meta">
54
+ <strong id="loading-label">Preparing viewer…</strong>
55
+ <span id="loading-percent">0%</span>
56
+ </div>
57
+ <div class="loading-track" aria-hidden="true">
58
+ <div id="loading-bar" class="loading-bar"></div>
59
+ </div>
60
+ </div>
61
+ <label class="slider-label hidden-control" for="run-select">Run</label>
62
+ <select id="run-select" class="run-select hidden-control"></select>
63
+ <div class="results-table-wrap">
64
+ <table class="results-table">
65
+ <thead>
66
+ <tr>
67
+ <th title="Best test accuracy">Acc</th>
68
+ <th title="Number of qubits">Q</th>
69
+ <th title="Explicit encoding layers">LE</th>
70
+ <th title="Re-uploading layers">LR</th>
71
+ <th title="Training methods">Method</th>
72
+ <th title="Datasets used">Dataset</th>
73
+ <th title="Run name">Run</th>
74
+ </tr>
75
+ </thead>
76
+ <tbody id="results-table-body"></tbody>
77
+ </table>
78
+ </div>
79
+ <p id="run-note" class="muted">
80
+ Choose a runtime export to compare different configurations.
81
+ </p>
82
+ </div>
83
+
84
+ <!-- experiment meta + dataset overview image -->
85
+ <div class="panel-card experiment-card">
86
+ <div class="panel-header">
87
+ <h2>Experiment</h2>
88
+ </div>
89
+ <div class="experiment-scroll">
90
+ <div class="reference-stack experiment-reference-stack">
91
+ <figure class="reference-figure">
92
+ <img id="datasets-image" class="previewable-image" alt="Dataset overview"
93
+ data-preview-caption="Circle and moons datasets" />
94
+ <figcaption>Dataset overview</figcaption>
95
+ </figure>
96
+ </div>
97
+ <dl id="experiment-meta" class="meta-list"></dl>
98
+ </div>
99
+ </div>
100
+
101
+ </aside>
102
+
103
+ <!-- ── main pane ───────────────────────────────���──────────────────── -->
104
+ <main class="content-pane">
105
+
106
+ <!-- decision boundary grid -->
107
+ <section class="canvas-card">
108
+ <div class="section-heading">
109
+ <div>
110
+ <h2>Decision Boundaries</h2>
111
+ </div>
112
+ <div class="boundary-home-row">
113
+ <button id="home-btn-circle" class="home-btn" type="button" title="Circle: isometric view">⌂ Circle</button>
114
+ <button id="top-btn-circle" class="home-btn" type="button" title="Circle: top-down view">⊤ Circle</button>
115
+ <button id="home-btn-moons" class="home-btn" type="button" title="Moons: isometric view">⌂ Moons</button>
116
+ <button id="top-btn-moons" class="home-btn" type="button" title="Moons: top-down view">⊤ Moons</button>
117
+ </div>
118
+ </div>
119
+
120
+ <!--
121
+ 3 rows (methods) × 2 columns (datasets)
122
+ Row labels are rendered as boundary-header method tags.
123
+ -->
124
+ <div class="boundary-grid">
125
+
126
+ <!-- Row 1: Circle dataset ─────────────────────────── -->
127
+
128
+ <!-- Explicit — Circle -->
129
+ <article class="boundary-card">
130
+ <div class="boundary-header">
131
+ <span class="boundary-method-tag tag-explicit">Explicit</span>
132
+ Circle
133
+ <span id="acc-explicit-circle" class="acc-pill">—</span>
134
+ </div>
135
+ <div id="bp-explicit-circle" class="boundary-plot"></div>
136
+ </article>
137
+
138
+ <!-- Kernel — Circle -->
139
+ <article class="boundary-card">
140
+ <div class="boundary-header">
141
+ <span class="boundary-method-tag tag-kernel">Kernel</span>
142
+ Circle
143
+ <span id="acc-kernel-circle" class="acc-pill">—</span>
144
+ </div>
145
+ <div id="bp-kernel-circle" class="boundary-plot"></div>
146
+ </article>
147
+
148
+ <!-- Reuploading — Circle -->
149
+ <article class="boundary-card">
150
+ <div class="boundary-header">
151
+ <span class="boundary-method-tag">Reuploading</span>
152
+ Circle
153
+ <span id="acc-reupload-circle" class="acc-pill">—</span>
154
+ </div>
155
+ <div id="bp-reupload-circle" class="boundary-plot"></div>
156
+ </article>
157
+
158
+ <!-- Row 2: Moons dataset ──────────────────────────── -->
159
+
160
+ <!-- Explicit — Moons -->
161
+ <article class="boundary-card">
162
+ <div class="boundary-header">
163
+ <span class="boundary-method-tag tag-explicit">Explicit</span>
164
+ Moons
165
+ <span id="acc-explicit-moons" class="acc-pill">—</span>
166
+ </div>
167
+ <div id="bp-explicit-moons" class="boundary-plot"></div>
168
+ </article>
169
+
170
+ <!-- Kernel — Moons -->
171
+ <article class="boundary-card">
172
+ <div class="boundary-header">
173
+ <span class="boundary-method-tag tag-kernel">Kernel</span>
174
+ Moons
175
+ <span id="acc-kernel-moons" class="acc-pill">—</span>
176
+ </div>
177
+ <div id="bp-kernel-moons" class="boundary-plot"></div>
178
+ </article>
179
+
180
+ <!-- Reuploading — Moons -->
181
+ <article class="boundary-card">
182
+ <div class="boundary-header">
183
+ <span class="boundary-method-tag">Reuploading</span>
184
+ Moons
185
+ <span id="acc-reupload-moons" class="acc-pill">—</span>
186
+ </div>
187
+ <div id="bp-reupload-moons" class="boundary-plot"></div>
188
+ </article>
189
+
190
+ </div>
191
+ </section>
192
+
193
+ <!-- accuracy / loss chart -->
194
+ <section class="canvas-card">
195
+ <div class="section-heading">
196
+ <div>
197
+ <div class="timeline-title-row">
198
+ <h2>Accuracy &amp; Loss</h2>
199
+ <span id="current-step-label" class="timeline-title-pill">Final snapshot</span>
200
+ <div class="timeline-slider-inline">
201
+ <input id="step-slider" type="range" min="0" max="0" value="0" disabled />
202
+ </div>
203
+ <div id="timeline-metrics" class="timeline-metrics-row">
204
+ <span id="timeline-caption" class="timeline-title-metrics timeline-title-metrics-status">
205
+ Waiting for per-step trajectory export.
206
+ </span>
207
+ <span id="train-acc-pill" class="timeline-title-pill timeline-metric-pill" hidden>
208
+ Train acc —
209
+ </span>
210
+ <span id="test-acc-pill"
211
+ class="timeline-title-pill timeline-metric-pill timeline-metric-pill-test" hidden>
212
+ Test acc —
213
+ </span>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ <div class="chart-wrap">
219
+ <div id="loss-chart" class="plot-surface plot-surface-wide"></div>
220
+ <div id="chart-empty" class="chart-empty">
221
+ No step timeline exported yet. The viewer is ready to replay training
222
+ snapshots once <code>timeline_steps</code> are populated.
223
+ </div>
224
+ </div>
225
+ </section>
226
+
227
+ </main>
228
+ </div>
229
+
230
+ <!-- image lightbox -->
231
+ <div id="image-lightbox" class="image-lightbox" hidden aria-hidden="true">
232
+ <div class="image-lightbox-backdrop" data-lightbox-close="true"></div>
233
+ <div class="image-lightbox-dialog" role="dialog" aria-modal="true" aria-label="Image preview">
234
+ <button id="image-lightbox-close" class="image-lightbox-close" type="button"
235
+ aria-label="Close image preview">×</button>
236
+ <img id="image-lightbox-image" class="image-lightbox-image" alt="Preview" />
237
+ <p id="image-lightbox-caption" class="image-lightbox-caption"></p>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- analysis modal -->
242
+ <div id="analysis-modal" class="analysis-modal" hidden aria-hidden="true">
243
+ <div class="analysis-modal-backdrop" data-analysis-close="true"></div>
244
+ <div class="analysis-modal-dialog" role="dialog" aria-modal="true"
245
+ aria-label="Problem 2 analysis">
246
+ <button id="analysis-close" class="analysis-modal-close" type="button"
247
+ aria-label="Close analysis">×</button>
248
+ <div class="analysis-modal-content">
249
+ <div id="analysis-modal-label" class="analysis-modal-label">Analysis notes</div>
250
+ <div id="analysis-markdown" class="analysis-markdown">
251
+ Loading analysis notes…
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ <script src="js/app.js" type="module"></script>
258
+ </body>
259
+
260
  </html>
js/app.js ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ pageTitle, pageSubtitle, exportStatus,
3
+ runSelect, runNote, resultsTableBody,
4
+ stepSlider, currentStepLabel,
5
+ timelineCaption, trainAccPill, testAccPill, chartEmpty,
6
+ experimentMeta, datasetsImage,
7
+ boundaryPlots, accPills,
8
+ lossChart,
9
+ homeButtons,
10
+ topButtons,
11
+ state,
12
+ } from "./dom.js";
13
+ import { bindImageLightbox, bindAnalysisModal, maybeShowAnalysisHint } from "./overlays.js";
14
+ import { renderBoundarySurface, renderAccuracyChart, renderEmptyState, DEFAULT_CAMERA, MOONS_DEFAULT_CAMERA, TOP_CAMERA, MOONS_TOP_CAMERA } from "./charts.js";
15
+ import {
16
+ formatMetric, formatInteger, appendMetaRow, withCacheBust,
17
+ setLoadingState, loadManifest, loadRunData, loadRunChunk, loadRuntimeSource,
18
+ } from "./data.js";
19
+
20
+ const METHODS = ["explicit", "kernel", "reuploading"];
21
+ const DATASETS = ["circle", "moons"];
22
+
23
+ // ── helpers ───────────────────────────────────────────────────────────────────
24
+
25
+ function formatAcc(v) {
26
+ if (v === null || v === undefined) return "—";
27
+ return (v * 100).toFixed(1) + "%";
28
+ }
29
+
30
+ // ── chunk helpers ─────────────────────────────────────────────────────────────
31
+
32
+ function getChunkPaths(data) {
33
+ return (data.timeline_chunks || []).map((c) => c.path).filter(Boolean);
34
+ }
35
+
36
+ function updatePrefetchProgress(completed, total) {
37
+ if (!total) return;
38
+ const percent = 82 + (completed / total) * 18;
39
+ setLoadingState({
40
+ visible: completed < total,
41
+ label: `Streaming epoch chunks ${completed}/${total}`,
42
+ percent,
43
+ status: completed < total ? "streaming" : "ready",
44
+ });
45
+ }
46
+
47
+ async function fetchChunkIntoCache(chunkPath, loadToken, { announce = false } = {}) {
48
+ if (state.currentRunChunkCache[chunkPath]) {
49
+ return state.currentRunChunkCache[chunkPath];
50
+ }
51
+ if (state.currentRunChunkInflight[chunkPath]) {
52
+ return state.currentRunChunkInflight[chunkPath];
53
+ }
54
+
55
+ if (announce) {
56
+ const epochMatch = chunkPath.match(/_epoch_(\d+)\.json$/);
57
+ const epochLabel = epochMatch ? String(Number(epochMatch[1])) : "?";
58
+ setLoadingState({
59
+ visible: true,
60
+ label: `Loading epoch ${epochLabel} boundaries`,
61
+ percent: 92,
62
+ status: "rendering",
63
+ });
64
+ }
65
+
66
+ const promise = loadRunChunk(chunkPath, loadToken)
67
+ .then((chunk) => {
68
+ state.currentRunChunkCache[chunkPath] = chunk;
69
+ delete state.currentRunChunkInflight[chunkPath];
70
+ return chunk;
71
+ })
72
+ .catch((error) => {
73
+ delete state.currentRunChunkInflight[chunkPath];
74
+ throw error;
75
+ });
76
+
77
+ state.currentRunChunkInflight[chunkPath] = promise;
78
+ return promise;
79
+ }
80
+
81
+ function prefetchChunkStream(data, loadToken, prefetchToken) {
82
+ const chunkPaths = getChunkPaths(data);
83
+ if (!chunkPaths.length) return;
84
+
85
+ const pump = async () => {
86
+ updatePrefetchProgress(Object.keys(state.currentRunChunkCache).length, chunkPaths.length);
87
+ for (const chunkPath of chunkPaths) {
88
+ if (prefetchToken !== state.activePrefetchToken || loadToken !== state.activeLoadToken) return;
89
+ await fetchChunkIntoCache(chunkPath, loadToken);
90
+ updatePrefetchProgress(Object.keys(state.currentRunChunkCache).length, chunkPaths.length);
91
+ if (prefetchToken !== state.activePrefetchToken || loadToken !== state.activeLoadToken) return;
92
+ await new Promise((resolve) => window.setTimeout(resolve, 0));
93
+ }
94
+ updatePrefetchProgress(chunkPaths.length, chunkPaths.length);
95
+ };
96
+
97
+ pump().catch((error) => console.warn("Chunk prefetch stopped:", error));
98
+ }
99
+
100
+ /**
101
+ * Resolve the full step payload for a given index.
102
+ * - Inline mode: step already has `boundaries` → return as-is.
103
+ * - Chunked mode: step has `chunk_path` → load chunk, find step by epoch.
104
+ */
105
+ async function resolveStepPayload(data, index, loadToken) {
106
+ const steps = data.timeline_steps || [];
107
+ const summaryStep = steps[index];
108
+ if (!summaryStep) return null;
109
+
110
+ // Inline: boundaries already present
111
+ if (summaryStep.boundaries) return summaryStep;
112
+
113
+ // Chunked: fetch from chunk file
114
+ if (!summaryStep.chunk_path) return summaryStep;
115
+
116
+ const chunkPath = summaryStep.chunk_path;
117
+ const chunk = await fetchChunkIntoCache(chunkPath, loadToken, { announce: true });
118
+ const chunkSteps = chunk.timeline_steps || [];
119
+ const targetEpoch = Number(summaryStep.epoch ?? summaryStep.global_step ?? index);
120
+ const fullStep = chunkSteps.find(
121
+ (s) => Number(s.epoch ?? s.global_step) === targetEpoch
122
+ );
123
+ if (!fullStep) {
124
+ throw new Error(`Epoch ${targetEpoch} missing from chunk ${chunkPath}`);
125
+ }
126
+ return { ...fullStep, chunk_path: chunkPath };
127
+ }
128
+
129
+ function renderResultsTable(runs, selectedRunId) {
130
+ if (!resultsTableBody) return;
131
+ resultsTableBody.innerHTML = "";
132
+ runs.forEach((run) => {
133
+ const row = document.createElement("tr");
134
+ row.dataset.runId = run.id;
135
+ if (run.id === selectedRunId) row.classList.add("is-selected");
136
+
137
+ const values = [
138
+ { text: formatAcc(run.best_test_acc ?? run.final_test_acc), className: "metric-cell metric-cell-strong" },
139
+ { text: formatInteger(run.num_qubits) },
140
+ { text: formatInteger(run.num_layers_explicit ?? run.num_layers) },
141
+ { text: formatInteger(run.num_layers_reuploading ?? run.num_layers) },
142
+ { text: (run.methods ?? [run.method]).filter(Boolean).join(", ") || "—" },
143
+ { text: (run.datasets ?? [run.dataset]).filter(Boolean).join(" + ") || "—" },
144
+ { text: run.label || run.id, className: "run-cell" },
145
+ ];
146
+
147
+ values.forEach(({ text, className }) => {
148
+ const cell = document.createElement("td");
149
+ cell.textContent = text;
150
+ if (className) cell.className = className;
151
+ row.appendChild(cell);
152
+ });
153
+
154
+ row.addEventListener("click", async () => {
155
+ runSelect.value = run.id;
156
+ await applyRun(run.id);
157
+ });
158
+
159
+ resultsTableBody.appendChild(row);
160
+ });
161
+ }
162
+
163
+ function updateAccPills(step) {
164
+ METHODS.forEach((method, mi) => {
165
+ DATASETS.forEach((dataset, di) => {
166
+ const pill = accPills[mi]?.[di];
167
+ if (!pill) return;
168
+ const acc = step?.accuracies?.[method]?.[dataset] ?? null;
169
+ pill.textContent = formatAcc(acc);
170
+ });
171
+ });
172
+ }
173
+
174
+ // ── step rendering ─────────────────────────────────────────────────────────────
175
+
176
+ async function refreshStepState(data, index, stepToken) {
177
+ const steps = data.timeline_steps || [];
178
+ if (!steps.length) {
179
+ renderEmptyState();
180
+ return;
181
+ }
182
+
183
+ // Render summary metrics immediately (no chunk needed)
184
+ const summaryStep = steps[index];
185
+ if (currentStepLabel) currentStepLabel.textContent = summaryStep.label || `Epoch ${summaryStep.epoch ?? index}`;
186
+ if (timelineCaption) timelineCaption.hidden = true;
187
+
188
+ if (trainAccPill) {
189
+ trainAccPill.textContent = `Train ${formatAcc(summaryStep.train_acc)}`;
190
+ trainAccPill.hidden = false;
191
+ }
192
+ if (testAccPill) {
193
+ testAccPill.textContent = `Test ${formatAcc(summaryStep.test_acc)}`;
194
+ testAccPill.hidden = false;
195
+ }
196
+
197
+ renderAccuracyChart(steps, index);
198
+ updateAccPills(summaryStep);
199
+
200
+ // Resolve full step payload (may require a chunk fetch)
201
+ const step = await resolveStepPayload(data, index, state.activeLoadToken);
202
+ if (stepToken !== state.activeStepToken) return;
203
+
204
+ if (!step?.boundaries) {
205
+ // Chunked data not yet available — leave boundary panels as-is
206
+ return;
207
+ }
208
+
209
+ // Scatter lives at top level (fixed) or falls back to per-step copy
210
+ const scatterSource = data.scatter ?? {};
211
+
212
+ // Decision boundaries (3D surface)
213
+ METHODS.forEach((method, mi) => {
214
+ DATASETS.forEach((dataset, di) => {
215
+ const container = boundaryPlots[mi]?.[di];
216
+ if (!container) return;
217
+ const heatmap = step.boundaries?.[method]?.[dataset] ?? null;
218
+ const points = scatterSource[dataset] ?? step.scatter?.[dataset] ?? null;
219
+ renderBoundarySurface(container, heatmap, points, method, state.cameraState[di]);
220
+ });
221
+ });
222
+
223
+ // Attach camera-sync listeners once per panel (idempotent)
224
+ DATASETS.forEach((_, di) => bindCameraSync(di));
225
+
226
+ if (chartEmpty) chartEmpty.hidden = true;
227
+ if (lossChart) lossChart.style.display = "";
228
+ setLoadingState({ visible: false });
229
+ }
230
+
231
+ // ── run loading ────────────────────────────────────────────────────────────────
232
+
233
+ function populateExperimentMeta(data, run) {
234
+ if (!experimentMeta) return;
235
+ experimentMeta.innerHTML = "";
236
+ appendMetaRow("Model", data.experiment?.model);
237
+ appendMetaRow("Task", data.experiment?.task);
238
+ appendMetaRow("Methods", (data.experiment?.methods ?? []).join(", "));
239
+ appendMetaRow("Datasets", (data.experiment?.datasets ?? []).join(", "));
240
+ appendMetaRow("Device", data.experiment?.device);
241
+ if (run?.num_qubits !== undefined) appendMetaRow("Qubits", run.num_qubits);
242
+ if (run?.num_layers_explicit !== undefined || run?.num_layers_reuploading !== undefined) {
243
+ appendMetaRow(
244
+ "Layers (explicit / reupload)",
245
+ `${formatInteger(run?.num_layers_explicit)} / ${formatInteger(run?.num_layers_reuploading)}`,
246
+ );
247
+ } else if (run?.num_layers !== undefined) {
248
+ appendMetaRow("Layers", run.num_layers);
249
+ }
250
+ if (run?.num_params !== undefined) appendMetaRow("Params", formatInteger(run.num_params));
251
+ if (run?.train_time !== undefined) appendMetaRow("Train time", run.train_time);
252
+ appendMetaRow("Note", data.experiment?.note);
253
+ }
254
+
255
+ async function applyRun(runId) {
256
+ state.currentRunId = runId;
257
+ state.currentRunChunkCache = {};
258
+ state.currentRunChunkInflight = {};
259
+ const loadToken = ++state.activeLoadToken;
260
+
261
+ const selectedRun =
262
+ state.currentManifest.runs.find((r) => r.id === runId) ||
263
+ state.currentManifest.runs[0];
264
+
265
+ setLoadingState({
266
+ visible: true,
267
+ label: `Preparing ${selectedRun.label}`,
268
+ percent: 20,
269
+ status: "loading",
270
+ });
271
+
272
+ state.currentData = await loadRunData(selectedRun.path, loadToken);
273
+ if (loadToken !== state.activeLoadToken) return;
274
+
275
+ setLoadingState({ visible: true, label: `Rendering ${selectedRun.label}`, percent: 82, status: "rendering" });
276
+
277
+ pageTitle.textContent = state.currentData.title;
278
+ pageSubtitle.textContent = state.currentData.subtitle;
279
+ exportStatus.textContent = state.currentData.status;
280
+ runNote.textContent = `Loaded ${selectedRun.label} with ${selectedRun.steps} exported steps.`;
281
+
282
+ renderResultsTable(state.currentManifest.runs, selectedRun.id);
283
+
284
+ if (datasetsImage) {
285
+ const src = state.currentData.assets?.datasets_overview;
286
+ if (src) {
287
+ datasetsImage.src = withCacheBust(src);
288
+ } else {
289
+ datasetsImage.removeAttribute("src");
290
+ }
291
+ }
292
+
293
+ populateExperimentMeta(state.currentData, selectedRun);
294
+
295
+ const steps = state.currentData.timeline_steps || [];
296
+ if (steps.length > 1) {
297
+ stepSlider.disabled = false;
298
+ stepSlider.min = "0";
299
+ stepSlider.max = String(steps.length - 1);
300
+ stepSlider.value = "0";
301
+ } else {
302
+ stepSlider.disabled = true;
303
+ stepSlider.min = stepSlider.max = stepSlider.value = "0";
304
+ }
305
+
306
+ await refreshStepState(state.currentData, 0, ++state.activeStepToken);
307
+ prefetchChunkStream(state.currentData, loadToken, ++state.activePrefetchToken);
308
+ if (!getChunkPaths(state.currentData).length) {
309
+ setLoadingState({ visible: false, label: "Viewer ready", percent: 100, status: "ready" });
310
+ }
311
+ }
312
+
313
+ // ── camera sync ───────────────────────────────────────────────────────────────
314
+
315
+ /**
316
+ * Bind plotly_relayout camera-sync for one dataset row.
317
+ * Safe to call multiple times — skips panels already bound.
318
+ * @param {number} di Dataset index (0=circle, 1=moons)
319
+ */
320
+ function bindCameraSync(di) {
321
+ const syncTimers = {};
322
+ METHODS.forEach((_, mi) => {
323
+ const container = boundaryPlots[mi]?.[di];
324
+ if (!container || container._cameraBound) return;
325
+ container._cameraBound = true;
326
+ container.on("plotly_relayout", (eventData) => {
327
+ if (container._receivingSync) return;
328
+ const cam = eventData["scene.camera"];
329
+ if (!cam) return;
330
+ state.cameraState[di] = cam;
331
+ clearTimeout(syncTimers[mi]);
332
+ syncTimers[mi] = setTimeout(() => {
333
+ METHODS.forEach((_, otherMi) => {
334
+ if (otherMi === mi) return;
335
+ const other = boundaryPlots[otherMi]?.[di];
336
+ if (!other?._hasPlot) return;
337
+ other._receivingSync = true;
338
+ Plotly.relayout(other, { "scene.camera": cam });
339
+ setTimeout(() => { other._receivingSync = false; }, 300);
340
+ });
341
+ }, 150);
342
+ });
343
+ });
344
+ }
345
+
346
+ // ── main ───────────────────────────────────────────────────────────────────────
347
+
348
+ async function main() {
349
+ bindImageLightbox();
350
+ bindAnalysisModal();
351
+ maybeShowAnalysisHint();
352
+
353
+ // Seed per-dataset home cameras before first render
354
+ state.cameraState[1] = MOONS_DEFAULT_CAMERA;
355
+
356
+ setLoadingState({ visible: true, label: "Booting viewer", percent: 2, status: "loading" });
357
+ await loadRuntimeSource();
358
+ state.currentManifest = await loadManifest();
359
+
360
+ const runs = state.currentManifest.runs || [];
361
+ runSelect.innerHTML = "";
362
+ runs.forEach((run) => {
363
+ const opt = document.createElement("option");
364
+ opt.value = run.id;
365
+ opt.textContent = run.label;
366
+ runSelect.appendChild(opt);
367
+ });
368
+
369
+ if (!runs.length) throw new Error("Viewer manifest contains no runs.");
370
+
371
+ const defaultRunId = state.currentManifest.default_run || runs[0].id;
372
+ runSelect.value = defaultRunId;
373
+ renderResultsTable(runs, defaultRunId);
374
+ await applyRun(defaultRunId);
375
+
376
+ runSelect.addEventListener("change", async (e) => applyRun(e.target.value));
377
+
378
+ // Camera preset buttons
379
+ const applyPresetCamera = (di, camera) => {
380
+ state.cameraState[di] = camera;
381
+ METHODS.forEach((_, mi) => {
382
+ const container = boundaryPlots[mi]?.[di];
383
+ if (!container?._hasPlot) return;
384
+ container._receivingSync = true;
385
+ Plotly.relayout(container, { "scene.camera": camera, "scene.dragmode": "turntable" });
386
+ setTimeout(() => { container._receivingSync = false; }, 300);
387
+ });
388
+ };
389
+
390
+ const homecameras = [DEFAULT_CAMERA, MOONS_DEFAULT_CAMERA];
391
+ homeButtons.forEach((btn, di) => {
392
+ if (!btn) return;
393
+ btn.addEventListener("click", () => applyPresetCamera(di, homecameras[di]));
394
+ });
395
+
396
+ const topcameras = [TOP_CAMERA, MOONS_TOP_CAMERA];
397
+ topButtons.forEach((btn, di) => {
398
+ if (!btn) return;
399
+ btn.addEventListener("click", () => applyPresetCamera(di, topcameras[di]));
400
+ });
401
+
402
+ stepSlider.addEventListener("input", async (e) => {
403
+ if (state.currentData) {
404
+ await refreshStepState(
405
+ state.currentData,
406
+ Number(e.target.value),
407
+ ++state.activeStepToken,
408
+ );
409
+ }
410
+ });
411
+ }
412
+
413
+ main().catch((error) => {
414
+ console.error(error);
415
+ pageSubtitle.textContent = "Failed to load static export.";
416
+ runNote.textContent = "Unable to load a viewer manifest.";
417
+ if (chartEmpty) {
418
+ chartEmpty.textContent = "The static viewer failed to load its export data.";
419
+ chartEmpty.hidden = false;
420
+ chartEmpty.style.display = "flex";
421
+ }
422
+ setLoadingState({ visible: true, label: "Load failed", percent: 100, status: "error" });
423
+ });
js/charts.js ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const METHOD_COLORS = {
2
+ explicit: { pos: "#4a90d9", neg: "#e8c06a" },
3
+ kernel: { pos: "#e87c4a", neg: "#6ac4e8" },
4
+ reuploading: { pos: "#5ab45a", neg: "#d46ab4" },
5
+ };
6
+
7
+ const PLOTLY_BASE = {
8
+ responsive: true,
9
+ displayModeBar: false,
10
+ };
11
+
12
+ // Slightly-tilted isometric view, zoomed in
13
+ export const DEFAULT_CAMERA = {
14
+ eye: { x: 0.8, y: -0.8, z: 0.65 },
15
+ up: { x: 0, y: 0, z: 1 },
16
+ center: { x: 0, y: 0, z: 0 },
17
+ };
18
+
19
+ // Moons home — same tilt but rotated 90° clockwise in XY
20
+ export const MOONS_DEFAULT_CAMERA = {
21
+ eye: { x: -0.8, y: -0.8, z: 0.65 },
22
+ up: { x: 0, y: 0, z: 1 },
23
+ center: { x: 0, y: 0, z: 0 },
24
+ };
25
+
26
+ // Top-down view for Circle — up matches Circle home direction (0.8, -0.8) → up = (-√2/2, √2/2)
27
+ export const TOP_CAMERA = {
28
+ eye: { x: 0, y: 0, z: 1.4 },
29
+ up: { x: -0.707, y: 0.707, z: 0 },
30
+ center: { x: 0, y: 0, z: 0 },
31
+ };
32
+
33
+ // Top-down view for Moons — up rotated 45° CW to match Moons home direction
34
+ export const MOONS_TOP_CAMERA = {
35
+ eye: { x: 0, y: 0, z: 1.4 },
36
+ up: { x: 0.707, y: 0.707, z: 0 },
37
+ center: { x: 0, y: 0, z: 0 },
38
+ };
39
+
40
+ /**
41
+ * Render a decision-boundary panel as an interactive 3D surface.
42
+ *
43
+ * @param {HTMLElement} container
44
+ * @param {object|null} heatmap { x, y, z } — z is P(class 1) in [0, 1]
45
+ * @param {object[]|null} points [{ x, y, label }] scatter points
46
+ * @param {string} method "explicit" | "kernel" | "reuploading"
47
+ * @param {object|null} camera Plotly scene.camera object; null → DEFAULT_CAMERA
48
+ */
49
+ export function renderBoundarySurface(container, heatmap, points, method, camera) {
50
+ if (!container) return;
51
+
52
+ const colors = METHOD_COLORS[method] ?? METHOD_COLORS.reuploading;
53
+ const appliedCamera = camera ?? DEFAULT_CAMERA;
54
+ const traces = [];
55
+
56
+ if (heatmap?.z?.length) {
57
+ traces.push({
58
+ type: "surface",
59
+ x: heatmap.x,
60
+ y: heatmap.y,
61
+ z: heatmap.z,
62
+ colorscale: [
63
+ [0, colors.neg],
64
+ [0.45, "#f8f4ee"],
65
+ [0.55, "#f8f4ee"],
66
+ [1, colors.pos],
67
+ ],
68
+ cmin: 0, cmax: 1,
69
+ showscale: false,
70
+ opacity: 0.72,
71
+ contours: {
72
+ z: {
73
+ show: true,
74
+ start: 0.5, end: 0.5, size: 0.1,
75
+ color: "rgba(20,20,20,0.85)",
76
+ width: 3,
77
+ usecolormap: false,
78
+ },
79
+ },
80
+ hovertemplate: "x: %{x:.2f}<br>y: %{y:.2f}<br>P(1): %{z:.2f}<extra></extra>",
81
+ });
82
+ } else {
83
+ traces.push({
84
+ type: "scatter3d", mode: "text",
85
+ x: [0], y: [0], z: [0.5],
86
+ text: ["No data"],
87
+ textfont: { color: "#aaa", size: 11 },
88
+ showlegend: false,
89
+ });
90
+ }
91
+
92
+ // Scatter points floating just above the surface
93
+ if (points?.length) {
94
+ const cls0x = [], cls0y = [], cls1x = [], cls1y = [];
95
+ for (const p of points) {
96
+ if (p.label === 0) { cls0x.push(p.x); cls0y.push(p.y); }
97
+ else { cls1x.push(p.x); cls1y.push(p.y); }
98
+ }
99
+ const Z = 0.5;
100
+ if (cls0x.length) traces.push({
101
+ type: "scatter3d", mode: "markers",
102
+ x: cls0x, y: cls0y, z: cls0x.map(() => Z),
103
+ marker: { color: "#1565c0", size: 2.5 },
104
+ showlegend: false, hoverinfo: "skip",
105
+ });
106
+ if (cls1x.length) traces.push({
107
+ type: "scatter3d", mode: "markers",
108
+ x: cls1x, y: cls1y, z: cls1x.map(() => Z),
109
+ marker: { color: "#c62828", size: 2.5, symbol: "square" },
110
+ showlegend: false, hoverinfo: "skip",
111
+ });
112
+ }
113
+
114
+ const layout = {
115
+ margin: { t: 0, b: 0, l: 0, r: 0 },
116
+ scene: {
117
+ xaxis: { visible: false, showgrid: false },
118
+ yaxis: { visible: false, showgrid: false },
119
+ zaxis: {
120
+ title: "", range: [0, 1.1],
121
+ tickvals: [0, 0.5, 1], ticktext: ["0", "·5", "1"],
122
+ tickfont: { size: 8 },
123
+ gridcolor: "rgba(23,33,29,0.08)",
124
+ },
125
+ camera: appliedCamera,
126
+ dragmode: "turntable",
127
+ aspectmode: "manual",
128
+ aspectratio: { x: 1, y: 1, z: 0.55 },
129
+ bgcolor: "rgba(0,0,0,0)",
130
+ },
131
+ paper_bgcolor: "rgba(0,0,0,0)",
132
+ plot_bgcolor: "rgba(0,0,0,0)",
133
+ };
134
+
135
+ if (container._hasPlot) {
136
+ Plotly.react(container, traces, layout, PLOTLY_BASE);
137
+ } else {
138
+ Plotly.newPlot(container, traces, layout, PLOTLY_BASE);
139
+ container._hasPlot = true;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Render the accuracy / loss curves.
145
+ *
146
+ * @param {object[]} steps Array of step objects from timeline_steps
147
+ * @param {number} current Index of current step (highlighted)
148
+ */
149
+ export function renderAccuracyChart(steps, current) {
150
+ const lossChart = document.getElementById("loss-chart");
151
+ if (!lossChart || !steps?.length) return;
152
+
153
+ const epochs = steps.map((s) => s.global_step ?? s.epoch ?? 0);
154
+ const trainAcc = steps.map((s) => s.train_acc ?? null);
155
+ const testAcc = steps.map((s) => s.test_acc ?? null);
156
+ const trainLoss = steps.map((s) => s.train_loss ?? null);
157
+ const testLoss = steps.map((s) => s.test_loss ?? null);
158
+
159
+ const curEpoch = steps[current]?.global_step ?? steps[current]?.epoch ?? null;
160
+
161
+ const shapes = curEpoch !== null ? [{
162
+ type: "line", xref: "x", yref: "paper",
163
+ x0: curEpoch, x1: curEpoch, y0: 0, y1: 1,
164
+ line: { color: "rgba(13,143,113,0.5)", width: 1.5, dash: "dot" },
165
+ }] : [];
166
+
167
+ const traces = [
168
+ { x: epochs, y: trainAcc, name: "Train acc", mode: "lines", line: { color: "#0d8f71", width: 2 } },
169
+ { x: epochs, y: testAcc, name: "Test acc", mode: "lines", line: { color: "#ef8354", width: 2 } },
170
+ { x: epochs, y: trainLoss, name: "Train loss", mode: "lines", line: { color: "#0d8f71", width: 1.5, dash: "dot" } },
171
+ { x: epochs, y: testLoss, name: "Test loss", mode: "lines", line: { color: "#ef8354", width: 1.5, dash: "dot" } },
172
+ ].filter((t) => t.y.some((v) => v !== null));
173
+
174
+ const layout = {
175
+ margin: { t: 8, b: 36, l: 42, r: 16 },
176
+ paper_bgcolor: "rgba(0,0,0,0)",
177
+ plot_bgcolor: "rgba(0,0,0,0)",
178
+ xaxis: { title: "Step", gridcolor: "rgba(23,33,29,0.08)", zeroline: false },
179
+ yaxis: { gridcolor: "rgba(23,33,29,0.08)", zeroline: false },
180
+ legend: { orientation: "h", y: -0.22, font: { size: 11 } },
181
+ shapes,
182
+ font: { family: "IBM Plex Sans, sans-serif", size: 11 },
183
+ };
184
+
185
+ if (lossChart._hasPlot) {
186
+ Plotly.react(lossChart, traces, layout, PLOTLY_BASE);
187
+ } else {
188
+ Plotly.newPlot(lossChart, traces, layout, PLOTLY_BASE);
189
+ lossChart._hasPlot = true;
190
+ }
191
+ }
192
+
193
+ export function renderEmptyState() {
194
+ const chartEmpty = document.getElementById("chart-empty");
195
+ if (chartEmpty) {
196
+ chartEmpty.hidden = false;
197
+ chartEmpty.style.display = "flex";
198
+ }
199
+ const lossChart = document.getElementById("loss-chart");
200
+ if (lossChart) lossChart.style.display = "none";
201
+ }
js/data.js ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ experimentMeta, loadingPanel, loadingLabel, loadingPercent, loadingBar,
3
+ exportStatus, runtimeSourceUrls, state,
4
+ } from "./dom.js";
5
+
6
+ function ensureTrailingSlash(value) {
7
+ if (!value) {
8
+ return "./";
9
+ }
10
+ return value.endsWith("/") ? value : `${value}/`;
11
+ }
12
+
13
+ function buildDatasetRuntimeRoot(repoId, revision = "main", pathPrefix = "") {
14
+ const safePrefix = pathPrefix.trim().replace(/^\/+|\/+$/g, "");
15
+ const suffix = safePrefix ? `${safePrefix}/` : "";
16
+ return `https://huggingface.co/datasets/${repoId}/resolve/${revision}/${suffix}`;
17
+ }
18
+
19
+ function isAbsoluteUrl(path) {
20
+ return /^(?:[a-z]+:)?\/\//i.test(path);
21
+ }
22
+
23
+ function resolveAgainstBase(path, baseUrl) {
24
+ if (!path) {
25
+ return path;
26
+ }
27
+ if (isAbsoluteUrl(path)) {
28
+ return path;
29
+ }
30
+ const normalizedBase = isAbsoluteUrl(baseUrl)
31
+ ? baseUrl
32
+ : new URL(baseUrl, window.location.href).toString();
33
+ return new URL(path, normalizedBase).toString();
34
+ }
35
+
36
+ function shouldResolveFromRuntimeRoot(path) {
37
+ if (!path || isAbsoluteUrl(path)) {
38
+ return false;
39
+ }
40
+ return path.startsWith("runtime/") || path.startsWith("./runtime/");
41
+ }
42
+
43
+ function buildRuntimeRoot(runtimeSource) {
44
+ if (runtimeSource?.runtime_root_url) {
45
+ return ensureTrailingSlash(runtimeSource.runtime_root_url);
46
+ }
47
+ if (runtimeSource?.hf_dataset_repo) {
48
+ return buildDatasetRuntimeRoot(
49
+ runtimeSource.hf_dataset_repo,
50
+ runtimeSource.hf_dataset_revision || "main",
51
+ runtimeSource.hf_dataset_path_prefix || ""
52
+ );
53
+ }
54
+ return ensureTrailingSlash("./");
55
+ }
56
+
57
+ function normalizeRuntimeSource(runtimeSource) {
58
+ const normalized = {
59
+ manifest_path: "./runtime/viewer_manifest.json",
60
+ fallback_manifest_urls: ["./data/viewer_manifest.template.json"],
61
+ ...runtimeSource,
62
+ };
63
+ normalized.runtime_root_url = buildRuntimeRoot(normalized);
64
+ return normalized;
65
+ }
66
+
67
+ function buildManifestUrls(runtimeSource) {
68
+ const urls = [];
69
+ if (runtimeSource?.manifest_path) {
70
+ urls.push(resolveAgainstBase(runtimeSource.manifest_path, runtimeSource.runtime_root_url));
71
+ }
72
+ for (const fallback of runtimeSource?.fallback_manifest_urls || []) {
73
+ urls.push(resolveAgainstBase(fallback, "./"));
74
+ }
75
+ return urls;
76
+ }
77
+
78
+ function resolveRuntimePath(path) {
79
+ if (!path) {
80
+ return path;
81
+ }
82
+ if (shouldResolveFromRuntimeRoot(path)) {
83
+ return resolveAgainstBase(path, state.runtimeSource?.runtime_root_url || "./");
84
+ }
85
+ return resolveAgainstBase(path, "./");
86
+ }
87
+
88
+ function normalizeManifest(manifest) {
89
+ return {
90
+ ...manifest,
91
+ runs: (manifest.runs || []).map((run) => ({
92
+ ...run,
93
+ path: resolveRuntimePath(run.path),
94
+ })),
95
+ };
96
+ }
97
+
98
+ function normalizeRunPayload(payload) {
99
+ const normalized = { ...payload };
100
+ if (normalized.assets) {
101
+ normalized.assets = {
102
+ ...normalized.assets,
103
+ circuit: resolveRuntimePath(normalized.assets.circuit),
104
+ data_overview: resolveRuntimePath(normalized.assets.data_overview),
105
+ };
106
+ }
107
+ if (normalized.run?.path) {
108
+ normalized.run = {
109
+ ...normalized.run,
110
+ path: resolveRuntimePath(normalized.run.path),
111
+ };
112
+ }
113
+ if (normalized.timeline_chunks) {
114
+ normalized.timeline_chunks = normalized.timeline_chunks.map((chunk) => ({
115
+ ...chunk,
116
+ path: resolveRuntimePath(chunk.path),
117
+ }));
118
+ }
119
+ if (normalized.timeline_steps) {
120
+ normalized.timeline_steps = normalized.timeline_steps.map((step) => {
121
+ if (!step.chunk_path) {
122
+ return step;
123
+ }
124
+ return {
125
+ ...step,
126
+ chunk_path: resolveRuntimePath(step.chunk_path),
127
+ };
128
+ });
129
+ }
130
+ return normalized;
131
+ }
132
+
133
+ function describePath(path) {
134
+ if (!path) {
135
+ return "runtime data";
136
+ }
137
+ try {
138
+ const url = new URL(path, window.location.href);
139
+ const parts = url.pathname.split("/").filter(Boolean);
140
+ return parts.slice(-2).join("/");
141
+ } catch {
142
+ return path.replace("./", "");
143
+ }
144
+ }
145
+
146
+ export function withCacheBust(path) {
147
+ const separator = path.includes("?") ? "&" : "?";
148
+ return `${path}${separator}t=${Date.now()}`;
149
+ }
150
+
151
+ export function formatMetric(value) {
152
+ if (value === undefined || value === null || Number.isNaN(Number(value))) {
153
+ return "—";
154
+ }
155
+ const numeric = Number(value);
156
+ const absValue = Math.abs(numeric);
157
+ if (absValue > 0 && absValue < 1e-3) {
158
+ return numeric.toExponential(2);
159
+ }
160
+ return numeric.toFixed(4);
161
+ }
162
+
163
+ export function formatInteger(value) {
164
+ if (value === undefined || value === null || Number.isNaN(Number(value))) {
165
+ return "—";
166
+ }
167
+ return String(Math.round(Number(value)));
168
+ }
169
+
170
+ export function appendMetaRow(label, value) {
171
+ const wrapper = document.createElement("div");
172
+ const dt = document.createElement("dt");
173
+ const dd = document.createElement("dd");
174
+ dt.textContent = label;
175
+ dd.textContent = value;
176
+ wrapper.append(dt, dd);
177
+ experimentMeta.appendChild(wrapper);
178
+ }
179
+
180
+ export function setLoadingState({ visible, label, percent, status }) {
181
+ if (loadingPanel) {
182
+ loadingPanel.hidden = !visible;
183
+ }
184
+ if (loadingLabel && label) {
185
+ loadingLabel.textContent = label;
186
+ }
187
+ if (typeof percent === "number") {
188
+ const clamped = Math.max(0, Math.min(100, Math.round(percent)));
189
+ if (loadingPercent) {
190
+ loadingPercent.textContent = `${clamped}%`;
191
+ }
192
+ if (loadingBar) {
193
+ loadingBar.style.width = `${clamped}%`;
194
+ }
195
+ }
196
+ if (status) {
197
+ exportStatus.textContent = status;
198
+ }
199
+ }
200
+
201
+ export async function loadRuntimeSource() {
202
+ for (const url of runtimeSourceUrls) {
203
+ const response = await fetch(withCacheBust(url), { cache: "no-store" });
204
+ if (response.ok) {
205
+ const runtimeSource = normalizeRuntimeSource(await response.json());
206
+ state.runtimeSource = runtimeSource;
207
+ return runtimeSource;
208
+ }
209
+ }
210
+ const fallback = normalizeRuntimeSource({});
211
+ state.runtimeSource = fallback;
212
+ return fallback;
213
+ }
214
+
215
+ export async function loadManifest() {
216
+ const runtimeSource = state.runtimeSource || (await loadRuntimeSource());
217
+ for (const url of buildManifestUrls(runtimeSource)) {
218
+ setLoadingState({
219
+ visible: true,
220
+ label: `Loading manifest from ${describePath(url)}`,
221
+ percent: 8,
222
+ status: "loading",
223
+ });
224
+ const response = await fetch(withCacheBust(url), { cache: "no-store" });
225
+ if (response.ok) {
226
+ const data = normalizeManifest(await response.json());
227
+ setLoadingState({
228
+ visible: true,
229
+ label: "Manifest ready",
230
+ percent: 18,
231
+ status: "loading",
232
+ });
233
+ return data;
234
+ }
235
+ }
236
+ throw new Error("No viewer manifest available.");
237
+ }
238
+
239
+ export function resolveRuntimeAssetPath(path) {
240
+ return resolveRuntimePath(path);
241
+ }
242
+
243
+ export async function loadRunData(path, loadToken) {
244
+ const response = await fetch(withCacheBust(path), { cache: "no-store" });
245
+ if (!response.ok) {
246
+ throw new Error(`Failed to load run data: ${path}`);
247
+ }
248
+ const contentLength = Number(response.headers.get("content-length") || 0);
249
+
250
+ if (!response.body || !contentLength) {
251
+ setLoadingState({
252
+ visible: true,
253
+ label: `Loading ${describePath(path)}`,
254
+ percent: 45,
255
+ status: "loading",
256
+ });
257
+ return normalizeRunPayload(await response.json());
258
+ }
259
+
260
+ const reader = response.body.getReader();
261
+ const decoder = new TextDecoder();
262
+ let received = 0;
263
+ let text = "";
264
+
265
+ while (true) {
266
+ const { done, value } = await reader.read();
267
+ if (done) {
268
+ break;
269
+ }
270
+ if (loadToken !== state.activeLoadToken) {
271
+ throw new Error("Stale run load aborted.");
272
+ }
273
+ received += value.byteLength;
274
+ text += decoder.decode(value, { stream: true });
275
+ const progress = 20 + (received / contentLength) * 55;
276
+ setLoadingState({
277
+ visible: true,
278
+ label: `Downloading ${describePath(path)}`,
279
+ percent: progress,
280
+ status: "loading",
281
+ });
282
+ }
283
+
284
+ text += decoder.decode();
285
+ return normalizeRunPayload(JSON.parse(text));
286
+ }
287
+
288
+ export async function loadRunChunk(path, loadToken) {
289
+ const response = await fetch(withCacheBust(path), { cache: "no-store" });
290
+ if (!response.ok) {
291
+ throw new Error(`Failed to load run chunk: ${path}`);
292
+ }
293
+ if (loadToken !== state.activeLoadToken) {
294
+ throw new Error("Stale chunk load aborted.");
295
+ }
296
+ return normalizeRunPayload(await response.json());
297
+ }
js/dom.js ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const runtimeSourceUrls = [
2
+ "./data/runtime_source.json",
3
+ "./data/runtime_source.template.json",
4
+ ];
5
+
6
+ export const loadingPanel = document.getElementById("loading-panel");
7
+ export const loadingLabel = document.getElementById("loading-label");
8
+ export const loadingPercent = document.getElementById("loading-percent");
9
+ export const loadingBar = document.getElementById("loading-bar");
10
+
11
+ export const pageTitle = document.getElementById("page-title");
12
+ export const pageSubtitle = document.getElementById("page-subtitle");
13
+ export const exportStatus = document.getElementById("export-status");
14
+
15
+ export const runSelect = document.getElementById("run-select");
16
+ export const runNote = document.getElementById("run-note");
17
+ export const resultsTableBody = document.getElementById("results-table-body");
18
+
19
+ export const stepSlider = document.getElementById("step-slider");
20
+ export const currentStepLabel = document.getElementById("current-step-label");
21
+ export const timelineCaption = document.getElementById("timeline-caption");
22
+ export const trainAccPill = document.getElementById("train-acc-pill");
23
+ export const testAccPill = document.getElementById("test-acc-pill");
24
+ export const chartEmpty = document.getElementById("chart-empty");
25
+
26
+ export const experimentMeta = document.getElementById("experiment-meta");
27
+ export const datasetsImage = document.getElementById("datasets-image");
28
+
29
+ // Decision-boundary plot containers: [method][dataset]
30
+ // method: 0=explicit, 1=kernel, 2=reuploading
31
+ // dataset: 0=circle, 1=moons
32
+ export const boundaryPlots = [
33
+ [document.getElementById("bp-explicit-circle"), document.getElementById("bp-explicit-moons")],
34
+ [document.getElementById("bp-kernel-circle"), document.getElementById("bp-kernel-moons")],
35
+ [document.getElementById("bp-reupload-circle"), document.getElementById("bp-reupload-moons")],
36
+ ];
37
+
38
+ export const accPills = [
39
+ [document.getElementById("acc-explicit-circle"), document.getElementById("acc-explicit-moons")],
40
+ [document.getElementById("acc-kernel-circle"), document.getElementById("acc-kernel-moons")],
41
+ [document.getElementById("acc-reupload-circle"), document.getElementById("acc-reupload-moons")],
42
+ ];
43
+
44
+ export const lossChart = document.getElementById("loss-chart");
45
+
46
+ // Home-camera buttons: index matches DATASETS order (0=circle, 1=moons)
47
+ export const homeButtons = [
48
+ document.getElementById("home-btn-circle"),
49
+ document.getElementById("home-btn-moons"),
50
+ ];
51
+
52
+ // Top-down camera buttons
53
+ export const topButtons = [
54
+ document.getElementById("top-btn-circle"),
55
+ document.getElementById("top-btn-moons"),
56
+ ];
57
+
58
+ // Image lightbox
59
+ export const imageLightbox = document.getElementById("image-lightbox");
60
+ export const imageLightboxImage = document.getElementById("image-lightbox-image");
61
+ export const imageLightboxCaption = document.getElementById("image-lightbox-caption");
62
+ export const imageLightboxClose = document.getElementById("image-lightbox-close");
63
+
64
+ // Analysis / answers modal
65
+ export const analysisOpenButton = document.getElementById("analysis-open");
66
+ export const answersOpenButton = document.getElementById("answers-open");
67
+ export const analysisModal = document.getElementById("analysis-modal");
68
+ export const analysisCloseButton = document.getElementById("analysis-close");
69
+ export const analysisHint = document.getElementById("analysis-hint");
70
+ export const analysisHintClose = document.getElementById("analysis-hint-close");
71
+ export const analysisModalLabel = document.getElementById("analysis-modal-label");
72
+ export const analysisMarkdown = document.getElementById("analysis-markdown");
73
+ export const previewableImages = Array.from(document.querySelectorAll(".previewable-image"));
74
+
75
+ export const state = {
76
+ currentRunId: null,
77
+ currentManifest: null,
78
+ currentData: null,
79
+ activeLoadToken: 0,
80
+ activeStepToken: 0,
81
+ activePrefetchToken: 0,
82
+ currentRunChunkCache: {},
83
+ currentRunChunkInflight: {},
84
+ activeDataset: "circle", // "circle" | "moons"
85
+ markdownCache: {},
86
+ // Camera state per dataset row (index 0=circle, 1=moons); null = use DEFAULT_CAMERA
87
+ cameraState: [null, null],
88
+ };
js/overlays.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ imageLightbox, imageLightboxImage, imageLightboxCaption, imageLightboxClose,
3
+ analysisOpenButton, answersOpenButton, analysisModal, analysisCloseButton,
4
+ analysisHint, analysisHintClose, analysisModalLabel, analysisMarkdown,
5
+ previewableImages, state,
6
+ } from "./dom.js";
7
+
8
+ function openImageLightbox(sourceImage) {
9
+ if (!imageLightbox || !imageLightboxImage || !sourceImage?.src) {
10
+ return;
11
+ }
12
+
13
+ imageLightboxImage.src = sourceImage.src;
14
+ imageLightboxImage.alt = sourceImage.alt || "Preview";
15
+ if (imageLightboxCaption) {
16
+ imageLightboxCaption.textContent =
17
+ sourceImage.dataset.previewCaption || sourceImage.alt || "";
18
+ }
19
+ imageLightbox.hidden = false;
20
+ imageLightbox.setAttribute("aria-hidden", "false");
21
+ }
22
+
23
+ function closeImageLightbox() {
24
+ if (!imageLightbox || !imageLightboxImage) {
25
+ return;
26
+ }
27
+
28
+ imageLightbox.hidden = true;
29
+ imageLightbox.setAttribute("aria-hidden", "true");
30
+ imageLightboxImage.removeAttribute("src");
31
+ }
32
+
33
+ export function bindPreviewableImages(images) {
34
+ images.forEach((image) => {
35
+ image.addEventListener("click", () => openImageLightbox(image));
36
+ });
37
+ }
38
+
39
+ export function bindImageLightbox() {
40
+ bindPreviewableImages(previewableImages);
41
+
42
+ imageLightbox?.addEventListener("click", (event) => {
43
+ const closeRequested =
44
+ event.target === imageLightbox || event.target?.dataset?.lightboxClose === "true";
45
+ if (closeRequested) {
46
+ closeImageLightbox();
47
+ }
48
+ });
49
+
50
+ imageLightboxClose?.addEventListener("click", closeImageLightbox);
51
+
52
+ document.addEventListener("keydown", (event) => {
53
+ if (event.key === "Escape" && imageLightbox && !imageLightbox.hidden) {
54
+ closeImageLightbox();
55
+ }
56
+ });
57
+ }
58
+
59
+ function dismissAnalysisHint() {
60
+ if (analysisHint) {
61
+ analysisHint.hidden = true;
62
+ }
63
+ }
64
+
65
+ export function maybeShowAnalysisHint() {
66
+ if (!analysisHint) {
67
+ return;
68
+ }
69
+ analysisHint.hidden = false;
70
+ }
71
+
72
+ function escapeHtml(markup) {
73
+ return markup.replace(/[&<>]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[char]));
74
+ }
75
+
76
+ function renderMarkdown(markdown) {
77
+ return globalThis.marked?.parse?.(markdown) ?? `<pre>${escapeHtml(markdown)}</pre>`;
78
+ }
79
+
80
+ function enhanceMarkdownImages() {
81
+ const analysisImages = Array.from(analysisMarkdown?.querySelectorAll("img") || []);
82
+ analysisImages.forEach((image) => {
83
+ image.classList.add("previewable-image");
84
+ if (!image.dataset.previewCaption && image.alt) {
85
+ image.dataset.previewCaption = image.alt;
86
+ }
87
+ });
88
+ bindPreviewableImages(analysisImages);
89
+ }
90
+
91
+ async function ensureMarkdownDocument(sourcePath) {
92
+ if (!analysisMarkdown) {
93
+ return;
94
+ }
95
+ if (state.markdownCache[sourcePath]) {
96
+ analysisMarkdown.innerHTML = state.markdownCache[sourcePath];
97
+ enhanceMarkdownImages();
98
+ return;
99
+ }
100
+
101
+ try {
102
+ const response = await fetch(sourcePath, { cache: "no-store" });
103
+ if (!response.ok) {
104
+ throw new Error(`Failed to load markdown document: ${sourcePath}`);
105
+ }
106
+ const markdown = await response.text();
107
+ const rendered = renderMarkdown(markdown);
108
+ analysisMarkdown.innerHTML = rendered;
109
+ enhanceMarkdownImages();
110
+ state.markdownCache[sourcePath] = rendered;
111
+ } catch (error) {
112
+ console.error(error);
113
+ analysisMarkdown.textContent = "Failed to load notes.";
114
+ }
115
+ }
116
+
117
+ function openAnalysisModal({ sourcePath, label }) {
118
+ if (!analysisModal) {
119
+ return;
120
+ }
121
+ dismissAnalysisHint();
122
+ analysisModal.hidden = false;
123
+ analysisModal.setAttribute("aria-hidden", "false");
124
+ if (analysisModalLabel) {
125
+ analysisModalLabel.textContent = label;
126
+ }
127
+ analysisMarkdown.textContent = "Loading notes...";
128
+ void ensureMarkdownDocument(sourcePath);
129
+ }
130
+
131
+ function closeAnalysisModal() {
132
+ if (!analysisModal) {
133
+ return;
134
+ }
135
+ analysisModal.hidden = true;
136
+ analysisModal.setAttribute("aria-hidden", "true");
137
+ }
138
+
139
+ export function bindAnalysisModal() {
140
+ analysisOpenButton?.addEventListener("click", () => openAnalysisModal({
141
+ sourcePath: "./ANALYSIS.md",
142
+ label: "Analysis notes",
143
+ }));
144
+ answersOpenButton?.addEventListener("click", () => openAnalysisModal({
145
+ sourcePath: "./ANSWERS.md",
146
+ label: "Problem 2 answers",
147
+ }));
148
+ analysisCloseButton?.addEventListener("click", closeAnalysisModal);
149
+ analysisHintClose?.addEventListener("click", dismissAnalysisHint);
150
+
151
+ analysisModal?.addEventListener("click", (event) => {
152
+ const closeRequested =
153
+ event.target === analysisModal || event.target?.dataset?.analysisClose === "true";
154
+ if (closeRequested) {
155
+ closeAnalysisModal();
156
+ }
157
+ });
158
+
159
+ document.addEventListener("keydown", (event) => {
160
+ if (event.key === "Escape" && analysisModal && !analysisModal.hidden) {
161
+ closeAnalysisModal();
162
+ }
163
+ });
164
+ }
runtime/chunks/q2-le2-lr2-e50_epoch_0000.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0001.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0002.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0003.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0004.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0005.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0006.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0007.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0008.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0009.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0010.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0011.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0012.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0013.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0014.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0015.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0016.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0017.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0018.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0019.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0020.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0021.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0022.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0023.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0024.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0025.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0026.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0027.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0028.json ADDED
The diff for this file is too large to render. See raw diff
 
runtime/chunks/q2-le2-lr2-e50_epoch_0029.json ADDED
The diff for this file is too large to render. See raw diff