Update QML Classifier Explorer
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- ANALYSIS.md +204 -0
- ANSWERS.md +20 -0
- README.md +40 -4
- assets/preview_boundaries.png +3 -0
- assets/preview_datasets.png +0 -0
- assets/preview_ui_sketch.png +3 -0
- css/base.css +146 -0
- css/charts.css +238 -0
- css/components.css +490 -0
- css/overlays.css +194 -0
- data/runtime_source.template.json +8 -0
- data/viewer_data.template.json +29 -0
- data/viewer_manifest.template.json +29 -0
- index.html +258 -17
- js/app.js +423 -0
- js/charts.js +201 -0
- js/data.js +297 -0
- js/dom.js +88 -0
- js/overlays.js +164 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0000.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0001.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0002.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0003.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0004.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0005.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0006.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0007.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0008.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0009.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0010.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0011.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0012.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0013.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0014.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0015.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0016.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0017.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0018.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0019.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0020.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0021.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0022.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0023.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0024.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0025.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0026.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0027.json +0 -0
- runtime/chunks/q2-le2-lr2-e50_epoch_0028.json +0 -0
- 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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
assets/preview_datasets.png
ADDED
|
assets/preview_ui_sketch.png
ADDED
|
Git LFS Details
|
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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 & 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) => ({ "&": "&", "<": "<", ">": ">" }[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
|
|
|