Created
June 6, 2026 00:34
-
-
Save Helw150/9c1fd90a1b36a6851201d891bd77d081 to your computer and use it in GitHub Desktop.
Static mixture explorer build script: rank-INT swarm targets, DSP per-task fits, Tristan's gamma, Pareto-improver, embedded JS gradient-slider mechanic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Build a single-file static HTML mixture explorer from the swarm CSVs. | |
| Output: docs/index.html (data inlined, Plotly via CDN). | |
| Open it directly with `file://` or serve with `python -m http.server -d docs/`. | |
| The docs/ folder is what GitHub Pages serves. | |
| Per-task fits use Calvin's DSP canonical form: | |
| e_p,d = c_p,d * w_p,d | |
| z_d = e_0,d + gamma * e_1,d | |
| y_hat(w) = -b0 + sum_d a_d (1 - exp(-rho_d z_d)) | |
| - sum_d p_d softplus(log(1+z_d) - tau_d)^2 | |
| Fit with L-BFGS-B against per-task rank-INT z's; params dumped to JSON | |
| so the JS can evaluate the predictor live on the by-mixture tab. | |
| """ | |
| import json | |
| import os | |
| import jax | |
| import jax.numpy as jnp | |
| import numpy as np | |
| import polars as pl | |
| from jax import jit, value_and_grad | |
| from jax.flatten_util import ravel_pytree | |
| from scipy.optimize import minimize | |
| from scipy.stats import norm, rankdata | |
| raw = pl.read_csv("raw_metric_matrix_300m.csv", infer_schema_length=500).filter( | |
| pl.col("status") == "completed" | |
| ) | |
| _MMLU_KEEP = {"lm_eval/mmlu_sl_verb_5shot/bpb"} | |
| # Collapse all paloma sub-benchmarks into the single macro-average task. | |
| _PALOMA_KEEP = {"eval/paloma/macro_bpb"} | |
| _AGG_DROP = { | |
| "eval/bpb", "eval/macro_bpb", | |
| "eval/paloma/bpb", | |
| "eval/uncheatable_eval/bpb", "eval/uncheatable_eval/macro_bpb", | |
| } | |
| _TASK_DROP = { | |
| "teacher_forced/gsm8k_5shot_answer_hash/bpb", | |
| "lm_eval/arc_easy/bpb", | |
| "lm_eval/piqa/bpb", | |
| "mcq_smooth/swag_0shot/bpb", | |
| } | |
| _EXTRA = [("lm_eval/socialiqa_5shot/choice_logprob", +1.0)] | |
| def keep(c): | |
| # Override: paloma/macro_bpb doesn't end in /bpb, force-keep it. | |
| if c in _PALOMA_KEEP: | |
| return True | |
| if not c.endswith("/bpb"): | |
| return False | |
| if c in _AGG_DROP or c in _TASK_DROP: | |
| return False | |
| if not c.startswith(("eval/paloma/", "eval/uncheatable_eval/", | |
| "lm_eval/", "mcq_smooth/", "teacher_forced/")): | |
| return False | |
| if c.startswith("lm_eval/mmlu_") and c not in _MMLU_KEEP: | |
| return False | |
| # Drop every individual paloma subdomain — only the macro stays. | |
| if c.startswith("eval/paloma/"): | |
| return False | |
| return True | |
| cols_raw = set(raw.columns) | |
| task_cols, signs = [], [] | |
| for c in [x for x in raw.columns if keep(x)]: | |
| base = c.removesuffix("/bpb") | |
| cl = base + "/choice_logprob" | |
| if base.startswith(("lm_eval/", "mcq_smooth/")) and cl in cols_raw: | |
| task_cols.append(cl); signs.append(+1.0) | |
| else: | |
| task_cols.append(c); signs.append(-1.0) | |
| for c, s in _EXTRA: | |
| if c in cols_raw and c not in task_cols: | |
| task_cols.append(c); signs.append(s) | |
| signs = np.array(signs) | |
| X = raw.select(task_cols).to_numpy().astype(np.float64) * signs[None, :] | |
| n = X.shape[0] | |
| Z = norm.ppf((np.apply_along_axis(rankdata, 0, X) - 0.5) / n) | |
| # Integer rank 0..N-1 per (run, task) for the slider. argsort twice = ranks. | |
| # runByRank[t][r] gives the run with rank r on task t (O(1) lookup on drag). | |
| R = np.argsort(np.argsort(Z, axis=0), axis=0) | |
| runByRank = np.argsort(Z, axis=0).T | |
| domains = sorted(c.removeprefix("phase_0_") for c in raw.columns | |
| if c.startswith("phase_0_")) | |
| w0 = raw.select([f"phase_0_{d}" for d in domains]).to_numpy().astype(np.float64) | |
| w1 = raw.select([f"phase_1_{d}" for d in domains]).to_numpy().astype(np.float64) | |
| w0 = w0 / w0.sum(axis=1, keepdims=True) | |
| w1 = w1 / w1.sum(axis=1, keepdims=True) | |
| meta = pl.read_csv("two_phase_many_epoch_metadata.csv") | |
| meta_row = {r["domain_name"]: r for r in meta.iter_rows(named=True)} | |
| c0 = np.array([meta_row[d]["phase_0_epoch_multiplier"] for d in domains]) | |
| c1 = np.array([meta_row[d]["phase_1_epoch_multiplier"] for d in domains]) | |
| w0_prop = ((1.0 / c0) / (1.0 / c0).sum()).tolist() | |
| w1_prop = ((1.0 / c1) / (1.0 / c1).sum()).tolist() | |
| short = [c.removesuffix("/bpb").removesuffix("/choice_logprob") for c in task_cols] | |
| # Fixed y-axis order for the bar chart: largest natural-size domains on top | |
| # (w_d_prop ∝ 1/c_d). Stable across runs, no reshuffle on slider drag. | |
| domain_order = np.argsort(-np.asarray(w0_prop)).tolist() | |
| # Stable x-axis upper bound for absolute-weight view: round the global swarm | |
| # max up to the nearest 0.05 so the axis label is tidy. | |
| wmax = float(np.ceil(max(w0.max(), w1.max()) * 20) / 20) | |
| # Tristan's perplexity-correlations estimator γ_j (Thrush et al. 2024): | |
| # γ_j = Σ_{k,l} sign(y_k - y_l) (rank_j(x_k) - rank_j(x_l)) | |
| # computed for each (task, phase, domain). Normalised to [-1, 1] by | |
| # dividing by its maximum n(n²-1)/3 (the value at perfect agreement) | |
| # so the heatmap is read on the same scale as a correlation. The signed | |
| # pairwise sum is mathematically equivalent to Spearman's ρ. | |
| print("computing Tristan's γ_j…") | |
| _T_tasks = Z.shape[1] | |
| _D_dom = w0.shape[1] | |
| corr0 = np.zeros((_T_tasks, _D_dom)) | |
| corr1 = np.zeros((_T_tasks, _D_dom)) | |
| # Combined-phase weight per (run, domain) = w0 + w1. Lets the user see | |
| # "does *total* weight on this domain across both phases associate with | |
| # this task" without per-phase confounding. | |
| _w_sum = w0 + w1 | |
| corr_sum = np.zeros((_T_tasks, _D_dom)) | |
| def _tristan_gamma(x, y): | |
| # γ = Σ_{k,l} sign(y_k - y_l)(rank(x_k) - rank(x_l)) | |
| # = 2 Σ_k rank(x_k) · [2 rank(y_k) - n - 1] (by k↔l swap symmetry, | |
| # since Σ_l sign(y_k - y_l) = 2 rank(y_k) - n - 1 when y has no ties). | |
| _n = len(x) | |
| _rx = rankdata(x) | |
| _ry = rankdata(y) | |
| _gamma = 2.0 * float(np.sum(_rx * (2 * _ry - _n - 1))) | |
| _gamma_max = _n * (_n * _n - 1) / 3.0 # value at perfect agreement | |
| return _gamma / _gamma_max | |
| for _t in range(_T_tasks): | |
| for _d in range(_D_dom): | |
| corr0[_t, _d] = _tristan_gamma(w0[:, _d], Z[:, _t]) | |
| corr1[_t, _d] = _tristan_gamma(w1[:, _d], Z[:, _t]) | |
| corr_sum[_t, _d] = _tristan_gamma(_w_sum[:, _d], Z[:, _t]) | |
| # Fit Calvin's DSP form per task on the rank-INT'd targets. ~30-60s for 41 | |
| # tasks; runs once at build time, params dumped to JSON for live JS eval. | |
| _c0_jax = jnp.asarray(c0) | |
| _c1_jax = jnp.asarray(c1) | |
| _D = len(domains) | |
| def _spinv(x): | |
| return float(np.log(np.exp(x) - 1.0)) | |
| def _predict_dsp(theta, w0_b, w1_b): | |
| b0 = theta["b0"] | |
| rho = jax.nn.softplus(theta["log_rho"]) | |
| tau = theta["tau"] | |
| gamma = jax.nn.softplus(theta["log_gamma"]) | |
| a = jax.nn.softplus(theta["log_a"]) | |
| p = jax.nn.softplus(theta["log_p"]) | |
| e0 = w0_b * _c0_jax[None, :] | |
| e1 = w1_b * _c1_jax[None, :] | |
| z = e0 + gamma * e1 | |
| signal = a[None, :] * (1.0 - jnp.exp(-rho[None, :] * z)) | |
| u = jnp.log1p(z) - tau[None, :] | |
| penalty = p[None, :] * jax.nn.softplus(u) ** 2 | |
| return -(b0 - signal.sum(axis=1) + penalty.sum(axis=1)) | |
| _theta_init = { | |
| "b0": jnp.array(0.0), | |
| "log_rho": jnp.full(_D, _spinv(0.3)), | |
| "tau": jnp.full(_D, 2.0), | |
| "log_gamma": jnp.array(_spinv(1.0)), | |
| "log_a": jnp.full(_D, _spinv(0.1)), | |
| "log_p": jnp.full(_D, _spinv(0.01)), | |
| } | |
| _flat_init, _unravel = ravel_pytree(_theta_init) | |
| @jit | |
| def _loss_vg(theta_flat, w0_b, w1_b, y_b): | |
| def loss(t): | |
| return jnp.mean((_predict_dsp(_unravel(t), w0_b, w1_b) - y_b) ** 2) | |
| return value_and_grad(loss)(theta_flat) | |
| def _fit_one(y): | |
| def f(t): | |
| v, g = _loss_vg(jnp.asarray(t), | |
| jnp.asarray(w0), jnp.asarray(w1), jnp.asarray(y)) | |
| return float(v), np.asarray(g, dtype=np.float64) | |
| res = minimize(f, np.asarray(_flat_init, dtype=np.float64), | |
| method="L-BFGS-B", jac=True, | |
| options={"maxiter": 300, "ftol": 1e-9}) | |
| return _unravel(jnp.asarray(res.x)) | |
| print(f"fitting DSP form for {Z.shape[1]} tasks…") | |
| dsp_params = [] | |
| for _t in range(Z.shape[1]): | |
| th = _fit_one(Z[:, _t]) | |
| # Residual RMSE on the swarm = per-task prediction-uncertainty std, | |
| # in rank-INT z units. Used in the UI to draw error bars around the | |
| # predicted percentile. | |
| _pred = np.asarray(_predict_dsp(th, | |
| jnp.asarray(w0), jnp.asarray(w1))) | |
| _sigma = float(np.sqrt(np.mean((_pred - Z[:, _t]) ** 2))) | |
| dsp_params.append({ | |
| "b0": round(float(th["b0"]), 5), | |
| "log_gamma": round(float(th["log_gamma"]), 5), | |
| "log_rho": [round(float(x), 5) for x in th["log_rho"]], | |
| "tau": [round(float(x), 5) for x in th["tau"]], | |
| "log_a": [round(float(x), 5) for x in th["log_a"]], | |
| "log_p": [round(float(x), 5) for x in th["log_p"]], | |
| "sigma": round(_sigma, 4), | |
| }) | |
| print(f" [{_t+1:>2d}/{Z.shape[1]}] {short[_t]:50s} sigma={_sigma:.3f}") | |
| # Predicted-Pareto-improver mixture vs token-proportional. We optimise | |
| # L(w) = sum_t max(0, z_prop_t + eps - z_pred_t(w))^2 | |
| # i.e. squared-shortfall against the per-task predictions at the | |
| # proportional baseline, with a small margin eps so the optimiser pushes | |
| # for strict improvement. Adam on softmax(theta) keeps w on the simplex | |
| # without projection. | |
| print("optimising Pareto-improver mixture vs proportional…") | |
| _w0_prop_jax = jnp.asarray((1.0 / c0) / (1.0 / c0).sum()) | |
| _w1_prop_jax = jnp.asarray((1.0 / c1) / (1.0 / c1).sum()) | |
| _b0_arr = jnp.asarray([p["b0"] for p in dsp_params]) | |
| _log_gamma_arr = jnp.asarray([p["log_gamma"] for p in dsp_params]) | |
| _log_rho_arr = jnp.asarray([p["log_rho"] for p in dsp_params]) | |
| _tau_arr = jnp.asarray([p["tau"] for p in dsp_params]) | |
| _log_a_arr = jnp.asarray([p["log_a"] for p in dsp_params]) | |
| _log_p_arr = jnp.asarray([p["log_p"] for p in dsp_params]) | |
| @jit | |
| def _predict_all(w0v, w1v): | |
| gamma = jax.nn.softplus(_log_gamma_arr)[:, None] | |
| rho = jax.nn.softplus(_log_rho_arr) | |
| a_t = jax.nn.softplus(_log_a_arr) | |
| p_t = jax.nn.softplus(_log_p_arr) | |
| e0 = (w0v * _c0_jax)[None, :] | |
| e1 = (w1v * _c1_jax)[None, :] | |
| z = e0 + gamma * e1 | |
| signal = a_t * (1.0 - jnp.exp(-rho * z)) | |
| u = jnp.log1p(z) - _tau_arr | |
| penalty = p_t * jax.nn.softplus(u) ** 2 | |
| return -(_b0_arr - signal.sum(axis=1) + penalty.sum(axis=1)) | |
| _z_baseline = _predict_all(_w0_prop_jax, _w1_prop_jax) | |
| _eps_margin = 0.05 | |
| @jit | |
| def _pareto_loss(theta0, theta1): | |
| w0v = jax.nn.softmax(theta0) | |
| w1v = jax.nn.softmax(theta1) | |
| zs = _predict_all(w0v, w1v) | |
| shortfall = jnp.maximum(0.0, _z_baseline + _eps_margin - zs) | |
| return jnp.sum(shortfall ** 2) | |
| @jit | |
| def _adam_step(theta0, theta1, m0, m1, v0, v1, t): | |
| g0, g1 = jax.grad(_pareto_loss, argnums=(0, 1))(theta0, theta1) | |
| lr, b1, b2, eps = 0.05, 0.9, 0.999, 1e-8 | |
| m0 = b1 * m0 + (1 - b1) * g0 | |
| m1 = b1 * m1 + (1 - b1) * g1 | |
| v0 = b2 * v0 + (1 - b2) * g0 ** 2 | |
| v1 = b2 * v1 + (1 - b2) * g1 ** 2 | |
| bc = 1 - b1 ** (t + 1) | |
| bv = 1 - b2 ** (t + 1) | |
| theta0 = theta0 - lr * (m0 / bc) / (jnp.sqrt(v0 / bv) + eps) | |
| theta1 = theta1 - lr * (m1 / bc) / (jnp.sqrt(v1 / bv) + eps) | |
| return theta0, theta1, m0, m1, v0, v1 | |
| _theta0 = jnp.log(_w0_prop_jax + 1e-12) | |
| _theta1 = jnp.log(_w1_prop_jax + 1e-12) | |
| _m0 = jnp.zeros_like(_theta0); _m1 = jnp.zeros_like(_theta1) | |
| _v0 = jnp.zeros_like(_theta0); _v1 = jnp.zeros_like(_theta1) | |
| for _step in range(1000): | |
| _theta0, _theta1, _m0, _m1, _v0, _v1 = _adam_step( | |
| _theta0, _theta1, _m0, _m1, _v0, _v1, _step | |
| ) | |
| w_pareto0 = np.asarray(jax.nn.softmax(_theta0)) | |
| w_pareto1 = np.asarray(jax.nn.softmax(_theta1)) | |
| _z_pareto = np.asarray(_predict_all(jax.nn.softmax(_theta0), | |
| jax.nn.softmax(_theta1))) | |
| _delta = _z_pareto - np.asarray(_z_baseline) | |
| _n_improved = int((_delta > 0).sum()) | |
| _n_strict = int((_delta > _eps_margin).sum()) | |
| print(f" improved on {_n_improved}/{_T_tasks} tasks " | |
| f"(strict >+{_eps_margin}: {_n_strict})") | |
| print(f" min Δz = {_delta.min():+.3f} median Δz = " | |
| f"{float(np.median(_delta)):+.3f} max Δz = {_delta.max():+.3f}") | |
| data = { | |
| "task_names": short, | |
| "domain_names": domains, | |
| "domain_order": domain_order, | |
| "wmax": wmax, | |
| "c0": [round(float(x), 4) for x in c0], | |
| "c1": [round(float(x), 4) for x in c1], | |
| "R": R.tolist(), # rank per (run, task), 0..N-1 | |
| "runByRank": runByRank.tolist(), # T × N: run idx with rank r on task t | |
| "w0": np.round(w0, 4).tolist(), | |
| "w1": np.round(w1, 4).tolist(), | |
| "w0_prop": [round(x, 4) for x in w0_prop], | |
| "w1_prop": [round(x, 4) for x in w1_prop], | |
| "w0_pareto": [round(float(x), 4) for x in w_pareto0], | |
| "w1_pareto": [round(float(x), 4) for x in w_pareto1], | |
| "dsp_params": dsp_params, | |
| "corr0": np.round(corr0, 3).tolist(), | |
| "corr1": np.round(corr1, 3).tolist(), | |
| "corr_sum": np.round(corr_sum, 3).tolist(), | |
| } | |
| HTML = r"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Swarm Mixture Explorer</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| margin: 0; padding: clamp(12px, 3vw, 28px); color: #1a1a1a; | |
| max-width: 1400px; margin: 0 auto; } | |
| h1 { font-size: clamp(18px, 3vw, 22px); margin: 0 0 6px; } | |
| .intro { color: #555; font-size: clamp(12px, 1.5vw, 13px); line-height: 1.45; | |
| max-width: 900px; margin-bottom: 18px; } | |
| .run-info { font-weight: 600; margin: 18px 0 6px; | |
| font-size: clamp(13px, 1.6vw, 14px); } | |
| .tabs { display: flex; gap: 0; border-bottom: 1px solid #ddd; | |
| margin: 8px 0 14px; } | |
| .tab { padding: 7px 14px; border: 1px solid transparent; border-bottom: none; | |
| background: transparent; cursor: pointer; font: inherit; | |
| font-size: 13px; color: #666; border-radius: 4px 4px 0 0; } | |
| .tab.active { border-color: #ddd; background: white; color: #1a1a1a; | |
| font-weight: 600; margin-bottom: -1px; } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; } | |
| .controls { display: flex; gap: 12px; align-items: center; margin: 12px 0 0; | |
| font-size: 13px; flex-wrap: wrap; } | |
| .controls select, .controls button { font-size: 13px; padding: 3px 8px; | |
| font-family: inherit; } | |
| .controls button { cursor: pointer; background: #f5f5f5; | |
| border: 1px solid #ccc; border-radius: 3px; } | |
| .controls button:hover { background: #ebebeb; } | |
| /* The mixture-view dropdown only applies to the mixture chart, which we | |
| only show on the "By task results" tab. */ | |
| body[data-tab="mixture"] .view-mode-control { display: none; } | |
| /* The reset button only makes sense on the mix tab. */ | |
| body:not([data-tab="mixture"]) .mix-only { display: none; } | |
| /* Correlations tab is a read-only viewer. */ | |
| body[data-tab="corr"] .controls { display: none; } | |
| .section { margin-bottom: 14px; } | |
| .section-title { font-weight: 600; font-size: 12px; margin: 0 0 4px; | |
| color: #333; text-transform: uppercase; letter-spacing: 0.04em; } | |
| /* auto-fit makes columns wrap when there's no room (1 col on phones, up to | |
| 4 cols on a 1400px desktop). */ | |
| .slider-grid { display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); | |
| gap: 4px 14px; } | |
| .slider-row { display: flex; align-items: center; gap: 6px; font-size: 11px; | |
| line-height: 1.3; min-width: 0; } | |
| .slider-label { flex: 0 0 clamp(110px, 28%, 170px); white-space: normal; | |
| line-height: 1.2; overflow: hidden; color: #333; | |
| overflow-wrap: anywhere; } | |
| .slider-label .quality { font-size: 9px; color: #888; font-style: italic; } | |
| .slider-row input[type=range] { flex: 1; min-width: 0; accent-color: #2c6aa0; | |
| touch-action: manipulation; } | |
| .slider-val { flex: 0 0 36px; text-align: right; | |
| font-variant-numeric: tabular-nums; color: #666; } | |
| #chart { margin-top: 14px; width: 100%; } | |
| .model-info { margin: 18px 0 0; padding: 10px 14px; background: #fafafa; | |
| border: 1px solid #e0e0e0; border-radius: 5px; | |
| font-size: 12.5px; line-height: 1.5; color: #333; } | |
| .model-info summary { cursor: pointer; font-weight: 600; padding: 4px 0; | |
| user-select: none; color: #1a1a1a; } | |
| .model-info[open] summary { margin-bottom: 8px; } | |
| .model-info p { margin: 8px 0; } | |
| .model-info .formula { overflow-x: auto; margin: 8px 0; } | |
| </style> | |
| <script src="https://cdn.plot.ly/plotly-2.35.0.min.js"></script> | |
| <link rel="stylesheet" | |
| href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"> | |
| <script defer | |
| src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script> | |
| <script defer | |
| src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js" | |
| onload="renderMathInElement(document.body)"></script> | |
| </head> | |
| <body> | |
| <h1>Swarm Mixture Explorer</h1> | |
| <p class="intro"> | |
| Two views on the 241-run swarm. | |
| <b>By mixture</b> — predictive: per-(phase, domain) weight sliders. | |
| Drag any slider to set the demanded mixture (other sliders in the same phase | |
| auto-adjust proportionally to keep Σw = 1); we evaluate Calvin's DSP fit | |
| on that mixture and the chart below shows the <i>predicted</i> per-task | |
| percentiles. Predictions near the swarm hull are reliable; far away they're | |
| the form extrapolating. | |
| <b>By task results</b> — empirical: each task has a slider whose stops | |
| are the 241 runs ordered by outcome on that task. Drag any slider to pick the | |
| run at that rank; all other sliders snap to that run's per-task percentiles, | |
| and the chart below shows its actual mixture. | |
| </p> | |
| <div class="tabs"> | |
| <button class="tab active" data-tab="mixture">By mixture</button> | |
| <button class="tab" data-tab="results">By task results</button> | |
| <button class="tab" data-tab="corr">Rank correlations</button> | |
| </div> | |
| <div id="tab-mixture" class="tab-content active"> | |
| <div id="run-info-mix" class="run-info"></div> | |
| <div id="sliders-mix"></div> | |
| <details class="model-info"> | |
| <summary>About the model (Calvin's DSP form)</summary> | |
| <p>Per-task fit of the rank-INT'd swarm-percentile z to the mixture | |
| \((w_0, w_1)\). For each domain \(d\), effective exposure stacks | |
| training time across the two phases:</p> | |
| <div class="formula"> | |
| \[ e_{0,d} = c_{0,d}\, w_{0,d}, \qquad | |
| e_{1,d} = c_{1,d}\, w_{1,d}, \qquad | |
| z_d = e_{0,d} + \gamma\, e_{1,d}. \] | |
| </div> | |
| <p>Predicted rank-INT z is a saturating per-domain benefit minus an | |
| overexposure penalty:</p> | |
| <div class="formula"> | |
| \[ \hat y(w) = -b_0 | |
| + \sum_{d=1}^{D} a_d \bigl(1 - e^{-\rho_d z_d}\bigr) | |
| - \sum_{d=1}^{D} p_d\,\operatorname{softplus}\!\bigl(\log(1+z_d) - \tau_d\bigr)^{2}. \] | |
| </div> | |
| <p>Predicted swarm percentile is \(\Phi(\hat y) \times 100\) (targets | |
| are rank-INT'd to \(\mathcal N(0,1)\) per task by construction). | |
| Per task: \(4D + 2 = 158\) parameters (\(D = 39\) domains), fit by | |
| L-BFGS-B against the 241-run swarm. The \(\pm 1\sigma\) band on each | |
| bar of the chart below is the fit's residual RMSE for that task, run | |
| through \(\Phi\) to get an asymmetric percentile interval.</p> | |
| </details> | |
| </div> | |
| <div id="tab-results" class="tab-content"> | |
| <div id="run-info-task" class="run-info"></div> | |
| <div id="sliders"></div> | |
| </div> | |
| <div id="tab-corr" class="tab-content"> | |
| <p class="intro" style="margin-top: 12px;"> | |
| <b>Tristan's γ<sub>j</sub></b> (Thrush et al. 2024, | |
| perplexity-correlations): \(\gamma_j = \sum_{k,l} \mathrm{sign}(y_k - y_l) | |
| (\mathrm{rank}_j(x_k) - \mathrm{rank}_j(x_l))\), normalised by its | |
| max so cells lie in [-1, 1]. <b>Blue = more weight on this (phase, | |
| domain) associates with higher z on this task; red = opposite</b>; | |
| white ≈ no monotone relationship. Equivalent to Spearman ρ up to | |
| scaling. Monotonic by construction — can't see the inverted-U | |
| overexposure dynamics that DSP's \(-p_d\,\mathrm{softplus}(\log(1+z_d) | |
| - \tau_d)^2\) penalty term models explicitly. So a domain that's | |
| helpful up to ~1 epoch and harmful past it will look white here even | |
| when DSP gives it a sharp τ. <b>Third panel</b> aggregates the two | |
| phases: γ between \(w_{0,d} + w_{1,d}\) and z, useful when you care | |
| about total presence of a domain rather than which phase it's in. | |
| </p> | |
| </div> | |
| <div class="controls"> | |
| <label class="view-mode-control">Mixture view: | |
| <select id="view-mode"> | |
| <option value="delta" selected>Δ from token-proportional</option> | |
| <option value="abs">Absolute mixture weight</option> | |
| </select> | |
| </label> | |
| <button id="download-mix">Download mixture (CSV)</button> | |
| <button id="download-results">Download task results (CSV)</button> | |
| <button id="reset-mix">Reset to proportional</button> | |
| <button id="reset-david">Reset to David's favorite mix</button> | |
| <button id="reset-pareto" title="DSP-optimised mixture predicted to beat proportional on every task by ≥ +0.05 z">Reset to predicted Pareto-improver</button> | |
| </div> | |
| <div id="chart"></div> | |
| <div id="chart-mix-detail" class="mix-only" style="width:100%;margin-top:18px;"></div> | |
| <script> | |
| const DATA = __DATA__; | |
| const N = DATA.runByRank[0].length; // 241 swarm runs | |
| const T = DATA.task_names.length; | |
| const SECTIONS = [ | |
| {key: "lm_eval", prefixes: ["lm_eval/", "mcq_smooth/", "teacher_forced/"]}, | |
| {key: "paloma", prefixes: ["eval/paloma/"]}, | |
| {key: "uncheatable_eval", prefixes: ["eval/uncheatable_eval/"]}, | |
| ]; | |
| const STRIP = ["eval/paloma/", "eval/uncheatable_eval/", | |
| "lm_eval/", "mcq_smooth/", "teacher_forced/"]; | |
| // Common dataset-name prefixes. Strip so the slider/y-axis label of | |
| // "dolma3_cc/art_and_design_high" becomes "art_and_design_high" instead of | |
| // getting truncated by the label column width. | |
| const DOMAIN_STRIP = ["dolma3_cc/", "dolma3_", "dolmino_"]; | |
| function sectionOf(name) { | |
| for (const s of SECTIONS) | |
| if (s.prefixes.some(p => name.startsWith(p))) return s.key; | |
| return "other"; | |
| } | |
| function shortLabel(name) { | |
| for (const p of STRIP) if (name.startsWith(p)) return name.slice(p.length); | |
| return name; | |
| } | |
| // [stripped name, "High Quality"/"Low Quality" or null] | |
| function domainParts(name) { | |
| let s = name; | |
| for (const p of DOMAIN_STRIP) | |
| if (s.startsWith(p)) { s = s.slice(p.length); break; } | |
| if (s.endsWith("_high")) return [s.slice(0, -5), "High Quality"]; | |
| if (s.endsWith("_low")) return [s.slice(0, -4), "Low Quality"]; | |
| return [s, null]; | |
| } | |
| function domainLabelHTML(name) { | |
| const [m, q] = domainParts(name); | |
| return q ? `${m}<br><span class="quality">${q}</span>` : m; | |
| } | |
| function domainLabelChart(name) { | |
| // Plotly y-axis: strip the common prefix but keep the _high/_low suffix | |
| // inline. Multi-line labels look bad in the chart; the suffix alone is | |
| // enough to distinguish a pair. | |
| let s = name; | |
| for (const p of DOMAIN_STRIP) | |
| if (s.startsWith(p)) { s = s.slice(p.length); break; } | |
| return s; | |
| } | |
| // Rank r ∈ [0, N-1] → percentile in (0, 100). N+1 spacing so neither endpoint | |
| // hits exactly 0 or 100. | |
| const pctOf = r => ((r + 1) / (N + 1)) * 100; | |
| function buildSliders() { | |
| const container = document.getElementById("sliders"); | |
| const groups = {}; | |
| DATA.task_names.forEach((name, idx) => { | |
| const sec = sectionOf(name); | |
| (groups[sec] = groups[sec] || []).push({name, idx}); | |
| }); | |
| SECTIONS.forEach(s => { | |
| const items = groups[s.key]; | |
| if (!items) return; | |
| const sec = document.createElement("div"); | |
| sec.className = "section"; | |
| const title = document.createElement("div"); | |
| title.className = "section-title"; | |
| title.textContent = `${s.key} · ${items.length}`; | |
| sec.appendChild(title); | |
| const grid = document.createElement("div"); | |
| grid.className = "slider-grid"; | |
| items.forEach(({name, idx}) => { | |
| const z0 = predictDSP(DATA.dsp_params[idx], MIX0, MIX1); | |
| const init = normCdf(z0) * 100; | |
| const row = document.createElement("div"); | |
| row.className = "slider-row"; | |
| row.innerHTML = ` | |
| <span class="slider-label" title="${name}">${shortLabel(name)}</span> | |
| <input type="range" min="0" max="100" step="0.5" value="${init}" id="s_${idx}"> | |
| <span class="slider-val" id="v_${idx}">${init.toFixed(1)}</span>`; | |
| grid.appendChild(row); | |
| const slider = row.querySelector("input"); | |
| // Drag = small gradient step on MIX toward higher (or lower) | |
| // predicted percentile on this task. Live, continuous, simplex-safe. | |
| slider.addEventListener("input", () => { | |
| const curZ = predictDSP(DATA.dsp_params[idx], MIX0, MIX1); | |
| const curPct = normCdf(curZ) * 100; | |
| const newPct = parseFloat(slider.value); | |
| stepMixTowardTask(idx, newPct - curPct); | |
| syncTaskSlidersFromMix(); | |
| syncMixSliders(0); | |
| syncMixSliders(1); | |
| updateChart(); | |
| scheduleURL(); | |
| }); | |
| }); | |
| sec.appendChild(grid); | |
| container.appendChild(sec); | |
| }); | |
| } | |
| // "By mixture" tab state: free-floating demanded mixture; not tied to any | |
| // swarm run. Starts at the token-proportional baseline so initial predictions | |
| // reflect "what plain proportional sampling would get." | |
| const MIX0 = DATA.w0_prop.slice(); | |
| const MIX1 = DATA.w1_prop.slice(); | |
| // Keep the mixture on the simplex: when the user pushes one slider by Δ, | |
| // take it proportionally from every other domain in the same phase (or give | |
| // it back proportionally if Δ is negative). Preserves Σw = 1 exactly. | |
| function redistribute(phase, idx, newVal) { | |
| const mix = phase === 0 ? MIX0 : MIX1; | |
| const D = mix.length; | |
| const oldVal = mix[idx]; | |
| newVal = Math.min(1, Math.max(0, newVal)); | |
| if (newVal >= 1) { | |
| for (let d = 0; d < D; d++) mix[d] = (d === idx) ? 1 : 0; | |
| return; | |
| } | |
| const otherSum = 1 - oldVal; | |
| if (otherSum <= 1e-9) { | |
| // All other weights were ~0; share the freed mass evenly. | |
| const share = (1 - newVal) / (D - 1); | |
| for (let d = 0; d < D; d++) mix[d] = (d === idx) ? newVal : share; | |
| return; | |
| } | |
| const scale = (1 - newVal) / otherSum; | |
| for (let d = 0; d < D; d++) { | |
| mix[d] = (d === idx) ? newVal : mix[d] * scale; | |
| } | |
| } | |
| function syncMixSliders(phase) { | |
| const mix = phase === 0 ? MIX0 : MIX1; | |
| const D = mix.length; | |
| for (let d = 0; d < D; d++) { | |
| const v = mix[d]; | |
| document.getElementById(`m_${phase}_${d}`).value = v; | |
| document.getElementById(`mv_${phase}_${d}`).textContent = v.toFixed(3); | |
| } | |
| } | |
| function buildMixSliders() { | |
| const container = document.getElementById("sliders-mix"); | |
| const D = DATA.domain_names.length; | |
| const mixArr = [MIX0, MIX1]; | |
| ["phase 0", "phase 1"].forEach((phaseName, p) => { | |
| const sec = document.createElement("div"); | |
| sec.className = "section"; | |
| const title = document.createElement("div"); | |
| title.className = "section-title"; | |
| title.textContent = `${phaseName} · ${D}`; | |
| sec.appendChild(title); | |
| const grid = document.createElement("div"); | |
| grid.className = "slider-grid"; | |
| DATA.domain_names.forEach((d, idx) => { | |
| // Sliders span the full simplex (0..1) since redistribution can push | |
| // any single domain anywhere in that range. Step 0.001. | |
| const init = mixArr[p][idx]; | |
| const row = document.createElement("div"); | |
| row.className = "slider-row"; | |
| row.innerHTML = ` | |
| <span class="slider-label" title="${d}">${domainLabelHTML(d)}</span> | |
| <input type="range" min="0" max="1" step="0.001" | |
| value="${init}" id="m_${p}_${idx}"> | |
| <span class="slider-val" id="mv_${p}_${idx}">${init.toFixed(3)}</span>`; | |
| grid.appendChild(row); | |
| const slider = row.querySelector("input"); | |
| slider.addEventListener("input", () => { | |
| redistribute(p, idx, parseFloat(slider.value)); | |
| syncMixSliders(p); | |
| updateChart(); | |
| scheduleURL(); | |
| }); | |
| }); | |
| sec.appendChild(grid); | |
| container.appendChild(sec); | |
| }); | |
| } | |
| // JS-side DSP evaluator. Mirrors _predict_dsp() in build_static.py. | |
| function softplus(x) { | |
| return x > 0 ? x + Math.log1p(Math.exp(-x)) | |
| : Math.log1p(Math.exp(x)); | |
| } | |
| function erf(x) { | |
| const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741; | |
| const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911; | |
| const s = x < 0 ? -1 : 1; x = Math.abs(x); | |
| const t = 1 / (1 + p * x); | |
| return s * (1 - ((((a5*t + a4)*t + a3)*t + a2)*t + a1)*t * Math.exp(-x*x)); | |
| } | |
| function normCdf(x) { return 0.5 * (1 + erf(x / Math.SQRT2)); } | |
| function predictDSP(p, w0, w1) { | |
| const D = DATA.c0.length; | |
| const gamma = softplus(p.log_gamma); | |
| let signalSum = 0, penaltySum = 0; | |
| for (let i = 0; i < D; i++) { | |
| const z = w0[i] * DATA.c0[i] + gamma * w1[i] * DATA.c1[i]; | |
| const rho = softplus(p.log_rho[i]); | |
| const a = softplus(p.log_a[i]); | |
| const pen = softplus(p.log_p[i]); | |
| signalSum += a * (1 - Math.exp(-rho * z)); | |
| const u = Math.log1p(z) - p.tau[i]; | |
| const sp = softplus(u); | |
| penaltySum += pen * sp * sp; | |
| } | |
| return -(p.b0 - signalSum + penaltySum); | |
| } | |
| // One prediction call per task; returns the central estimate + a 1-sigma band | |
| // in percentile units. Sigma is the swarm-residual RMSE from the fit, so | |
| // noisier (less predictable) tasks get visibly wider error bars. | |
| function predictAllWithUncertainty(w0, w1) { | |
| return DATA.dsp_params.map(p => { | |
| const z = predictDSP(p, w0, w1); | |
| return { | |
| pct: normCdf(z) * 100, | |
| pctLow: normCdf(z - p.sigma) * 100, | |
| pctHi: normCdf(z + p.sigma) * 100, | |
| }; | |
| }); | |
| } | |
| // Analytical gradient of predictDSP w.r.t. (w0, w1). DSP form: | |
| // z_d = c0[d]*w0[d] + gamma * c1[d]*w1[d] | |
| // pred = -(b0 - Σ a_d (1 - e^{-rho_d z_d}) + Σ p_d softplus(log(1+z_d) - tau_d)^2) | |
| // dpred/dz_d = a_d * rho_d * e^{-rho_d z_d} | |
| // - p_d * 2 * softplus(u_d) * sigmoid(u_d) / (1+z_d) | |
| // with u_d = log(1+z_d) - tau_d. | |
| function gradPredict(p, w0, w1) { | |
| const D = w0.length; | |
| const gamma = softplus(p.log_gamma); | |
| const g0 = new Array(D); | |
| const g1 = new Array(D); | |
| for (let d = 0; d < D; d++) { | |
| const z = w0[d] * DATA.c0[d] + gamma * w1[d] * DATA.c1[d]; | |
| const rho = softplus(p.log_rho[d]); | |
| const a = softplus(p.log_a[d]); | |
| const pen = softplus(p.log_p[d]); | |
| const u = Math.log1p(z) - p.tau[d]; | |
| const sp = softplus(u); | |
| const sig = 1.0 / (1.0 + Math.exp(-u)); | |
| const dpred_dz = a * rho * Math.exp(-rho * z) | |
| - pen * 2 * sp * sig / (1 + z); | |
| g0[d] = dpred_dz * DATA.c0[d]; | |
| g1[d] = dpred_dz * gamma * DATA.c1[d]; | |
| } | |
| return [g0, g1]; | |
| } | |
| // Take a gradient step on MIX0/MIX1 toward Δpct on a target task. | |
| // Converts Δpct → Δz via the local norm.cdf slope so the step has the | |
| // right scale, then projects back to the simplex. | |
| function stepMixTowardTask(taskIdx, deltaPct) { | |
| const p = DATA.dsp_params[taskIdx]; | |
| const curZ = predictDSP(p, MIX0, MIX1); | |
| // d(pct)/dz at curZ = phi(curZ) * 100; invert to get Δz from Δpct. | |
| const phi = Math.exp(-curZ * curZ / 2) / Math.sqrt(2 * Math.PI); | |
| const deltaZ = (deltaPct / 100) / Math.max(phi, 1e-4); | |
| const [g0, g1] = gradPredict(p, MIX0, MIX1); | |
| let gNormSq = 0; | |
| for (let d = 0; d < g0.length; d++) gNormSq += g0[d]*g0[d] + g1[d]*g1[d]; | |
| if (gNormSq < 1e-12) return; | |
| const alpha = deltaZ / gNormSq; | |
| for (let d = 0; d < MIX0.length; d++) { | |
| MIX0[d] = Math.max(0, MIX0[d] + alpha * g0[d]); | |
| MIX1[d] = Math.max(0, MIX1[d] + alpha * g1[d]); | |
| } | |
| // Renormalise each phase back onto the simplex. | |
| let s0 = 0, s1 = 0; | |
| for (let d = 0; d < MIX0.length; d++) { s0 += MIX0[d]; s1 += MIX1[d]; } | |
| if (s0 > 0) for (let d = 0; d < MIX0.length; d++) MIX0[d] /= s0; | |
| if (s1 > 0) for (let d = 0; d < MIX0.length; d++) MIX1[d] /= s1; | |
| } | |
| // Re-sync task sliders to current MIX predictions. | |
| function syncTaskSlidersFromMix() { | |
| for (let t = 0; t < T; t++) { | |
| const z = predictDSP(DATA.dsp_params[t], MIX0, MIX1); | |
| const pct = normCdf(z) * 100; | |
| const sld = document.getElementById("s_" + t); | |
| if (sld) sld.value = pct; | |
| const lbl = document.getElementById("v_" + t); | |
| if (lbl) lbl.textContent = pct.toFixed(1); | |
| } | |
| const ri = document.getElementById("run-info-task"); | |
| if (ri) updateTaskInfo(); | |
| } | |
| // ---------- URL state ---------- | |
| // Active tab + shared MIX. Both mix and task tabs steer the same MIX. | |
| // #tab=mixture&w0=...&w1=... | |
| // #tab=results&w0=...&w1=...&view=delta | |
| // #tab=corr | |
| function updateURL() { | |
| const params = new URLSearchParams(); | |
| const tab = activeTab(); | |
| params.set("tab", tab); | |
| if (tab !== "corr") { | |
| params.set("w0", MIX0.map(x => x.toFixed(4)).join(",")); | |
| params.set("w1", MIX1.map(x => x.toFixed(4)).join(",")); | |
| if (tab === "results") { | |
| params.set("view", document.getElementById("view-mode").value); | |
| } | |
| } | |
| history.replaceState(null, "", "#" + params.toString()); | |
| } | |
| let _urlT; | |
| function scheduleURL() { | |
| clearTimeout(_urlT); | |
| _urlT = setTimeout(updateURL, 200); | |
| } | |
| function activateTab(tab) { | |
| document.querySelectorAll(".tab").forEach(b => | |
| b.classList.toggle("active", b.dataset.tab === tab)); | |
| document.querySelectorAll(".tab-content").forEach(c => | |
| c.classList.toggle("active", c.id === "tab-" + tab)); | |
| document.body.dataset.tab = tab; | |
| } | |
| function loadFromURL() { | |
| if (!location.hash) return false; | |
| const params = new URLSearchParams(location.hash.slice(1)); | |
| const tab = params.get("tab"); | |
| if (!tab) return false; | |
| activateTab(tab); | |
| const w0s = params.get("w0"), w1s = params.get("w1"); | |
| if (w0s && w1s) { | |
| const v0 = w0s.split(",").map(parseFloat); | |
| const v1 = w1s.split(",").map(parseFloat); | |
| if (v0.length === MIX0.length && v1.length === MIX1.length | |
| && v0.every(Number.isFinite) && v1.every(Number.isFinite)) { | |
| for (let d = 0; d < MIX0.length; d++) { | |
| MIX0[d] = v0[d]; | |
| MIX1[d] = v1[d]; | |
| } | |
| syncMixSliders(0); | |
| syncMixSliders(1); | |
| syncTaskSlidersFromMix(); | |
| } | |
| } | |
| const view = params.get("view"); | |
| if (view) document.getElementById("view-mode").value = view; | |
| updateChart(); | |
| return true; | |
| } | |
| // Tab toggle. Also flip the chart between mixture and task-results. | |
| document.querySelectorAll(".tab").forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| activateTab(btn.dataset.tab); | |
| updateChart(); | |
| updateURL(); | |
| }); | |
| }); | |
| document.body.dataset.tab = "mixture"; | |
| function updateMixInfo(pcts) { | |
| const mean = pcts.reduce((a,b)=>a+b, 0) / pcts.length; | |
| document.getElementById("run-info-mix").textContent = | |
| `Predicted mean percentile ${mean.toFixed(1)} for the current demanded mixture`; | |
| } | |
| function updateTaskInfo() { | |
| let sumZ = 0; | |
| for (let t = 0; t < T; t++) sumZ += predictDSP(DATA.dsp_params[t], MIX0, MIX1); | |
| const meanPct = normCdf(sumZ / T) * 100; | |
| document.getElementById("run-info-task").textContent = | |
| `Predicted mean percentile ${meanPct.toFixed(1)} for the current demanded mixture`; | |
| } | |
| function activeTab() { | |
| return document.querySelector(".tab.active").dataset.tab; | |
| } | |
| function updateChart() { | |
| const t = activeTab(); | |
| if (t === "mixture") { | |
| renderPredictedTaskChart(); | |
| renderMixDetail(); | |
| } else if (t === "corr") { | |
| renderCorrelations(); | |
| } else { | |
| renderMixChart(MIX0, MIX1); | |
| } | |
| } | |
| function renderMixDetail() { | |
| // Demanded-mixture bar chart at the bottom of the mix tab. Always shows | |
| // delta from token-proportional so it's a direct "how far am I asking | |
| // the optimiser to push each domain?" view. | |
| const d0 = MIX0.map((w, i) => w - DATA.w0_prop[i]); | |
| const d1 = MIX1.map((w, i) => w - DATA.w1_prop[i]); | |
| const order = DATA.domain_order; | |
| const names = order.map(i => domainLabelChart(DATA.domain_names[i])); | |
| const d0o = order.map(i => d0[i]); | |
| const d1o = order.map(i => d1[i]); | |
| const color = arr => arr.map(x => | |
| x > 0 ? "rgba(44,160,44,0.85)" : "rgba(214,39,40,0.85)"); | |
| const narrow = window.innerWidth < 700; | |
| const traces = [ | |
| {type:"bar", orientation:"h", x:d0o, y:names, xaxis:"x", yaxis:"y", | |
| marker:{color: color(d0o)}, showlegend:false, | |
| hovertemplate:"%{y}: %{x:+.3f}<extra></extra>"}, | |
| {type:"bar", orientation:"h", x:d1o, y:names, xaxis:"x2", yaxis:"y2", | |
| marker:{color: color(d1o)}, showlegend:false, | |
| hovertemplate:"%{y}: %{x:+.3f}<extra></extra>"}, | |
| ]; | |
| const layout = { | |
| grid: {rows: narrow ? 2 : 1, columns: narrow ? 1 : 2, pattern:"independent"}, | |
| height: narrow ? 1100 : 620, | |
| margin: {l: narrow ? 140 : 240, r: 20, t: 60, b: 50}, | |
| title: {text: "Demanded mixture — Δ from token-proportional", | |
| font:{size: 14}, x: 0.5, xanchor: "center"}, | |
| xaxis: {title:{text:"Δ phase 0 weight"}, range:[-0.25, 0.25], | |
| zeroline:true, zerolinecolor:"rgba(0,0,0,0.4)"}, | |
| xaxis2: {title:{text:"Δ phase 1 weight"}, range:[-0.25, 0.25], | |
| zeroline:true, zerolinecolor:"rgba(0,0,0,0.4)"}, | |
| yaxis: {autorange:"reversed", | |
| tickfont:{size: narrow ? 8 : 9}, automargin:true}, | |
| yaxis2: {autorange:"reversed", | |
| tickfont:{size: narrow ? 8 : 9}, automargin:true, | |
| matches: narrow ? undefined : "y"}, | |
| plot_bgcolor:"white", paper_bgcolor:"white", | |
| }; | |
| Plotly.react("chart-mix-detail", traces, layout, {responsive: true}); | |
| } | |
| function renderCorrelations() { | |
| // Three γ heatmaps: phase 0, phase 1, and (w0 + w1) summed across phases. | |
| // Side by side on wide screens, stacked on narrow. | |
| const order = DATA.domain_order; | |
| const xLabels = order.map(i => domainLabelChart(DATA.domain_names[i])); | |
| const yLabels = DATA.task_names.map(shortLabel); | |
| const reorder = mat => mat.map(row => order.map(i => row[i])); | |
| const z0 = reorder(DATA.corr0); | |
| const z1 = reorder(DATA.corr1); | |
| const zs = reorder(DATA.corr_sum); | |
| const narrow = window.innerWidth < 700; | |
| const baseTrace = { | |
| type: "heatmap", x: xLabels, y: yLabels, | |
| colorscale: "RdBu", reversescale: true, | |
| zmid: 0, zmin: -1, zmax: 1, | |
| }; | |
| const traces = [ | |
| {...baseTrace, z: z0, xaxis: "x", yaxis: "y", showscale: false, | |
| hovertemplate: "task: %{y}<br>domain: %{x}<br>γ = %{z:+.3f}<extra>phase 0</extra>"}, | |
| {...baseTrace, z: z1, xaxis: "x2", yaxis: "y2", showscale: false, | |
| hovertemplate: "task: %{y}<br>domain: %{x}<br>γ = %{z:+.3f}<extra>phase 1</extra>"}, | |
| {...baseTrace, z: zs, xaxis: "x3", yaxis: "y3", showscale: true, | |
| colorbar: {title: "γ", thickness: 12, len: 0.7}, | |
| hovertemplate: "task: %{y}<br>domain: %{x}<br>γ = %{z:+.3f}<extra>w0+w1</extra>"}, | |
| ]; | |
| const layout = { | |
| grid: {rows: narrow ? 3 : 1, columns: narrow ? 1 : 3, | |
| pattern: "independent"}, | |
| height: narrow ? 1800 : 700, | |
| margin: {l: narrow ? 140 : 200, r: 60, t: 60, b: 110}, | |
| title: {text: "Tristan's γⱼ — phase 0 / phase 1 / w0+w1 summed", | |
| font: {size: 14}, x: 0.5, xanchor: "center"}, | |
| xaxis: {title: {text: "phase 0 domain"}, tickfont: {size: 7}, | |
| tickangle: -60, automargin: true}, | |
| xaxis2: {title: {text: "phase 1 domain"}, tickfont: {size: 7}, | |
| tickangle: -60, automargin: true}, | |
| xaxis3: {title: {text: "w0 + w1 (summed) domain"}, tickfont: {size: 7}, | |
| tickangle: -60, automargin: true}, | |
| yaxis: {tickfont: {size: 9}, automargin: true, | |
| autorange: "reversed"}, | |
| yaxis2: {tickfont: {size: 9}, automargin: true, | |
| autorange: "reversed", matches: narrow ? undefined : "y"}, | |
| yaxis3: {tickfont: {size: 9}, automargin: true, | |
| autorange: "reversed", matches: narrow ? undefined : "y"}, | |
| plot_bgcolor: "white", paper_bgcolor: "white", | |
| }; | |
| Plotly.react("chart", traces, layout, {responsive: true}); | |
| } | |
| function renderPredictedTaskChart() { | |
| // Predicted per-task percentile from the DSP fits at the demanded mixture, | |
| // with ±1σ uncertainty bands from the fit's residual RMSE. | |
| const preds = predictAllWithUncertainty(MIX0, MIX1); | |
| const pcts = preds.map(p => p.pct); | |
| updateMixInfo(pcts); | |
| const order = [...pcts.keys()].sort((a, b) => pcts[b] - pcts[a]); | |
| const names = order.map(i => shortLabel(DATA.task_names[i])); | |
| const vals = order.map(i => pcts[i]); | |
| const errPlus = order.map(i => preds[i].pctHi - preds[i].pct); | |
| const errMinus = order.map(i => preds[i].pct - preds[i].pctLow); | |
| const colors = vals.map(v => | |
| v >= 50 ? "rgba(44,160,44,0.85)" : "rgba(214,39,40,0.85)"); | |
| const trace = { | |
| type: "bar", orientation: "h", x: vals, y: names, | |
| marker: {color: colors}, showlegend: false, | |
| error_x: { | |
| type: "data", symmetric: false, | |
| array: errPlus, arrayminus: errMinus, | |
| color: "rgba(0,0,0,0.45)", thickness: 1, width: 4, | |
| }, | |
| hovertemplate: | |
| "%{y}: %{x:.1f} pct (±1σ band %{customdata[0]:.1f}–%{customdata[1]:.1f})<extra></extra>", | |
| customdata: order.map(i => [preds[i].pctLow, preds[i].pctHi]), | |
| }; | |
| const narrow = window.innerWidth < 700; | |
| const layout = { | |
| height: narrow ? 900 : 700, | |
| margin: {l: narrow ? 140 : 220, r: 20, t: 60, b: 50}, | |
| title: {text: "Predicted task percentiles for current demanded mixture", | |
| font: {size: 14}, x: 0.5, xanchor: "center"}, | |
| xaxis: {title: {text: "Predicted swarm percentile"}, range: [0, 100], | |
| zeroline: false}, | |
| yaxis: {autorange: "reversed", tickfont: {size: narrow ? 8 : 9}, | |
| automargin: true}, | |
| plot_bgcolor: "white", paper_bgcolor: "white", | |
| shapes: [ | |
| {type: "line", xref: "x", yref: "paper", x0: 50, x1: 50, y0: 0, y1: 1, | |
| line: {color: "rgba(0,0,0,0.4)", width: 1, dash: "dot"}}, | |
| ], | |
| }; | |
| Plotly.react("chart", [trace], layout, {responsive: true}); | |
| } | |
| function renderMixChart(w0r, w1r) { | |
| const mode = document.getElementById("view-mode").value; | |
| const x0 = mode === "delta" | |
| ? w0r.map((w, i) => w - DATA.w0_prop[i]) : w0r; | |
| const x1 = mode === "delta" | |
| ? w1r.map((w, i) => w - DATA.w1_prop[i]) : w1r; | |
| // Stable order across runs (precomputed by natural size). | |
| const order = DATA.domain_order; | |
| const names = order.map(i => domainLabelChart(DATA.domain_names[i])); | |
| const x0o = order.map(i => x0[i]); | |
| const x1o = order.map(i => x1[i]); | |
| // Delta view uses signed colors; absolute uses one neutral blue. | |
| const signColor = arr => arr.map(x => x > 0 ? "rgba(44,160,44,0.85)" | |
| : "rgba(214,39,40,0.85)"); | |
| const c0 = mode === "delta" ? signColor(x0o) : "rgba(31,119,180,0.85)"; | |
| const c1 = mode === "delta" ? signColor(x1o) : "rgba(31,119,180,0.85)"; | |
| const xrange = mode === "delta" ? [-0.25, 0.25] : [0, DATA.wmax]; | |
| const xt0 = mode === "delta" ? "Δ phase 0 weight" : "phase 0 weight"; | |
| const xt1 = mode === "delta" ? "Δ phase 1 weight" : "phase 1 weight"; | |
| const title = mode === "delta" | |
| ? "Current demanded mixture — Δ from token-proportional" | |
| : "Current demanded mixture — absolute weights"; | |
| const hover = mode === "delta" | |
| ? "%{y}: %{x:+.3f}<extra></extra>" | |
| : "%{y}: %{x:.3f}<extra></extra>"; | |
| // Stack subplots vertically on narrow screens. | |
| const narrow = window.innerWidth < 700; | |
| const traces = [ | |
| {type:"bar", orientation:"h", x:x0o, y:names, xaxis:"x", yaxis:"y", | |
| marker:{color: c0}, showlegend:false, hovertemplate: hover}, | |
| {type:"bar", orientation:"h", x:x1o, y:names, xaxis:"x2", yaxis:"y2", | |
| marker:{color: c1}, showlegend:false, hovertemplate: hover}, | |
| ]; | |
| const layout = { | |
| grid: {rows: narrow ? 2 : 1, columns: narrow ? 1 : 2, pattern: "independent"}, | |
| height: narrow ? 1100 : 620, | |
| margin: {l: narrow ? 140 : 240, r: 20, t: 60, b: 50}, | |
| title: {text: title, font: {size: 14}, x: 0.5, xanchor: "center"}, | |
| xaxis: {title:{text: xt0}, range: xrange, | |
| zeroline:true, zerolinecolor:"rgba(0,0,0,0.4)"}, | |
| xaxis2: {title:{text: xt1}, range: xrange, | |
| zeroline:true, zerolinecolor:"rgba(0,0,0,0.4)"}, | |
| yaxis: {autorange:"reversed", | |
| tickfont:{size: narrow ? 8 : 9}, automargin:true}, | |
| yaxis2: {autorange:"reversed", | |
| tickfont:{size: narrow ? 8 : 9}, automargin:true, | |
| matches: narrow ? undefined : "y"}, | |
| plot_bgcolor:"white", paper_bgcolor:"white", | |
| }; | |
| Plotly.react("chart", traces, layout, {responsive: true}); | |
| } | |
| // Re-render when the user toggles the view dropdown. | |
| document.getElementById("view-mode").addEventListener("change", | |
| () => { updateChart(); updateURL(); }); | |
| function downloadCSV(rows, filename) { | |
| const csv = rows.map(r => r.map(c => | |
| (typeof c === "string" && /[",\n]/.test(c)) ? `"${c.replace(/"/g, '""')}"` : c | |
| ).join(",")).join("\n"); | |
| const url = URL.createObjectURL(new Blob([csv], {type: "text/csv"})); | |
| const a = document.createElement("a"); | |
| a.href = url; a.download = filename; | |
| document.body.appendChild(a); a.click(); a.remove(); | |
| setTimeout(() => URL.revokeObjectURL(url), 0); | |
| } | |
| // Download the demanded mixture + the DSP-predicted task results. | |
| document.getElementById("download-mix").addEventListener("click", () => { | |
| const rows = [["domain", "w0", "w1", "w0_prop", "w1_prop", | |
| "delta_w0", "delta_w1"]]; | |
| DATA.domain_names.forEach((d, j) => { | |
| const wp0 = DATA.w0_prop[j], wp1 = DATA.w1_prop[j]; | |
| rows.push([d, MIX0[j].toFixed(4), MIX1[j].toFixed(4), | |
| wp0, wp1, (MIX0[j]-wp0).toFixed(4), | |
| (MIX1[j]-wp1).toFixed(4)]); | |
| }); | |
| downloadCSV(rows, "demanded_mixture.csv"); | |
| }); | |
| // On the mix tab, reset edits the demanded mixture directly. On the task | |
| // tab, "proportional" maps to the swarm run nearest token-proportional and | |
| // "David's mix" is just swarm run #157. | |
| function resetMixTo(w0Vec, w1Vec) { | |
| for (let d = 0; d < MIX0.length; d++) { | |
| MIX0[d] = w0Vec[d]; | |
| MIX1[d] = w1Vec[d]; | |
| } | |
| syncMixSliders(0); | |
| syncMixSliders(1); | |
| syncTaskSlidersFromMix(); | |
| updateChart(); | |
| updateURL(); | |
| } | |
| document.getElementById("reset-mix").addEventListener("click", | |
| () => resetMixTo(DATA.w0_prop, DATA.w1_prop)); | |
| document.getElementById("reset-david").addEventListener("click", | |
| () => resetMixTo(DATA.w0[157], DATA.w1[157])); | |
| document.getElementById("reset-pareto").addEventListener("click", | |
| () => resetMixTo(DATA.w0_pareto, DATA.w1_pareto)); | |
| document.getElementById("download-results").addEventListener("click", () => { | |
| const rows = [["task", "predicted_percentile", | |
| "pct_1sigma_lower", "pct_1sigma_upper", "sigma_z"]]; | |
| const preds = predictAllWithUncertainty(MIX0, MIX1); | |
| DATA.task_names.forEach((t, j) => rows.push([ | |
| t, preds[j].pct.toFixed(2), | |
| preds[j].pctLow.toFixed(2), preds[j].pctHi.toFixed(2), | |
| DATA.dsp_params[j].sigma.toFixed(4), | |
| ])); | |
| downloadCSV(rows, "predicted_task_results.csv"); | |
| }); | |
| buildSliders(); | |
| buildMixSliders(); | |
| updateChart(); | |
| // If the URL has shareable state, apply it; otherwise show the default view. | |
| if (!loadFromURL()) updateURL(); | |
| // Re-render on resize (debounced) so the layout flips between | |
| // stacked and side-by-side when the viewport crosses ~700px. | |
| let _resizeT; | |
| window.addEventListener("resize", () => { | |
| clearTimeout(_resizeT); | |
| _resizeT = setTimeout(() => updateChart(), 120); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| out = HTML.replace("__DATA__", json.dumps(data)) | |
| os.makedirs("docs", exist_ok=True) | |
| with open("docs/index.html", "w") as f: | |
| f.write(out) | |
| print(f"wrote docs/index.html ({len(out)/1024:.1f} KB, " | |
| f"{len(data['R'])} runs × {len(data['task_names'])} tasks " | |
| f"× {len(data['domain_names'])} domains)") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment