1// âââ style.css backstop (see _bundle_head) âââ 2;(function(){var want = ['public/intro-functional/style.css', 'public/intro-structural/style.css', 'public/ooo-heatmap/style.css', 'public/ooo-abcd/style.css', 'public/ooo-patch/style.css', 'public/feature-examples/style.css', 'public/feature-coverage/style.css', 'public/comp-lines/style.css', 'public/comp-strata/style.css', 'public/comp-tail-energy/style.css', 'public/mlp-gain/style.css', 'public/lens-similarity/style.css', 'public/transcoder-arith/style.css', 'public/transcoder-translation/style.css', 'public/attn-panel/style.css', 'public/jlens-circuit-graph/style.css', 'public/attn-broadcast-reselect/style.css', 'public/ablation-bars/style.css', 'public/ablation-examples/style.css', 'public/ablation-strength/style.css', 'public/blackmail-clamp/style.css', 'public/lensapp-panel/style.css', 'public/reflection-training/style.css', 'public/dn-tracecond/style.css', 'public/dn-exclusion/style.css', 'public/refl-training-examples/style.css', 'public/modulation-readout/style.css', 'public/modulation-lines/style.css', 'public/modulation-prompts/style.css', 'public/latent-patching/style.css', 'public/multihop-swap-success/style.css', 'public/repeat-switch/style.css', 'public/verbal-report/style.css', 'public/verbal-introspection/style.css', 'public/top-down-summoning/style.css', 'public/flex-gen-systematic/style.css', 'public/selectivity-linecount/style.css', 'public/selectivity-language/style.css', 'public/post-training/style.css', 'public/lens-inline/style.css', 'public/methods-qualitative/style.css', 'public/jlens-audit/style.css', 'public/jlens-rm-bias/style.css', 'public/jlens-rm-bias-examples/style.css', 'public/pref-violation-lens/style.css', 'public/layer-diagram/style.css', 'public/line-panels/style.css', 'public/capacity-fve-occupancy/style.css', 'public/broadcast-ablation/style.css', 'public/lens-callout/style.css', 'public/misalign-lens/style.css', 'public/roleplay-lens/style.css', 'public/reward-hack-readout/style.css', 'public/reward-hack-quant/style.css', 'public/modulation-probe/style.css', 'public/probe-swap/style.css', 'public/verbal-report-decomposition-merged/style.css', 'public/selfreport/style.css', 'public/slice-inline/style.css', 'public/slice-stack/style.css', 'public/metacog-alarm/style.css', 'public/ignition/style.css', 'public/eval-awareness-probe/style.css', 'public/dual-task-simple/style.css', 'public/capacity-final-band/style.css', 'public/flex-generalization-example/style.css', 'public/flex-generalization-systematic/style.css', 'public/capacity-band-vs-single/style.css', 'public/app-mt-examples/style.css', 'public/oracle-lens/style.css', 'public/app-mt-bytok/style.css', 'public/app-mt-paper/style.css'] 3 var have = new Set([...document.querySelectorAll('link[rel=stylesheet]')] 4 .map(l => (l.getAttribute('href')||'').replace(/\?.*/,''))) 5 want.forEach(function(h){ if (have.has(h)) return 6 var l = document.createElement('link') 7 l.rel = 'stylesheet'; l.href = h 8 document.head.appendChild(l) }) 9})() 10// âââ public/util.js âââ 11window.util = (function(){ 12 13 // âââââ url params âââââ 14 // URL params become fetch bases and hrefs; keep them on this page's origin. 15 // Rejects scheme injection (javascript:, data:, https://other-host) and 16 // protocol-relative //host. Relative paths (incl. ../) and same-origin 17 // /absolute paths pass through unchanged. 18 function safeRelPath(p, fallback){ 19 return p && !/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(p) ? p : fallback 20 } 21 22 var params = { 23 get: key => { 24 var v = new URLSearchParams(location.search).get(key) 25 return v && decodeURIComponent(v) 26 }, 27 set: (key, value) => { 28 var url = new URL(location) 29 if (value == null) url.searchParams.delete(key) 30 else url.searchParams.set(key, encodeURIComponent(value)) 31 history.replaceState(null, '', url) 32 }, 33 getAll: () => { 34 var rv = {} 35 for (var [k, v] of new URLSearchParams(location.search)) rv[k] = decodeURIComponent(v) 36 return rv 37 }, 38 } 39 40 // âââââ render fan-out âââââ 41 function initRenderAll(labels){ 42 var rv = {}
43 labels.forEach(label => { 44 rv[label] = ev => Object.values(rv[label].fns).forEach(d => d(ev)) 45 rv[label].fns = [] 46 }) 47 return rv 48 } 49 50 function attachRenderAllHistory(renderAll, visState, skipKeys=['hoverId', 'hoverIdx']){ 51 Object.keys(renderAll).forEach(key => { 52 renderAll[key].fns.push(() => { 53 if (skipKeys.includes(key)) return 54 var url = new URL(location) 55 if (visState[key] == url.searchParams.get(key)) return 56 url.searchParams.set(key, visState[key]) 57 history.pushState({...visState}, '', url) 58 }) 59 }) 60 d3.select(window).on('popstate.updateState', ev => { 61 if (!ev.state) return 62 Object.keys(renderAll).forEach(key => { 63 if (skipKeys.includes(key) || visState[key] == ev.state[key]) return 64 visState[key] = ev.state[key] 65 renderAll[key]() 66 }) 67 }) 68 } 69 70 // âââââ data loading âââââ 71 // window-scoped cache survives hot-reload re-runs of init() 72 window.__datacache = window.__datacache || {} 73 74 async function loadParquet(url){ 75 var buffer = await fetch(url, {cache: 'default'}).then(r => r.arrayBuffer()) 76 var rows 77 await hyparquet.parquetRead({file: buffer, rowFormat: 'object', onComplete: d => rows = d}) 78 rows.forEach(r => { for (var k in r) if (typeof r[k] === 'bigint') r[k] = Number(r[k]) }) 79 return rows 80 } 81 82 function cachedParquet(url){ 83 if (!__datacache[url]) __datacache[url] = loadParquet(url) 84 return __datacache[url] 85 } 86 87 async function loadNpy(url){ 88 return npyjs.parse(await fetch(url, {cache: 'default'}).then(r => r.arrayBuffer())) 89 } 90 91 async function getFile(path){ 92 if (!__datacache[path]) __datacache[path] = (async () => { 93 var res = await fetch(path, {cache: 'no-cache'}) 94 if (!res.ok) throw Error(res.status + ' ' + path) 95 var type = path.split('?')[0].replaceAll('..', '').split('.').at(-1) 96 if (type == 'csv') return d3.csvParse(await res.text()) 97 if (type == 'npy') return npyjs.parse(await res.arrayBuffer()) 98 if (type == 'json') return res.json() 99 if (type == 'jsonl') return (await res.text()).split(/\r?\n/).filter(d => d).map(JSON.parse) 100 if (type == 'parquet') return loadParquet(path) 101 return res.text() 102 })() 103 return __datacache[path] 104 } 105 106 // âââââ chart styling âââââ 107 function addAxisLabel(c, xText, yText, title='', xOffset=0, yOffset=0, titleOffset=0){ 108 c.svg.select('.x').append('g').translate([c.width/2, xOffset + 27]) 109 .append('text.axis-label').text(xText).at({textAnchor: 'middle'}) 110 c.svg.select('.y').append('g').translate([yOffset - 30, c.height/2]) 111 .append('text.axis-label').text(yText).at({textAnchor: 'middle', transform: 'rotate(-90)'}) 112 if (title) c.svg.append('g.axis').translate([c.width/2, titleOffset - 10]) 113 .append('text.axis-label.axis-title').text(title).at({textAnchor: 'middle'}) 114 } 115 116 function ggPlot(c){ 117 c.svg.append('rect.bg-rect').at({width: c.width, height: c.height, fill: c.isBlack ? '#000' : '#EAECED'}).lower() 118 c.svg.selectAll('.domain').remove() 119 ggPlotUpdate(c) 120 } 121 122 function ggPlotUpdate(c){ 123 // inline style, not attr â several per-figure sheets set `.axis path { stroke: var(--tick-line) }` 124 // and CSS beats SVG presentation attrs, which turned the gridlines #ddd. 125 var grid = c.isBlack ? '#444' : '#fff' 126 c.svg.selectAll('.tick line').remove() 127 c.svg.selectAll('.x text').at({y: 4}) 128 c.svg.selectAll('.x .tick').selectAppend('path').at({d: 'M 0 0 V -' + c.height, strokeWidth: 1}).st({stroke: grid}) 129 c.svg.selectAll('.y text').at({x: -3}) 130 c.svg.selectAll('.y .tick').selectAppend('path').at({d: 'M 0 0 H ' + c.width, strokeWidth: 1}).st({stroke: grid}) 131 } 132 133 // workspace-layer band with consistent z-order: sits on top of bg-rect but 134 // BELOW the white gridlines that ggPlot draws inside .x/.y .tick paths. 135 // Bracket + label sit above the data in the same kraft tone as the band so 136 // they read as part of the band, not as a chart annotation. 137 function addWsBand(c, lo, hi, opts={}){ 138 var bg = c.svg.select('.bg-rect').node() 139 var x0 = c.x(lo), x1 = c.x(hi), tk = 5 140 var r = c.svg.insert('rect', bg ? () => bg.nextSibling : ':first-child') 141 .at({class: 'ws-band', x: x0, width: x1 - x0, y: 0, height: c.height}) 142 if (opts.label !== false){ 143 var g = c.svg.append('g.ws-annot') 144 g.append('path.ws-bracket').at({d: `M${x0},${tk} V0 H${x1} V${tk}`})
145 g.append('text.ws-label').text(opts.label || 'Workspace layers') 146 .at({x: (x0 + x1)/2, y: 7, textAnchor: 'middle'}) 147 } 148 return r 149 } 150 151 // Canonical layer-axis ticks: pub indices that display as 0/25/50/75/100. 152 // Filtered to [0, nLayers-1] so figures whose data starts late or stops 153 // short of L24 just drop the out-of-range ticks. 154 function layerTicks(nLayers, nTicks){ 155 var canon = [0, 6, 12, 18, 24] 156 return canon.filter(t => t < nLayers) 157 } 158 159 // Display mapping for published layer indices: 0..24 â 0..100. Data and scale 160 // domains stay 0..24; this only formats the visible number. 161 var DISPLAY_LAYERS = [0,4,8,12,17,21,25,29,33,38,42,46,50,54,58,62,67,71,75,79,83,88,92,96,100] 162 function layerLabel(i){ var d = DISPLAY_LAYERS[i]; return d == null ? i : d } 163 164 // âââââ colors âââââ 165 // Visual vocab. Source of truth for figure colors/spacing â `syncVars()` 166 // writes these to CSS custom properties at load so shared-styles.css and 167 // figure CSS read the same values. Don't hardcode hex in figure JS or CSS; 168 // use util.gray/accent/series or var(--gray-*) etc. 169 // 170 // gray ramp = neutral, distill-derived (transformer-circuits.pub body). 171 // series/accents = Paul Tol vibrant (CVD-safe). 172 var gray = { 173 100: '#fafafa', // soft bg / card bg 174 200: '#eee', // rules, hairline borders 175 300: '#ddd', // grid lines, tick lines 176 400: '#bbb', // faint text, disabled 177 500: '#888', // light text, secondary labels 178 600: '#555', // tick labels 179 700: '#333', // body text 180 900: '#111', // emphasis 181 } 182 // Category10-ish, ordered for hue separation: blue, green, brown, purple, 183 // orange, red. Orange is in the series AND is the activation/accent hue â 184 // figures that cycle series and also use accent.main will show it twice. 185 var series = ['#1f77b4', '#2ca02c', '#8c564b', '#9467bd', '#ee7733', '#d62728'] 186 // Named aliases into `series` so figure code can read `tol.blue`/`tol.teal` 187 // and get the well-spaced palette. Names are approximate (tealâgreen, 188 // magentaâpurple, cyanâbrown). 189 var tol = { 190 blue: series[0], 191 teal: series[1], 192 cyan: series[2], 193 magenta: series[3], 194 orange: series[4], 195 red: series[5], 196 } 197 var accent = { 198 main: tol.orange, 199 secondary: tol.blue, 200 pro: tol.magenta, 201 danger: tol.red, 202 } 203 // Brand palette â for non-data accents: callout boxes, brand moments, soft 204 // fills. Not for series. 205 // manilla/kraft are the J-space marker colors (intro-functional thought 206 // bubble, intro-structural sliver, .ws-band shading, layer-diagram, â¦); 207 // values are lifted from the J-lens readout box in the J-lens schematic 208 // figure so every "this is the workspace" fill in the paper matches. 209 var brand = { 210 clay: '#d98058', 211 bookCloth: '#cc7d5c', 212 kraft: '#dd9e57', 213 manilla: '#fdf4e3', 214 lilypad: '#7eba8f', 215 } 216 var pos = tol.blue, neg = tol.orange 217 // Same model â same color, every figure that compares across models. 218 var modelColors = { haiku: '#f7b380', sonnet: tol.orange, opus: '#a8460e' } 219 // Ablation tier â orange shade (shared by ablation-bars/-strength/-examples). 220 var ablationColors = { light: '#f8c08a', medium: tol.orange, heavy: '#b34d10', random: gray[400] } 221 // Same lens type â same color, every figure that compares lens variants. 222 // jacobian stays blue (paper's namesake); logit/tuned get purple/green for 223 // separation â the old blue/cyan/teal triple was indistinguishable. 224 var lensColors = { jacobian: series[0], logit: series[3], tuned: series[1] } 225 var space = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 } 226 // Solid tint instead of opacity â equivalent visual but bars stay opaque. 227 // tint(color, 0.5) = halfway to white. 228 function tint(c, t){ return d3.interpolateRgb(c, '#fff')(t) } 229 // J-space decomposition conditions â shared by probe-swap (Fig 15) and 230 // verbal-report-decomposition-merged (Fig 7) so a reader learns the 231 // mapping once. Clamped variants are a 0.5 tint of their parent. 232 var jspaceColors = { 233 jlens: tol.blue, clean: gray[300], random: gray[300],
234 aligned: tol.orange, probe_jpart2: tol.orange, 235 residual: gray[500], probe_ortho2: gray[500], 236 full: tol.teal, 237 probe_jpart2_oclamp: tint(tol.orange, .5), 238 probe_ortho2_jclamp: tint(gray[500], .5), 239 residual_jclamp: tint(gray[500], .5), 240 } 241 // Inline style for shading a token by activation strength t â [0,1]. 242 // t=0 â white, t=1 â tinted color at ~60%. Use for any "how much does this 243 // token activate" shading so figures share one ramp. 244 function activationBg(color, t){ return `background:${tint(color, 1 - t * 0.6)}` } 245 246 // NYT-style direct line label: colored text with a halo so it reads over the 247 // plot. x/y are pixel coords (caller converts via c.x/c.y); dx/dy nudge. 248 // halo defaults to the ggPlot bg since every line chart in the paper uses it. 249 // Pass leader:[lx,ly] (px) to draw a thin gray connector from label to line. 250 function directLabel(svg, {x, y, text, color, dx=0, dy=0, anchor='start', halo='#EAECED', leader, key}){ 251 var g = svg.append('g.direct-label').translate([x, y]) 252 if (leader) g.append('path').at({ 253 d: `M${dx + (anchor == 'end' ? -2 : anchor == 'middle' ? 0 : 2)},${dy - 3} L${leader[0]-x},${leader[1]-y}`, 254 stroke: gray[400], strokeWidth: 1, fill: 'none', 255 }) 256 var t = g.append('text.dlabel').text(text).at({x: dx, y: dy, textAnchor: anchor, fill: color}) 257 .st({paintOrder: 'stroke', stroke: halo, strokeWidth: 1, strokeLinejoin: 'round', 258 fontSize: 'var(--fs-label)', fontWeight: 600}) 259 if (key) t.at({'data-series': key}) 260 return g 261 } 262 263 // Cross-panel line+label hover: any element with data-series=<k> highlights 264 // every [data-series=<k>] inside figSel on mouseover. Figures tag their line 265 // paths with .line-series + data-series, pass key to directLabel, then call 266 // this once on the figure root. Only binds when there are many series â for a 267 // handful of labelled lines the highlight is just visual noise. 268 function bindSeriesHover(figSel, {minSeries=9}={}){ 269 if (params.get('tune') === '1') return // .raise() detaches d3.drag mid-gesture 270 var keys = new Set(); figSel.selectAll('[data-series]').each(function(){ keys.add(this.dataset.series) }) 271 if (keys.size < minSeries) return 272 figSel.on('mouseover.series', e => { 273 var k = e.target.closest('[data-series]')?.dataset.series 274 var all = figSel.selectAll('[data-series]').classed('on', false) 275 if (k) all.filter(function(){ return this.dataset.series === k }).classed('on', true).raise() 276 }).on('mouseleave.series', () => figSel.selectAll('[data-series]').classed('on', false)) 277 } 278 279 // Expanded hit-area for small jitter circles: mousemove on svgSel picks the 280 // nearest dataSel circle within `radius` px (Delaunay if loaded, else linear 281 // scan). Picked el gets .hov; onHover(d, el) fires while one is picked, 282 // onLeave() when none. Coords read from cx/cy â assumes circles share 283 // svgSel's coordinate space. 284 function nearestHover(svgSel, dataSel, {radius=40, onHover, onLeave}={}){ 285 var nodes = dataSel.nodes(), data = dataSel.data() 286 var pts = nodes.map(n => [+n.getAttribute('cx'), +n.getAttribute('cy')]) 287 var del = d3.Delaunay && pts.length ? d3.Delaunay.from(pts) : null 288 var find = del ? (x, y) => del.find(x, y) : (x, y) => { 289 var bi = -1, bd = Infinity 290 for (var i = 0; i < pts.length; i++){ 291 var dd = (pts[i][0]-x)**2 + (pts[i][1]-y)**2 292 if (dd < bd){ bd = dd; bi = i } 293 } 294 return bi 295 } 296 var cur = -1 297 svgSel.on('mousemove.nearestHover', e => { 298 var [mx, my] = d3.pointer(e, svgSel.node()) 299 var i = pts.length ? find(mx, my) : -1 300 if (i >= 0 && Math.hypot(pts[i][0]-mx, pts[i][1]-my) > radius) i = -1 301 if (i !== cur){ 302 if (cur >= 0) nodes[cur].classList.remove('hov') 303 if (i >= 0) nodes[i].classList.add('hov'); else onLeave?.() 304 cur = i 305 } 306 if (i >= 0) onHover?.(data[i], nodes[i], e) 307 }).on('mouseleave.nearestHover', () => { 308 if (cur < 0) return 309 nodes[cur].classList.remove('hov'); onLeave?.(); cur = -1 310 }) 311 } 312 313 // Stack a set of labels along a vertical edge without overlap: pass anchors 314 // as [{text,color,y,...}] sorted by data-y; returns the same with y nudged so 315 // adjacent labels are ⥠minGap apart, preserving order. 316 function spreadLabels(specs, minGap=13){ 317 var s = specs.slice().sort((a,b) => a.y - b.y) 318 for (var i = 1; i < s.length; i++) if (s[i].y - s[i-1].y < minGap) s[i].y = s[i-1].y + minGap 319 for (var i = s.length - 2; i >= 0; i--) if (s[i+1].y - s[i].y < minGap) s[i].y = s[i+1].y - minGap 320 return specs 321 } 322 323 // Error-bar whisker: vertical line from loâhi (pixel coords) with top
323/bottom 324 // caps. capW=0 for no caps; stroke defaults to gray[700]. 325 function barWhisker(svg, {x, lo, hi, capW=4, stroke=gray[700]}){ 326 var s = {stroke, strokeWidth: 1.2} 327 svg.append('line').at({x1: x, x2: x, y1: lo, y2: hi, ...s}) 328 if (!capW) return 329 svg.append('line').at({x1: x - capW, x2: x + capW, y1: lo, y2: lo, ...s}) 330 svg.append('line').at({x1: x - capW, x2: x + capW, y1: hi, y2: hi, ...s}) 331 } 332 333 function syncVars(){ 334 var root = document.documentElement.style 335 Object.entries(gray).forEach(([k,v]) => root.setProperty('--gray-'+k, v)) 336 Object.entries(tol).forEach(([k,v]) => root.setProperty('--tol-'+k, v)) 337 Object.entries(accent).forEach(([k,v]) => root.setProperty('--accent-'+k, v)) 338 Object.entries(brand).forEach(([k,v]) => root.setProperty('--brand-'+k, v)) 339 series.forEach((c,i) => root.setProperty('--series-'+i, c)) 340 Object.entries(modelColors).forEach(([k,v]) => root.setProperty('--model-'+k, v)) 341 Object.entries(lensColors).forEach(([k,v]) => root.setProperty('--lens-'+k, v)) 342 Object.entries(space).forEach(([k,v]) => root.setProperty('--space-'+k, v+'px')) 343 } 344 syncVars() 345 346 function rankColorCapped(cap){ 347 return d3.scaleSequential(d3.interpolateViridis).domain([Math.log10(cap), 0]) 348 } 349 var rankColor = rankColorCapped(10000) 350 var lineColors = series // legacy alias; new code should use util.series 351 352 function drawColorbar(sel, {label='rank', w=300, h=10, cap=10000}={}){ 353 var n = 100 354 var rc = rankColorCapped(cap) 355 var ticks = cap <= 1000 ? [1, 10, 100, 1000] : [1, 10, 100, 1000, 10000] 356 var svg = sel.append('svg.colorbar').at({width: w + 60, height: h + 20}).st({overflow: 'visible'}) 357 var g = svg.append('g').translate([30, 0]) 358 g.appendMany('rect', d3.range(n)).at({x: i => i*w/n, width: w/n+1, height: h, fill: i => rc(i/n * Math.log10(cap))}) 359 var x = d3.scaleLog().domain([1, cap]).range([0, w]) 360 g.appendMany('text', ticks).text(d => d >= cap ? (cap >= 1000 ? cap/1000+'k+' : cap+'+') : d >= 1000 ? d/1000+'k' : d).at({x: d => x(d), y: h+12, textAnchor: 'middle'}) 361 g.append('text.axis-label').text(label).at({x: -4, y: h-1, textAnchor: 'end'}) 362 return rc 363 } 364 365 // âââââ drag-tune (?tune=1) âââââ 366 // On a figure's standalone page, ?tune=1 makes registered elements draggable. 367 // Positions land in ?pos= and a fixed config panel â copy either back to 368 // hardcode. items: [{sel, key, get:()=>({x,y}), set:({x,y})=>void}]. 369 // ns groups keys in the URL/panel so multiple dragTune calls coexist. 370 var _dt 371 function _dtInit(){ 372 if (_dt) return _dt 373 var pos = {} 374 ;(params.get('pos') || '').split(';
374').filter(Boolean).forEach(e => { 375 var m = e.match(/^([^.]+)\.(.+):(-?\d+),(-?\d+)$/); if (!m) return 376 ;(pos[m[1]] ||= {})[m[2]] = {x: +m[3], y: +m[4]} 377 }) 378 var panel = d3.select('body').selectAppend('div.dragtune-panel') 379 .st({position: 'fixed', bottom: 0, left: 0, right: 0, background: '#111', color: '#eee', 380 font: '11px/1.4 monospace', padding: '8px 12px', zIndex: 9999, maxHeight: '30vh', overflow: 'auto'}) 381 panel.selectAppend('button').text('copy URL').st({float: 'right', cursor: 'pointer'}) 382 .on('click', () => navigator.clipboard.writeText(location.href)) 383 var pre = panel.selectAppend('pre').st({margin: 0, whiteSpace: 'pre-wrap'}) 384 function sync(){ 385 var enc = Object.entries(pos).flatMap(([n, ks]) => 386 Object.entries(ks).map(([k, p]) => `${n}.${k}:${p.x},${p.y}`)).join(';') 387 var q = new URLSearchParams(location.search); q.delete('pos') 388 history.replaceState(null, '', '?' + q + (enc ? '&pos=' + enc : '')) 389 pre.text(Object.entries(pos).map(([n, ks]) => 390 `// ${n}\n` + Object.entries(ks).map(([k, p]) => ` ${k}: {x: ${p.x}, y: ${p.y}},`).join('\n') 391 ).join('\n')) 392 } 393 return _dt = {pos, sync} 394 } 395 function dragTune(items, ns='fig'){ 396 if (params.get('tune') !== '1') return 397 var {pos, sync} = _dtInit() 398 items.forEach(it => { 399 var sel = it.sel.call ? it.sel : d3.select(it.sel) 400 var saved = pos[ns]?.[it.key], p = saved ? {...saved} : it.get() 401 if (saved) it.set(p) 402 ;(pos[ns] ||= {})[it.key] = {x: Math.round(p.x), y: Math.round(p.y)} 403 sel.st({cursor: 'move'}).call(d3.drag() 404 .on('drag', e => { p.x += e.dx; p.y += e.dy; it.set(p) }) 405 .on('end', () => { pos[ns][it.key] = {x: Math.round(p.x), y: Math.round(p.y)}; sync() })) 406 }) 407 sync() 408 } 409 // Auto-register every text.dlabel under sel â key from data-series or text. 410 function dragTuneLabels(sel, ns='dlabel'){ 411 if (params.get('tune') !== '1') return 412 var items = [] 413 ;(sel || d3.select('body')).selectAll('text.dlabel').each(function(){ 414 var el = d3.select(this), key = (this.dataset.series || el.text()).replace(/\W+/g, '_').slice(0, 24) 415 items.push({sel: el, key, 416 get: () => ({x: +el.attr('x') || 0, y: +el.attr('y') || 0}), 417 set: p => el.at({x: p.x, y: p.y})}) 418 }) 419 dragTune(items, ns) 420 } 421 422 // âââââ misc âââââ 423 function throttle(fn, delay){ 424 var last = 0 425 return (...a) => { if (Date.now() - last < delay) return; last = Date.now(); fn(...a) } 426 } 427 function debounce(fn, delay){ 428 var t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), delay) } 429 } 430 function throttleDebounce(fn, delay){ 431 var last = 0, t 432 return function(...a){ 433 clearTimeout(t) 434 var rem = delay - (Date.now() - last) 435 if (rem <= 0){ last = Date.now(); fn.apply(this, a) } 436 else t = setTimeout(() => { last = Date.now(); fn.apply(this, a) }, rem) 437 } 438 } 439 function sleep(ms){ return new Promise(r => setTimeout(r, ms)) } 440 function cache(fn){ 441 var c = {} 442 return function(...a){ var k = JSON.stringify(a); if (!(k in c)) c[k] = fn.apply(this, a); return c[k] } 443 } 444 // per-panel caption (.psub). First call per figure gets the "Figure N: " prefix. 445 function panelCaption(sel, opts, text){ 446 var n = opts._pc = (opts._pc || 0) + 1 447 var div = sel.append('div.psub') 448 if (n == 1 && opts.figNum) div.append('span.fig-num').text(`Figure ${opts.figNum}: `) 449 div.append('span').html(text) 450 return div 451 } 452 // Wrap occurrences of each word in the figure's <figcaption> with a colored 453 // span so caption text visually keys to series colors in the chart. 454 // replacements: {word: '#hex' | 'var(--x)' | 'css-class' | {bg: '#hex', color?: '#hex'}}. 455 // {bg:} sets a background highlight instead of foreground â use when the 456 // series color is gray-ish and would vanish against the caption text color. 457 // Case-insensitive whole-word; warns on miss. 458 function colorCaption(figSel, replacements){ 459 var cap = (figSel.node?.() || figSel)?.closest('figure')?.querySelector('figcaption') 460 if (!cap) return
461 Object.entries(replacements).forEach(([word, val]) => { 462 var re = new RegExp('(?<![\\w-])' + word + '(?![\\w-])', 'gi'), found = false 463 var isObj = val && typeof val == 'object' 464 var firstOnly = isObj && val.firstOnly 465 var isColor = !isObj && /^(#|rgb|hsl|var\()/i.test(val) 466 var style = s => isObj ? (s.style.cssText = `background:${val.bg};border-radius:2px;padding:0 2px` + (val.color ? `;color:${val.color}` : '')) 467 : isColor ? (s.style.color = val) : (s.className = val) 468 var reFull = new RegExp('^' + word + '$', 'i') 469 ;(function walk(n){ 470 for (var c of [...n.childNodes]){ 471 if (firstOnly && found) return 472 if (c.nodeType == 1) { 473 // if a wrapper span's entire text is the word, replace the wrapper 474 // itself so the new class doesn't nest inside the chip styling. 475 if (c.tagName == 'SPAN' && !c.children.length && reFull.test(c.textContent.trim())){ 476 found = true 477 var s = document.createElement('span'); s.textContent = c.textContent 478 style(s); c.replaceWith(s); continue 479 } 480 walk(c); continue 481 } 482 if (c.nodeType != 3) continue 483 var txt = c.data, m, last = 0, out = [] 484 re.lastIndex = 0 485 while ((m = re.exec(txt))){ 486 found = true 487 if (m.index > last) out.push(txt.slice(last, m.index)) 488 var s = document.createElement('span'); s.textContent = m[0] 489 style(s); out.push(s); last = re.lastIndex 490 if (firstOnly) break 491 } 492 if (last) { if (last < txt.length) out.push(txt.slice(last)); c.replaceWith(...out) } 493 } 494 })(cap) 495 if (!found) console.warn(`[colorCaption] no match for "${word}" in figcaption`) 496 }) 497 } 498 function resolveFigRefs(){ 499 document.querySelectorAll('a.fig-ref').forEach(a => { 500 var ref = a.dataset.ref ?? a.hash.slice(5) 501 var fignum = document.getElementById('fig-' + ref)?.dataset.fignum 502 if (fignum != null) { a.textContent = fignum; return } 503 var sec = window.__secNum?.[ref] 504 if (sec != null) { a.textContent = '§' + sec; a.setAttribute('href', '#' + ref); return } 505 a.textContent = '??' 506 }) 507 } 508 509 function ppToken(s, keepNl){ 510 if (s == null) return '' 511 var out = String(s) 512 .replaceAll(/\u25b2[\u00b7\u237d\u205f]*(\p{L})/gu, (_, c) => c.toUpperCase()) 513 .replaceAll(/\u2191[\u00b7\u237d\u205f]*(\p{L})/gu, (_, c) => c.toUpperCase()) 514 .replaceAll(/\u21ea[\u00b7\u237d\u205f]*(\p{L}+)/gu, (_, w) => w.toUpperCase()) 515 .replaceAll(/[\u25b2\u2191\u21ea]/gu, '') 516 .replaceAll(/[\u237d\u2028\u205f\u00b7]/gu, ' ') 517 return keepNl ? out : out.replaceAll('\n', '\u23ce') 518 } 519 520 // ranks above the cap are not informative 521 function ppRank(r, cap=999){ return r >= cap ? cap + '+' : r } 522 523 // ppToken for cell/chip display: spaces visible, never empty 524 function ppCell(s){ 525 var t = ppToken(s) 526 return t.trim() ? t : t.replaceAll(' ', '\u2423').replaceAll('\u23ce', '\u23ce') || '\u2205' 527 } 528 529 // âââââ lens-slice shared (lens-slice + lens-inline) âââââ 530 // {slug}.json: {slug, title, ctxTokens:[str], n_layers, default_ctx} 531 // {slug}.parquet: rows of {layer, ctx, rank, str, prob} \u2014 long format, rank 0 = argmax 532 async function loadLensSlice(base, slug){ 533 var [d, rows] = await Promise.all([getFile(base + slug + '.json'), getFile(base + slug + '.parquet')]) 534 d.layers = d.layers || d3.range(d.n_layers) 535 d.nCtx = d.ctxTokens.length 536 var by = d3.group(rows, r => r.layer, r => r.ctx) 537 d.cell = (ctx, layer) => by.get(layer)?.get(ctx) || [] 538 d.argmax = (ctx, layer) => d.cell(ctx, layer)[0]?.str 539 d.probOf = (ctx, layer, str) => (d.cell(ctx, layer).find(e => e.str === str)?.prob) || 0 540 var rankBy = {} 541 d.rankOf = (ctx, layer, str) => { 542 var m = rankBy[str] 543 if (m) return (m.get(layer)?.get(ctx)?.[0]?.rank ?? -1) + 1 || null 544 var e = d.cell(ctx, layer).find(e => e.str === str) 545 return e ? e.rank + 1 : null 546 } 547 // Per-token rank trajectory; no-op when the slug ships without rankFiles 548 // (lens-inline) so callers don't have to check. 549 d.loadRanks = async (str) => { 550 if (rankBy[str] || d.rankFiles?.[str] == null) return 551 var rr = await getFile(base + slug + '_ranks/' + d.rankFiles[str] + '.parquet') 552 rankBy[str] = d3.group(rr, r => r.layer, r => r.ctx) 553 } 554 return d 555 } 556 557 function pinnedStyle(color){ 558 if (!color) return '' 559 return `background:${color}20;text-decoration:underline 2px ${color};text-underline-offset:2px` 560 } 561 562 // clickable prompt-token paragraph; returns the span selection 563 function appendPromptPara(sel, ctxTokens, {keepNl=true}={}){ 564 return sel.appendMany('span.ptok', d3.range(ctxTokens.length)) 565 .each(function(i){ 566 var s = ppToken(ctxTokens[i], keepNl) 567 d3.select(this).text(s).classed('empty', !s) 568 }) 569 } 570 571 // one row of lens top-k chips, fixed-k so hover never reflows 572 function appendLensTopk(sel, items, k=10){ 573 return sel.appendMany('span.lenstk', d3.range(k)) 574 .each(function(i){ 575 var it = items[i]
576 d3.select(this).text(it ? ppCell(it.str) : '\u00a0') 577 .at({title: it ? ppCell(it.str) : null}).datum(it).classed('empty', !it) 578 }) 579 } 580 581 // Scale a figure mount down when its natural width overflows the <figure> 582 // container. Works for SVG and div-based charts alike. Stacking via 583 // .panel-row/.figgrid is the first defense; this catches fixed-width 584 // d3.conventions output and any intermediate-width overflow (768\u20131100px). 585 function fitMount(mount){ 586 var fig = mount.closest('figure') 587 if (!fig) return 588 // measure natural size with any prior scale/fit removed â fit-scaled's 589 // explicit width changes what container queries inside the mount resolve to. 590 // width:fit-content during the measure makes --mount-w track the rendered 591 // content (so figcaption max-width matches it), not the column the block 592 // mount happens to fill. Skip container-typed mounts: intrinsic sizing 593 // collapses them to 0. 594 var prev = mount.style.transform, prevW = mount.style.width 595 var prevH = mount.style.height, prevM = mount.style.marginLeft 596 var prevC = mount.classList.contains('fit-scaled') 597 mount.style.transform = 'none' 598 mount.classList.remove('fit-scaled') 599 // height/margin-left are fit outputs too â measuring with a stale clamp 600 // from the previous run makes consecutive measurements state-dependent. 601 mount.style.height = '' 602 mount.style.marginLeft = '' 603 if (getComputedStyle(mount).containerType == 'normal') mount.style.width = 'fit-content' 604 var natural = mount.scrollWidth 605 var naturalH = mount.scrollHeight 606 mount.style.width = prevW 607 // available space is the figure's content box, not clientWidth â the 608 // figure-render gallery card has 14px horizontal padding, and a scale 609 // computed against clientWidth leaves ~28px of overflow that the card's 610 // overflow-x:auto fallback turns into a scrollbar. (Paper figures have no 611 // padding, so this is a no-op there.) 612 var cs = getComputedStyle(fig) 613 var avail = fig.clientWidth 614 - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0) 615 var restore = () => { 616 mount.style.transform = prev 617 mount.style.height = prevH 618 mount.style.marginLeft = prevM 619 if (prevC) mount.classList.add('fit-scaled') 620 } 621 if (!natural || !avail) { restore(); return } 622 // Idempotence guard: this runs under a ResizeObserver watching the very 623 // <figure> the writes below resize, so re-applying on â¤1px measurement 624 // noise (integer scrollWidth vs fractional clientWidth) feeds back into 625 // an endless remeasure loop at fit-boundary widths. If the inputs moved 626 // â¤1px and the verdict is unchanged, keep the prior state untouched. 627 var f0 = mount.__fit, fits = natural <= avail 628 if (f0 && fits == f0.fits && Math.abs(natural - f0.natural) <= 1 629 && Math.abs(avail - f0.avail) <= 1 && Math.abs(naturalH - f0.naturalH) <= 1){ 630 restore(); return 631 } 632 mount.__fit = {natural, naturalH, avail, fits} 633 fig.style.setProperty('--mount-w', natural + 'px') 634 if (natural <= avail){ 635 mount.style.transform = '' 636 mount.style.height = '' 637 mount.style.marginLeft = '' 638 fig.style.overflowX = '' 639 mount.classList.remove('fit-scaled') 640 return 641 } 642 var min = +mount.dataset.minScale || 0 643 var s = Math.max(avail / natural, min) 644 mount.classList.add('fit-scaled') 645 mount.style.transform = `scale(${s})` 646 // collapse the dead vertical space the transform leaves behind 647 mount.style.height = (naturalH * s) + 'px' 648 // center the visual box â transform-origin is top-left and several figure 649 // mounts set max-width:100% (overriding .fit-scaled's max-width:none), so 650 // the layout box can resolve narrower than `natural`; scale then leaves a 651 // gap on the right. Read the actual box width and offset so the scaled 652 // visual sits mid-figure regardless of which width won. 653 var ow = mount.offsetWidth 654 mount.style.marginLeft = Math.max(0, (avail - ow * s) / 2) + 'px' 655 // floored scale still overflows: let the <figure> h-scroll instead of clipping 656 fig.style.overflowX = natural * s > avail + 1 ? 'auto' : '' 657 } 658 659 // Shared sliver schematic for intro-structural + layer-diagram: body box, 660 // residual column, manilla J-space band (same width as the sliver), and the 661 // right-side "J-space" label with a double-arrow spanning the full column. 662 function drawJSliver(svg, {bx0, bx1, H, capH=26, pad=14, jFrac=[0.20, 0.74], arrowId}){ 663 var jEdge = d3.color(brand.kraft).darker(0.5).formatHex() 664 var cx = (bx0 + bx1)/2, bw = bx1 - bx0, gap = 8
665 var by0 = capH + gap, by1 = H - capH - gap 666 var rTop = by0 + pad, rBot = by1 - pad 667 var jTop = rTop + (rBot - rTop)*jFrac[0], jBot = rTop + (rBot - rTop)*jFrac[1] 668 var rr = (x, y, w, h, r, a) => svg.append('rect').at({x, y, width: w, height: h, rx: r, ry: r, ...a}) 669 svg.append('defs').append('marker').at({id: arrowId, viewBox: '0 0 10 10', refX: 8, refY: 5, 670 markerWidth: 6, markerHeight: 6, orient: 'auto-start-reverse'}) 671 .append('path').at({d: 'M1,1 L8,5 L1,9', fill: 'none', stroke: gray[400], 672 strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round'}) 673 rr(bx0, 0, bw, capH, 4, {fill: gray[100], stroke: gray[200], strokeWidth: 1}) 674 rr(bx0, H - capH, bw, capH, 4, {fill: gray[100], stroke: gray[200], strokeWidth: 1}) 675 svg.append('text.io').text('Output').translate([cx, capH/2]).at({textAnchor: 'middle', dy: '.35em'}) 676 svg.append('text.io').text('Input').translate([cx, H - capH/2]).at({textAnchor: 'middle', dy: '.35em'}) 677 rr(bx0, by0, bw, by1 - by0, 12, {fill: gray[100], stroke: gray[300], strokeWidth: 1}) 678 rr(cx - 7, rTop, 14, rBot - rTop, 7, {fill: gray[300], opacity: .65}) 679 rr(cx - 7, jTop, 14, jBot - jTop, 4, {fill: brand.manilla}) 680 rr(cx - 7, jTop, 14, jBot - jTop, 4, {fill: 'none', stroke: jEdge, strokeWidth: 1.1, strokeDasharray: '4 3'}) 681 var jx = cx + 18 682 svg.append('line').at({x1: jx, y1: rTop + 2, x2: jx, y2: rBot - 2, 683 stroke: gray[400], strokeWidth: 1, markerStart: `url(#${arrowId})`, markerEnd: `url(#${arrowId})`}) 684 svg.append('text.jspace').text('J-space') 685 .at({transform: `translate(${jx + 11},${(rTop + rBot)/2}) rotate(-90)`, textAnchor: 'middle'}) 686 return {cx, bx0, bx1, by0, by1, rTop, rBot, jTop, jBot, sliverL: cx - 7} 687 } 688 689 var __fitRO 690 function fitFigures(){ 691 var mounts = () => document.querySelectorAll('figure > div[class]') 692 mounts().forEach(fitMount) 693 if (!__fitRO) __fitRO = new ResizeObserver( 694 throttleDebounce(() => mounts().forEach(fitMount), 100)) 695 mounts().forEach(m => { var f = m.closest('figure'); if (f) __fitRO.observe(f) }) 696 } 697 698 return { 699 params, safeRelPath, initRenderAll, attachRenderAllHistory, 700 loadParquet, cachedParquet, loadNpy, getFile, 701 addAxisLabel, ggPlot, ggPlotUpdate, addWsBand, layerTicks, layerLabel, DISPLAY_LAYERS, drawJSliver, 702 gray, tol, accent, brand, series, pos, neg, modelColors, lensColors, ablationColors, jspaceColors, space, tint, activationBg, barWhisker, 703 directLabel, spreadLabels, bindSeriesHover, nearestHover, dragTune, dragTuneLabels, 704 rankColor, lineColors, drawColorbar, 705 throttle, debounce, throttleDebounce, sleep, cache, resolveFigRefs, panelCaption, colorCaption, 706 ppToken, ppCell, ppRank, loadLensSlice, appendPromptPara, appendLensTopk, pinnedStyle, 707 rankColorCapped, fitMount, fitFigures, 708 } 709})() 710 711window.init?.() 712 713// âââ public/intro-functional/init-intro-functional.js âââ 714// Figure 1 â five functional properties of a global workspace. 715// Pure illustration: hardcoded content, no data fetch. 716window.initIntroFunctional = function(opts){ 717 var sel = d3.select(opts?.sel || '.intro-functional').html('') 718 719 var arr = `<span class='if-arr'>â</span>` 720 var cards = [ 721 { 722 letter: 'A', title: 'Verbal report', 723 q: 'âWhat are you thinking about?â', 724 op: 'swap', 725 thought: {kind: 'swap', from: 'banana', to: 'elephant'}, 726 out: {kind: 'lines', from: 'â¦about a banana.', to: 'â¦about an elephant.'}, 727 }, 728 { 729 letter: 'B', title: 'Directed modulation', 730 q: 'Compute 3² â 2 while writing<br>âThe old painting hungâ¦â', 731 op: null, 732 thought: {kind: 'chain', steps: ['math', 'calc', 'nine', 'seven', 'equals']}, 733 out: {kind: 'plain', text: 'The old painting hung crookedly on the wall.'}, 734 }, 735 { 736 letter: 'C', title: 'Internal reasoning', 737 q: 'âWhat color is the planet fourth from the sun?â', 738 op: 'swap', 739 thought: {kind: 'swap', from: 'Mars', to: 'Earth'}, 740 out: {kind: 'lines', from: 'red', to: 'blue'}, 741 }, 742 { 743 letter: 'D', title: 'Flexible generalization', 744 q: 'âWhat is the capital of France?â', 745 op: 'swap', 746 thought: {kind: 'swap', from: 'France', to: 'China'}, 747 out: {kind: 'table', rows: [ 748 ['capital', 'Paris', 'Beijing'], 749 ['language', 'French', 'Chinese'], 750 ['continent', 'Europe', 'Asia'], 751 ['currency', 'Euro', 'Yuan'], 752 ]}, 753 }, 754 { 755 letter: 'E', title: 'Selectivity', 756 q: '<em>(a battery of tasks)</em>', 757 op: 'ablate', 758 thought: {kind: 'ablate', text: 'task plan step<br>reason answer'}, 759 out: {kind: 'check', title: 'Doable without workspace', rows: [ 760 ['parse inputs', true], 761 ['recall facts', true], 762 ['speak fluently', true], 763 ['reason internally', false], 764 ['make complex inferences', false], 765 ]}, 766 }, 767 ] 768
769 var rows = [sel.append('div.if-row.if-row-3'), sel.append('div.if-row.if-row-2')] 770 cards.forEach((d, i) => { 771 var cell = rows[i < 3 ? 0 : 1].append('div.if-cell') 772 cell.append('div.if-hdr.fig-title').text(d.title) 773 var card = cell.append('div.card.light.if-card') 774 card.append('div.if-q.prompt-block').html(d.q) 775 776 var tw = card.append('div.if-mid').append('div.if-thoughtwrap') 777 if (d.op) tw.append('div.if-op.mlabel').text(d.op) 778 var th = tw.append('div.if-thought').classed('if-thought-ablated', d.thought.kind == 'ablate') 779 if (d.thought.kind == 'swap'){ 780 th.append('div.if-from').text(d.thought.from) 781 th.append('div.if-to').text(d.thought.to) 782 } else if (d.thought.kind == 'chain'){ 783 th.classed('if-thought-chain', true) 784 // meandering chain: tokens at irregular y, curvy connectors with arrowheads 785 var svg = th.append('svg.if-chain-svg') 786 svg.append('defs').html( 787 `<marker id='if-arrhead' viewBox='0 0 6 6' refX='5' refY='3' ` + 788 `markerWidth='5' markerHeight='5' orient='auto'>` + 789 `<path d='M0,0 L6,3 L0,6' fill='none' stroke='var(--text-faint)' stroke-width='1'/></marker>`) 790 var xs = [14, 53, 94, 138, 186], ys = [30, 8, 38, 11, 33] 791 var bend = [-8, 7, -9, 6] 792 // half-word-width + 2px gap so arrow tail/head clear the text on both ends 793 var hw = d.thought.steps.map(s => s.length * 3.3 + 2) 794 var arrD = j => { 795 var x0 = xs[j] + hw[j], y0 = ys[j] - 4, x1 = xs[j+1] - hw[j+1], y1 = ys[j+1] - 4 796 var mx = (x0 + x1)/2, my = (y0 + y1)/2 + bend[j] 797 return `M${x0},${y0} Q${mx},${my} ${x1},${y1}` 798 } 799 var toks = svg.appendMany('text.if-chain-tok', d.thought.steps) 800 .at({x: (_, j) => xs[j], y: (_, j) => ys[j], textAnchor: 'middle'}).text(String) 801 var arrs = svg.appendMany('path.if-chain-arr', d3.range(d.thought.steps.length - 1)) 802 .at({markerEnd: 'url(#if-arrhead)', d: arrD}) 803 util.dragTune(toks.nodes().map((n, j) => ({ 804 sel: n, key: d.thought.steps[j], 805 get: () => ({x: xs[j], y: ys[j]}), 806 set: p => { xs[j] = p.x; ys[j] = p.y; d3.select(n).at({x: p.x, y: p.y}); arrs.at({d: arrD}) }, 807 })), 'chain') 808 } else if (d.thought.kind == 'ablate'){ 809 th.append('div.if-from').html(d.thought.text) 810 } 811 // Clawd left of the speech bubble; thought-bubble tail circles drop from 812 // .if-thoughtwrap's bottom-left down to his head. 813 var bot = card.append('div.if-bottom') 814 bot.append('div.if-robot').html( 815 `<svg width="32" height="25" viewBox="0 0 66 52" xmlns="http://www.w3.org/2000/svg">` + 816 `<path fill="var(--brand-clay)" d="M60 13H66V26H60V52H54V39H48V52H42V39H24V52H18V39H12V52H6V26H0V13H6V0H60V13Z"/>` + 817 `<rect x="12" y="13" width="6" height="6" fill="var(--gray-900)"/>` + 818 `<rect x="48" y="13" width="6" height="6" fill="var(--gray-900)"/>` + 819 `</svg>`) 820 821 var out = bot.append('div.if-out') 822 var role = `<span class='prompt-role'>Assistant:</span> ` 823 if (d.out.kind == 'lines'){ 824 out.html(`${role}<span class='if-out-from'>${d.out.from}</span> ` + 825 `<span class='if-out-to'>${d.out.to}</span>`) 826 } else if (d.out.kind == 'plain'){ 827 out.html(`${role}${d.out.text}`) 828 } else if (d.out.kind == 'table'){ 829 out.append('div.if-ttable').appendMany('div.if-trow', d.out.rows).html(r => 830 `<span class='if-tk'>${r[0]}</span>${arr}` + 831 `<span class='if-out-from'>${r[1]}</span>` + 832 `<span class='if-out-to'>${r[2]}</span>`) 833 } else if (d.out.kind == 'check'){ 834 if (d.out.title) out.append('div.if-ctitle').text(d.out.title) 835 var cols = out.append('div.if-ccols') 836 ;[true, false].forEach(ok => cols.append('div.if-ccol') 837 .appendMany('div.if-crow', d.out.rows.filter(r => r[1] == ok)).html(r => 838 `<span class='if-cmark ${r[1] ? 'ok' : 'nope'}'>${r[1] ? 'â' : 'â'}</span>` + 839 `<span class='if-ctxt'>${r[0]}</span>`)) 840 } 841 }) 842} 843 844window.init?.() 845 846// âââ public/intro-structural/init-intro-structural.js âââ 847window.initIntroStructural = function(opts={}){ 848 var sel = d3.select('.intro-structural').html('') 849 if (!sel.size()) return 850 851 // Visual vocab: paper shared styles. Gray ramp for structure; brand-manilla 852 // marks the J-space (same color as `.ws-band` on the line charts) so the 853 // reader recognizes "this is the workspace" everywhere it appears. 854 var gray = util.gray 855 var manilla = util.brand.manilla 856 var jBand = manilla 857 var jEdge = d3.color(util.brand.kraft).darker(0.5).formatHex() 858 var jDot = d3.color(util.brand.kraft).darker(0.45).formatHex() 859 var soft = gray[100] // var(--bg-soft) 860 var softEdge = gray[300] 861 862 function rrect(g, x, y, w, h, r, attrs){ 863 return g.append('rect').at({x, y, width: w, height: h, rx: r, ry: r, ...attrs}) 864 } 865 function brace(g, x, y0, y1, bw){ 866 // `{` opening right â curl ends on the box edge, point sticks left toward text. 867 var ym = (y0 + y1) / 2 868 g.append('path.brace').at({fill: 'none', stroke: gray[400], d: 869 `M ${x+bw},${y0} Q ${x},${y0} ${x},${y0+bw} V ${ym-bw}` + 870 ` Q ${x},${ym} ${x-bw},${ym} Q ${x},${ym} ${x},${ym+bw}` + 871 ` V ${y1-bw} Q ${x},${y1} ${x+bw},${y1}` 872 }) 873 } 874 875 var diag = sel.append('div.is-diagram') 876 var panels = sel.append('div.is-panels') 877 878 // âââââ left: vertical layer diagram âââââ 879 drawLayerDiagram(diag) 880 function drawLayerDiagram(host){ 881 var W = 360, H = 580 882 var svg = host.append('svg').at({width: W, height: H, viewBox: `0 0 ${W} ${H}`}) 883 884 var g = util.drawJSliver(svg, {bx0: 124, bx1: 252, H, capH: 28, pad: 16, arrowId: 'is-arrow'}) 885 var {cx, bx0, bx1, rTop, rBot, jTop, jBot, sliverL} = g 886 887 // left annotations â motor/sensory outside, workspace inside w/ dashed leader 888 var bwBrace = 8, tip = 3
889 var bxBrace = bx0 - bwBrace - tip, txtX = bxBrace - bwBrace - 8 890 function annoText(ym, lines){ 891 var ty0 = ym - (lines.length - 1) * 8 892 var txt = svg.append('text').at({textAnchor: 'end', x: txtX}) 893 lines.forEach((t, i) => txt.append(i === 0 ? 'tspan.anno-key' : 'tspan.anno-sub') 894 .text(t).at({x: txtX, y: ty0 + i*16, dy: '.35em'})) 895 } 896 ;[{y0: rTop, y1: jTop, key: 'Motor', sub: ['determining', 'the output']}, 897 {y0: jBot, y1: rBot, key: 'Sensory', sub: ['input parsing,', 'preprocessing']}] 898 .forEach(a => { 899 brace(svg, bxBrace, a.y0 + tip, a.y1 - tip, bwBrace) 900 annoText((a.y0 + a.y1)/2, [a.key, ...a.sub]) 901 }) 902 var wym = (jTop + jBot) / 2, ibx = sliverL - bwBrace - tip 903 brace(svg, ibx, jTop + tip, jBot - tip, bwBrace) 904 svg.append('line').at({x1: ibx - bwBrace, y1: wym, x2: txtX + 4, y2: wym, 905 stroke: gray[400], strokeWidth: 1, strokeDasharray: '3 3'}) 906 annoText(wym, ['Workspace', 'lives here']) 907 908 // right: layer arcs (residual skip-connections, bottom â top) 909 var nArc = 8, ax = bx1 + 4, span = (rBot - rTop) / nArc, bulge = span * 1.45, arcGap = 9 910 d3.range(nArc).forEach(i => { 911 var yb = rTop + (i + 1) * span - arcGap/2, yt = rTop + i * span + arcGap/2 912 svg.append('path.layer-arc').at({ 913 d: `M ${ax},${yb} C ${ax + bulge},${yb} ${ax + bulge},${yt} ${ax + 1},${yt + 1}`, 914 fill: 'none', stroke: gray[400], markerEnd: 'url(#is-arrow)', 915 }) 916 }) 917 svg.append('text.layers-lbl').text('Layers') 918 .at({x: ax + bulge*0.78 + 2, y: (rTop + rBot)/2, dy: '.35em'}) 919 } 920 921 // âââââ right: panels âââââ 922 function panel(title, desc){ 923 var c = panels.append('div.card') 924 c.append('div.fig-title').text(title) 925 c.append('div.desc').text(desc) 926 return c 927 } 928 929 panel('Intermediate processing stage', 930 'J-space carries workspace-like content only at intermediate depths') 931 932 var pB = panel('Limited capacity', 933 'Few concepts active at once; minority of activation variance; most features lie outside it') 934 drawCapacityIllo(pB.append('div.illo')) 935 936 var pC = panel('Broadcast format', 937 'J-lens vectors compose with many upstream output weights and downstream input weights') 938 drawBroadcastIllo(pC.append('div.illo')) 939 940 // âââââ B: J-space as a small dashed sub-box inside one shared container âââââ 941 function drawCapacityIllo(host){ 942 var W = 380, H = 102, bH = 72 943 var svg = host.append('svg').at({viewBox: `0 0 ${W} ${H}`}).st({maxWidth: W + 'px'}) 944 var pad = 8, sw = 72 945 946 function scatter(x0, y0, w, h, nx, ny, jit, seed){ 947 var rng = d3.randomLcg(seed), cw = w/nx, ch = h/ny, pts = [] 948 d3.range(nx*ny).forEach(k => { 949 var i = k % nx, j = (k - i)/nx 950 pts.push([x0 + (i + .5 + (rng() - .5)*jit)*cw, 951 y0 + (j + .5 + (rng() - .5)*jit)*ch]) 952 }) 953 return pts 954 } 955 956 // shared outer container 957 rrect(svg, 0, 0, W, bH, 4, {fill: soft, stroke: softEdge, strokeWidth: 1}) 958 svg.appendMany('circle', scatter(pad + sw + 10, 8, W - pad - sw - 20, bH - 16, 17, 4, 1.1, 53)) 959 .at({cx: d => d[0], cy: d => d[1], r: 2.6, fill: gray[400]}) 960 // J-space dashed sub-box inset on the left 961 rrect(svg, pad, pad, sw, bH - 2*pad, 4, {fill: jBand}) 962 svg.appendMany('circle', scatter(pad + 7, pad + 7, sw - 14, bH - 2*pad - 14, 5, 3, 1.1, 11)) 963 .at({cx: d => d[0], cy: d => d[1], r: 3, fill: jDot}) 964 rrect(svg, pad, pad, sw, bH - 2*pad, 4, {fill: 'none', stroke: jEdge, strokeWidth: 1.1, strokeDasharray: '4 3'}) 965 966 svg.append('text.illo-key').text('J-space').at({x: pad + sw/2, y: bH + 20, textAnchor: 'middle'}) 967 svg.append('text').text('Other representational features') 968 .at({x: pad + sw + (W - pad - sw)/2, y: bH + 20, textAnchor: 'middle'}) 969 } 970 971 // âââââ C: fan-in / fan-out broadcast hub âââââ 972 function drawBroadcastIllo(host){ 973 var W = 420, H = 158 974 var svg = host.append('svg').at({viewBox: `0 0 ${W} ${H}`}).st({maxWidth: W + 'px'}) 975 svg.append('defs').html( 976 `<marker id='is-arrow-o' viewBox='0 0 10 10' refX='7' refY='5' markerWidth='5.5' markerHeight='5.5' orient='auto'>` + 977 `<path d='M1,1 L8,5 L1,9' fill='none' stroke='${gray[500]}' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></marker>` 978 ) 979 980 var n = 5, bw = 76, bh = 20, vgap = 6 981 var spanH = n * bh + (n - 1) * vgap, y0 = 4 982 var hx = W/2 - 32, hw = 64, hh = 36, hy = y0 + spanH/2 - hh/2 983 var rx = W - bw 984
985 d3.range(n).forEach(i => { 986 var by = y0 + i * (bh + vgap), cy = by + bh/2 987 var ht = hy + 5 + (hh - 10) * i/(n - 1) 988 rrect(svg, 0, by, bw, bh, 4, {fill: soft, stroke: softEdge, strokeWidth: 1}) 989 rrect(svg, rx, by, bw, bh, 4, {fill: soft, stroke: softEdge, strokeWidth: 1}) 990 svg.append('line').at({x1: bw + 2, y1: cy, x2: hx - 4, y2: ht, 991 stroke: gray[500], strokeWidth: 1, markerEnd: 'url(#is-arrow-o)'}) 992 svg.append('line').at({x1: hx + hw + 2, y1: ht, x2: rx - 6, y2: cy, 993 stroke: gray[500], strokeWidth: 1, markerEnd: 'url(#is-arrow-o)'}) 994 }) 995 rrect(svg, hx, hy, hw, hh, 4, {fill: jBand}) 996 rrect(svg, hx, hy, hw, hh, 4, {fill: 'none', stroke: jEdge, strokeWidth: 1.1, strokeDasharray: '4 3'}) 997 998 svg.append('text.illo-key').text('Upstream').at({x: bw/2, y: y0 + spanH + 16, textAnchor: 'middle'}) 999 svg.append('text.illo-key').text('circuits').at({x: bw/2, y: y0 + spanH + 30, textAnchor: 'middle'}) 1000 svg.append('text.illo-key').text('Downstream').at({x: rx + bw/2, y: y0 + spanH + 16, textAnchor: 'middle'}) 1001 svg.append('text.illo-key').text('circuits').at({x: rx + bw/2, y: y0 + spanH + 30, textAnchor: 'middle'}) 1002 } 1003} 1004 1005window.init?.() 1006 1007// âââ public/ooo-heatmap/init-ooo-heatmap.js âââ 1008// Canvas heatmap: number_tokens (x-axis) Ã layers (y-axis), colored by log10(rank). 1009// Wide-format rows: each row is one (x, layer) with token strings as columns. 1010window.initOooHeatmap = function({visState, renderAll, sel, cfg, byX}){ 1011 var tokens = cfg.number_tokens 1012 var layers = cfg.layers || d3.range(cfg.n_layers) 1013 var nLayers = layers.length 1014 var rowOf = new Map(layers.map((l, i) => [l, i])) 1015 var cellW = tokens.length > 220 ? 2 : 3 1016 var w = tokens.length * cellW, h = 140 1017 var margin = {top: 14, right: 10, bottom: 24, left: 42} 1018 1019 var c = d3.conventions({sel, width: w, height: h, margin, layers: 'scs'}) 1020 var ctx = c.layers[1] 1021 var svg = c.layers[2] 1022 1023 var cellH = c.height / nLayers 1024 1025 c.x.domain([-0.5, tokens.length - 0.5]) 1026 c.y.domain([-0.5, nLayers - 0.5]) 1027 c.xAxis.ticks(8).tickFormat(i => `"${util.ppToken(tokens[Math.round(i)])}"`).tickSizeOuter(0).tickSizeInner(4) 1028 // y-axis ticks at evenly-spaced row indices so they line up with the cell rects. 1029 var tickRows = d3.ticks(0, nLayers - 1, 4).map(Math.round) 1030 c.yAxis.tickValues(tickRows).tickFormat(i => util.layerLabel(layers[i])).tickSizeOuter(0).tickSizeInner(4) 1031 c.drawAxis() 1032 util.addAxisLabel(c, 'Token â', 'Layer (reindexed) â') 1033 1034 var nLabels = cfg.line_labels.length 1035 var markG = svg.append('g.value-marks') 1036 1037 function drawMarks(){ 1038 var xi = cfg.xs.indexOf(visState.x) 1039 var vals = cfg.line_vals_per_x?.[xi] || [] 1040 var marks = cfg.line_labels.map((lbl, j) => { 1041 var ti = tokens.indexOf(String(vals[j])) 1042 return ti < 0 ? null : {ti, color: (cfg.colors || util.lineColors)[(nLabels-1-j) % (cfg.colors || util.lineColors).length]} 1043 }).filter(Boolean) 1044 var s = markG.selectAll('path').data(marks) 1045 s.enter().append('path').merge(s) 1046 .at({d: d => `M${d.ti*cellW + cellW/2},-2 l-4,-7 l8,0 z`, fill: d => d.color, stroke: '#fff', strokeWidth: 0.5}) 1047 s.exit().remove() 1048 } 1049 renderAll.x.fns.push(drawMarks) 1050 1051 var hoverRect = svg.append('rect').at({width: c.width, height: c.height, fill: 'transparent'}) 1052 var dot = svg.append('circle').at({r: 3, fill: 'none', stroke: '#f0f', strokeWidth: 1, opacity: 0, pointerEvents: 'none'}) 1053 var ttSel = d3.select('.tooltip') 1054 1055 var currentRows = [] // [n_layers] rows for current x, indexed by layer 1056 1057 hoverRect 1058 .on('mousemove', function(e){ 1059 var [mx, my] = d3.pointer(e) 1060 var ti = Math.floor(mx / cellW) 1061 var ri = nLayers - 1 - Math.floor(my / cellH) 1062 var inBounds = ti >= 0 && ti < tokens.length && ri >= 0 && ri < nLayers 1063 if (!inBounds) return hide() 1064 dot.at({cx: ti * cellW + cellW/2, cy: (nLayers - 1 - ri) * cellH + cellH/2, opacity: 1}) 1065 var r = currentRows[ri]?.[tokens[ti]] 1066 ttSel.html(`at L${util.layerLabel(layers[ri])} "${util.ppToken(tokens[ti])}" is rank ${r != null ? util.ppRank(r, cfg.rank_cap || 9999) : '?'}`) 1067 .classed('tooltip-hidden', 0) 1068 .st({left: e.clientX + 12, top: e.clientY + 12}) 1069 }) 1070 .on('mouseleave', hide) 1071 function hide(){ dot.at({opacity: 0}); ttSel.classed('tooltip-hidden', 1) } 1072 1073 function draw(){ 1074 var rows = byX.get(visState.x) || [] 1075 currentRows = new Array(nLayers) 1076 ctx.clearRect(0, 0, c.width, c.height) 1077 for (var r of rows){ 1078 var ri = rowOf.get(r.layer) 1079 if (ri == null) continue 1080 currentRows[ri] = r 1081 var y = (nLayers - 1 - ri) * cellH 1082 for (var ti = 0; ti < tokens.length; ti++){ 1083 var rank = r[tokens[ti]] 1084 if (!rank) continue 1085 ctx.fillStyle = util.rankColor(Math.log10(rank)) 1086 ctx.fillRect(ti * cellW, y, cellW + 0.5, cellH + 0.5) 1087 } 1088 } 1089 } 1090 renderAll.x.fns.push(draw) 1091} 1092 1093window.init?.() 1094 1095// âââ public/ooo-heatmap/init-ooo-lines.js âââ 1096// Line chart: layer â rank (log, reversed). One line per line_label (wide-format column). 1097window.initOooLines = function({visState, renderAll, sel, cfg, byX}){ 1098 var nLayers = cfg.n_layers 1099 var labels = [...cfg.line_labels].reverse() 1100 var w = 320, h = 140 1101 var margin = {top: 14, right: 10, bottom: 24, left: 44} 1102 1103 var c = d3.conventions({sel, width: w, height: h, margin}) 1104 c.x.domain([0, nLayers - 1]).rangeRound([0, c.width]) 1105 c.y = d3.scaleLog().domain([10000, 1]).rangeRound([c.height, 0]) 1106 c.xAxis.tickValues(util.layerTicks(nLayers)).tickFormat(util.layerLabel) 1107 c.yAxis = d3.axisLeft(c.y).ticks(4, '~s').tickFormat(d => d >= 9999 ? '10k+' : d3.format('~s')(d)) 1108 c.drawAxis() 1109 util.ggPlot(c) 1110 util.addAxisLabel(c, 'Layer (reindexed) â', cfg.yLabel || 'J-Lens rank â') 1111 1112 var colors = cfg.colors || util.lineColors 1113 var line = d3.line().x(d => c.x(d.layer)).y(d => c.y(Math.max(1, d.rank))) 1114 1115 var pathSel = c.svg.appendMany('path.line', labels) 1116 .at({fill: 'none', strokeWidth: 1.5, stroke: (d, i) => colors[i % colors.length]}) 1117 1118 var dotSel = c.svg.appendMany('g.dots', labels) 1119 .at({fill: (d, i) => colors[i % colors.length]}) 1120 1121 function draw(){ 1122 var rows = (byX.get(visState.x) || []).slice().sort((a, b) => a.layer - b.layer) 1123 var xi = cfg.xs.indexOf(visState.x) 1124 var valStrs = cfg.line_vals_per_x?.[xi] || [] // what value each label resolves to at this x 1125
1126 labels.forEach((lbl, i) => { 1127 var col = cfg.colFor ? cfg.colFor(lbl, xi) : lbl 1128 var pts = rows.map(r => ({layer: r.layer, rank: r[col]})).filter(d => d.rank > 0) 1129 pathSel.filter(d => d === lbl).at({d: pts.length ? line(pts) : ''}) 1130 1131 var g = dotSel.filter(d => d === lbl) 1132 var dots = g.selectAll('circle').data(pts) 1133 dots.enter().append('circle').at({r: 2}).merge(dots) 1134 .at({cx: d => c.x(d.layer), cy: d => c.y(Math.max(1, d.rank))}) 1135 .each(function(d){ d.label = lbl; d.value = valStrs[cfg.line_labels.indexOf(lbl)] }) 1136 .call(d3.attachTooltip, null, [ 1137 d => `${d.label} = ${d.value}`, 1138 d => `at L${util.layerLabel(d.layer)} is rank ${util.ppRank(d.rank, cfg.rank_cap || 9999)}`, 1139 ]) 1140 dots.exit().remove() 1141 }) 1142 } 1143 renderAll.x.fns.push(draw) 1144} 1145 1146window.init?.() 1147 1148// âââ public/ooo-abcd/init-ooo-abcd.js âââ 1149window.abcdState = window.abcdState || {active: 'A', val: 4} 1150window.abcdRedrawFns = window.abcdRedrawFns || [] 1151 1152// shared meta + parquet loader (both figures use the same data) 1153async function abcdShared(base, layerFilter){ 1154 var fkey = layerFilter ? [...layerFilter].sort((a, b) => a - b).join(',') : '' 1155 if (window.__abcd?.base == base && window.__abcd?.fkey == fkey) return window.__abcd 1156 var meta = await util.getFile(base + 'meta.json?' + Date.now()) 1157 if (!layerFilter && meta.layers) layerFilter = new Set(meta.layers) 1158 if (layerFilter){ 1159 var ls = [...layerFilter].sort((a, b) => a - b) 1160 // leave cfg.n_layers at the full extent so the line chart's x-axis stays 1161 // 0..24; only the heatmap reads cfg.layers (compact rows). 1162 meta = {...meta, layers: ls, 1163 cfgs: Object.fromEntries(Object.entries(meta.cfgs || {}).map(([k, v]) => [k, {...v, layers: ls}]))} 1164 } 1165 var pq = {} 1166 async function loadVar(v, probe){ 1167 var k = v + '/' + probe 1168 if (!pq[k]) pq[k] = util.loadParquet(base + `${v}/${meta.slug}/${probe}.parquet`) 1169 .then(rows => d3.group(layerFilter ? rows.filter(d => layerFilter.has(d.layer)) : rows, d => d.x)) 1170 return pq[k] 1171 } 1172 return window.__abcd = {base, fkey, meta, loadVar} 1173} 1174 1175function abcdHeader(sel, titleText, meta, colors){ 1176 var hdr = sel.append('div.abcd-title') 1177 hdr.append('span.fig-title').text(titleText) 1178 if (!meta.vars?.length) return 1179 var expr = hdr.append('span.expr') 1180 var scrubs = {}, stacks = {}, ops = [] 1181 ;['calc: ( ', 'A', ' + ', 'B', ' ) * ', 'C', ' + ', 'D', ' ='].forEach(part => { 1182 if (!meta.vars.includes(part)) return ops.push(expr.append('span.op').text(part).node()) 1183 var v = part, [lo, hi] = meta.ranges?.[v] || [1, 10], dft = meta.defaults[v] 1184 var cur = v == abcdState.active ? abcdState.val : dft 1185 var stack = expr.append('span.scrub-stack') 1186 stacks[v] = stack.node() 1187 var num = stack.append('span.scrub').at({tabindex: 0}).datum({v, lo, hi, dft}).text(cur) 1188 var slider = stack.append('input').at({type: 'range', min: lo, max: hi, value: cur, step: 1, tabindex: -1}) 1189 .on('input', function(){ setVar(v, +this.value) }) 1190 scrubs[v] = {num, slider, lo, hi, dft} 1191 }) 1192 1193 // operand brackets under the sliders: each intermediate's span, in its line color 1194 function drawBrackets(){ 1195 if (!colors || !stacks.A) return 1196 var er = expr.node().getBoundingClientRect() 1197 if (!er.width) return 1198 var bk = expr.selectAll('.op-brackets').data([0]) 1199 bk = bk.enter().append('div.op-brackets').merge(bk) 1200 // ops[0]='calc: ( ', ops[2]=' ) * '. Innermost bracket spans the parens; 1201 // each outer bracket steps 3px further left so the three nest visibly. 1202 var ch = ops[0].getBoundingClientRect().width / ops[0].textContent.length 1203 var x0base = ops[0].getBoundingClientRect().right - er.left - 2 * ch // back over '( ' 1204 var x1paren = ops[2] ? ops[2].getBoundingClientRect().left - er.left + 2 * ch : null // through ' )' 1205 var spans = [['B', x1paren], ['C'], ['D']] 1206 .filter(([b]) => stacks[b]) 1207 .map(([b, x1e], i) => { 1208 var x0 = x0base - i * 3 1209 var x1 = x1e ?? (stacks[b].getBoundingClientRect().right - er.left) 1210 return {left: x0, width: x1 - x0, bottom: i * 4, color: colors[i % colors.length]} 1211 }) 1212 var s = bk.selectAll('.op-bracket').data(spans)
1213 s.enter().append('div.op-bracket').merge(s) 1214 .st({left: d => d.left, width: d => d.width, bottom: d => d.bottom, borderColor: d => d.color}) 1215 s.exit().remove() 1216 } 1217 abcdRedrawFns.push(drawBrackets) 1218 d3.select(window).on('resize.abcdBrackets', drawBrackets) 1219 requestAnimationFrame(drawBrackets) 1220 var redrawAll = util.throttleDebounce(() => abcdRedrawFns.forEach(f => f()), 60) 1221 function sync(){ 1222 meta.vars.forEach(u => { 1223 var x = u == abcdState.active ? abcdState.val : scrubs[u].dft 1224 scrubs[u].num.text(x); scrubs[u].slider.node().value = x 1225 }) 1226 } 1227 function setVar(v, val){ 1228 var s = scrubs[v] 1229 val = Math.max(s.lo, Math.min(s.hi, val)) 1230 if (abcdState.active == v && abcdState.val == val) return 1231 abcdState.active = v; abcdState.val = val 1232 redrawAll() 1233 } 1234 abcdRedrawFns.push(sync) 1235 var previewing = null 1236 function revertPreview(){ 1237 if (!previewing) return 1238 var p = previewing; previewing = null 1239 expr.selectAll('.scrub').classed('dragging', false) 1240 setVar(p.active, p.val) 1241 } 1242 d3.select(window).on('keyup.abcdPreview', ev => { if (ev.key == 'Shift') revertPreview() }) 1243 expr.selectAll('.scrub') 1244 .on('pointerdown', function(ev, d){ 1245 ev.preventDefault() 1246 previewing = null 1247 var startX = ev.clientX, startVal = +this.textContent 1248 this.setPointerCapture(ev.pointerId) 1249 d3.select(this).classed('dragging', true) 1250 .on('pointermove', ev2 => setVar(d.v, startVal + Math.round((ev2.clientX - startX)/(ev2.shiftKey ? 8 : 2)))) 1251 .on('pointerup pointercancel', function(ev2){ 1252 this.releasePointerCapture(ev2.pointerId) 1253 d3.select(this).classed('dragging', false).on('pointermove pointerup pointercancel', null) 1254 }) 1255 }) 1256 .on('mousemove', function(ev, d){ 1257 if (!ev.shiftKey || ev.buttons) return 1258 if (!previewing) previewing = {active: abcdState.active, val: abcdState.val} 1259 var box = this.getBoundingClientRect(), [lo, hi] = meta.ranges[d.v] 1260 setVar(d.v, lo + Math.round((ev.clientX - box.left)/box.width * (hi - lo))) 1261 d3.select(this).classed('dragging', true) 1262 }) 1263 .on('mouseleave', revertPreview) 1264 .on('keydown', function(ev, d){ 1265 var dx = ev.key == 'ArrowLeft' ? -1 : ev.key == 'ArrowRight' ? 1 : 0 1266 if (!dx) return 1267 ev.preventDefault() 1268 setVar(d.v, +this.textContent + dx * (ev.shiftKey ? 5 : 1)) 1269 }) 1270} 1271 1272function drawProbeRow(mount, {visState, renderAll, cfg, byX, withLegend, yLabel, colors}){ 1273 colors = colors || util.lineColors 1274 var hmCol = mount.append('div') 1275 var hmSel = hmCol.append('div.heatmap') 1276 var lnCol = mount.append('div') 1277 var lnSel = lnCol.append('div.lines') 1278 initOooHeatmap({visState, renderAll, sel: hmSel, cfg: {...cfg, colors}, byX}) 1279 initOooLines({visState, renderAll, sel: lnSel, cfg: {...cfg, yLabel, colors}, byX}) 1280 if (!withLegend) return 1281 util.drawColorbar(hmCol.append('div.heatmap-legend')) 1282 var lnLeg = lnCol.append('div.line-legend') 1283 var n = cfg.line_labels.length 1284 var legendItems = lnLeg.appendMany('div.legend-item', d3.range(n).reverse()) 1285 .each(function(i){ 1286 d3.select(this).append('div.legend-swatch').st({background: colors[(n-1-i) % colors.length]}) 1287 d3.select(this).append('span.lbl').text(cfg.line_labels[i] + '=') 1288 d3.select(this).append('span.val') 1289 }) 1290 renderAll.x.fns.push(() => { 1291 var xi = cfg.xs.indexOf(visState.x) 1292 var vals = cfg.line_vals_per_x?.[xi] || [] 1293 legendItems.select('span.val').text(i => vals[i] ?? '?') 1294 }) 1295} 1296 1297window.initOooAbcd = async function(opts){ 1298 var sel = d3.select('.ooo-abcd').html('') 1299 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1300 var {meta, loadVar} = await abcdShared(base, opts?.layerFilter) 1301 var renderAll = util.initRenderAll(['x']) 1302 var visState = {x: abcdState.val} 1303 1304 new IntersectionObserver((es, o) => { 1305 if (!es.some(e => e.isIntersecting)) return 1306 var probes = meta.probes || ['resid'] 1307 meta.vars.forEach(v =>
1307 probes.forEach(p => loadVar(v, p))) 1308 o.disconnect() 1309 }).observe(sel.node()) 1310 1311 var colors = [util.gray[500], util.series[4], util.series[2]] // warm orange/brown â distinct from each other, don't blend with viridis heatmap; match ooo-patch 1312 1313 abcdRedrawFns.length = 0 1314 abcdHeader(sel, 'Residual-Stream J-Lens Rank', meta, colors) 1315 1316 var rowMount = sel.append('div.probe-row') 1317 1318 async function draw(){ 1319 var v = abcdState.active, cfg = meta.cfgs[v] 1320 visState.x = abcdState.val 1321 renderAll.x.fns = [] 1322 var byX = await loadVar(v, 'resid') 1323 rowMount.html('') 1324 drawProbeRow(rowMount, {visState, renderAll, cfg, byX, withLegend: true, yLabel: 'Residual-stream J-Lens rank â', colors}) 1325 renderAll.x() 1326 } 1327 1328 abcdRedrawFns.push(draw) 1329 await draw() 1330} 1331 1332window.init?.() 1333 1334// âââ public/ooo-patch/init-ooo-patch.js âââ 1335window.initOooPatch = async function(opts){ 1336 var sel = d3.select('.ooo-patch').html('') 1337 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1338 var model = opts?.state?.model || 'opus-4.5' 1339 var d = await util.getFile(base + model + '.json') 1340 1341 sel.append('div.op-title').append('span.ttl').text('Patching intermediate values localizes to the same layers') 1342 var row = sel.append('div.panel-row') 1343 var W = 220, H = 140, M = {top: 16, right: 10, bottom: 24, left: 44} 1344 1345 var allDiff = d.cols.flatMap(col => d.panels[col].diff.flat()) 1346 var yExt = d3.extent(allDiff) 1347 var yPad = (yExt[1] - yExt[0]) * 0.04 1348 1349 // s = [textX, textY, ax, ay, bx, by, cx, cy] arc aâc through b, arrowhead at c 1350 var annos = { 1351 inter1: {L: 17, text: `Mean patching A+B at L${util.layerLabel(17)}\nusually flips the answer`, s: [-136,-42, -40,-22, -30, 0, -13, 0]}, 1352 inter2: {L: 19, text: '(A+B)*C flips\n~10% later', s: [-140,-35, -45,-37, -23,-32, -10,-21]}, 1353 ans: {L: 22, text: 'and the answer itself\nin the final layers', s: [-167, -5, -80, 14, -57, 33, -14, 12]}, 1354 } 1355 var dragOn = new URLSearchParams(location.search).get('drag') 1356 1357 var panelColor = {inter1: util.gray[500], inter2: util.series[4], ans: util.series[2]} // warm orange/brown â distinct from each other, don't blend with viridis heatmap; match ooo-abcd 1358 1359 d.cols.forEach((col, ci) => { 1360 var p = d.panels[col] 1361 var pc = panelColor[col] 1362 var cell = row.append('div.op-cell') 1363 1364 var c = d3.conventions({sel: cell.append('div.op-chart'), width: W, height: H, margin: M}) 1365 d3.select(c.svg.node().ownerSVGElement).append('defs').append('marker') 1366 .at({id: 'oop-arrow', viewBox: '0 0 10 10', refX: 8, refY: 5, markerWidth: 5, markerHeight: 5, orient: 'auto'}) 1367 .append('path').at({d: 'M 0 0 L 10 5 L 0 10 z', fill: '#333'}) 1368 c.x.domain([0, d.n_layers - 1]).rangeRound([0, c.width]) 1369 c.y.domain([yExt[0] - yPad, yExt[1] + yPad]).nice() 1370 c.xAxis.tickValues(util.layerTicks(d.n_layers)).tickFormat(util.layerLabel) 1371 c.yAxis.ticks(5) 1372 c.drawAxis() 1373 util.ggPlot(c) 1374 util.addAxisLabel(c, ci == 1 ? 'Layer (reindexed) â' : '', ci == 0 ? `Patched logit â "${d.orig_ans}" logit â` : '') 1375 c.svg.append('text.panel-title').at({x: c.width/2, y: -5, textAnchor: 'middle'}).text(p.expr) 1376 1377 var line = d3.line().x((_, i) => c.x(i)).y(v => c.y(v)) 1378 var ttSel = d3.select('.tooltip') 1379 var expr = p.expr 1380 1381 var tgtG = c.svg.append('g') 1382 var paths = tgtG.appendMany('path.tgt', p.targets) 1383 .at({d: (_, i) => line(p.diff[i]), fill: 'none', stroke: pc, strokeWidth: 0.7, opacity: 0.25}) 1384 1385 var hi = -1 1386 c.svg.append('rect.hover') 1387 .at({width: c.width, height: c.height, fill: 'none', pointerEvents: 'all'}) 1388 .on('mousemove', function(ev){ 1389 var [mx, my] = d3.pointer(ev, c.svg.node()) 1390 var L = d3.clamp(0, Math.round(c.x.invert(mx)), d.n_layers - 1) 1391 var yv = c.y.invert(my) 1392 var ti = d3.minIndex(p.diff, s => Math.abs(s[L] - yv)) 1393 if (ti !== hi){ 1394 if (hi >= 0) paths.filter((_, i) => i === hi).at({strokeWidth: 0.7, opacity: 0.25}) 1395 paths.filter((_, i) => i === ti).raise().at({strokeWidth: 1.5, opacity: 1}) 1396 hi = ti 1397 } 1398 var v = p.targets[ti] 1399 ttSel.classed('tooltip-hidden', false) 1400 .st({left: ev.clientX + 12, top: ev.clientY + 12}) 1401 .html(`L${util.layerLabel(L)} · ${d3.format('+.2f')(p.diff[ti][L])}<br>${expr} = ${v} â ans ${p.new_ans[v]}`) 1402 }) 1403 .on('mouseleave', function(){ 1404 if (hi >= 0) paths.filter((_, i) => i === hi).at({strokeWidth: 0.7, opacity: 0.25}) 1405 hi = -1 1406 ttSel.classed('tooltip-hidden', true) 1407 }) 1408 1409 var a = annos[col] 1410 if (a) drawSwoopy(c, c.x(a.L), c.y(p.mean[a.L]), a, col) 1411 }) 1412 1413 if (!d3.select('.tooltip').size()) d3.select('body').append('div.tooltip.tooltip-hidden') 1414 1415 function drawSwoopy(c, tx, ty, a, key){ 1416 var s = a.s 1417 var g = c.svg.append('g.anno').translate([tx, ty]) 1418 var path = g.append('path').at({stroke: '#000', fill: 'none', strokeWidth: 1, 'marker-end': 'url(#oop-arrow)'}) 1419 var text = g.append('text').at({fontSize: 12, fill: '#000'}) 1420 a.text.split('\n').forEach((ln, i) => text.append('tspan').at({x: 0, dy: i ? 12 : 0}).text(ln)) 1421 function render(){ 1422 text.translate([s[0], s[1]]) 1423 path.at({d: arcThrough([s[2],s[3]], [s[4],s[5]], [s[6],s[7]])}) 1424 } 1425 render() 1426 if (!dragOn) return 1427 g.appendMany('circle.drag-handle', [[0,1],[2,3],[4,5],[6,7]]) 1428 .at({r: 7, fill: 'rgba(0,0,0,0)', stroke: '#333', strokeDasharray: '2 2', cursor: 'move'}) 1429 .translate(d => [s[d[0]], s[d[1]]]) 1430 .call(d3.drag() 1431 .subject((e,d) =>
1431 ({x: s[d[0]], y: s[d[1]]})) 1432 .on('drag', function(e, d){ 1433 s[d[0]] = Math.round(e.x); s[d[1]] = Math.round(e.y) 1434 d3.select(this).translate([s[d[0]], s[d[1]]]) 1435 render() 1436 console.log(Object.entries(annos).map(([k,v]) => ` ${k}: {L: ${v.L}, text: ${JSON.stringify(v.text)}, s: [${v.s.join(',')}]},`).join('\n')) 1437 })) 1438 } 1439 function dist(a,b){ return Math.hypot(a[0]-b[0], a[1]-b[1]) } 1440 function arcThrough(a, b, c){ 1441 var A=dist(c,b), B=dist(b,a), C=dist(a,c) 1442 var ang = Math.acos((A*A + B*B - C*C)/(2*A*B)) 1443 var K = .5*A*B*Math.sin(ang) 1444 if (!K) return 'M'+a+' L'+c 1445 var r = Math.round(A*B*C/4/K*1000)/1000 1446 var laf = +(Math.PI/2 > ang), saf = +((c[0]-a[0])*(b[1]-a[1])-(c[1]-a[1])*(b[0]-a[0]) < 0) 1447 return ['M',a,'A',r,r,0,laf,saf,c].join(' ') 1448 } 1449 1450} 1451 1452window.init?.() 1453 1454// âââ public/feature-examples/init-feature-examples.js âââ 1455window.initFeatureExamples = async function(opts){ 1456 var sel = d3.select('.feature-examples').html('') 1457 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1458 var d = await util.getFile(base + 'feature_examples.json?v=' + Date.now()) 1459 1460 var orange = t => d3.interpolateOranges(0.1 + 0.55*t) 1461 1462 d.rows.forEach(row => { 1463 var col = sel.append('div.fe-col') 1464 var head = col.append('div.fe-colhead') 1465 head.append('div.fig-title').text(row.label) 1466 head.append('div.fe-colsub').text(row.sublabel) 1467 1468 col.appendMany('div.cell.fchip', row.chips).each(function(f){ 1469 var s = d3.select(this) 1470 var hdr = s.append('div.fchip-head') 1471 hdr.append('span.fe-key').text('Label') 1472 hdr.append('span.clerp').text(f.clerp || 'â').at({title: f.clerp || ''}) 1473 hdr.append('span.kurt').text(`κ = ${(f.kurt - 3).toFixed(1)}`) 1474 var tk = s.append('div.toks') 1475 tk.append('span.fe-key').text('Top J-tokens') 1476 tk.appendMany('span.tok', f.top_tokens).text(util.ppToken) 1477 s.appendMany('div.exrow', f.examples) 1478 .at({title: ex => ex.map(t => util.ppToken(t.t)).join('')}) 1479 .each(function(ex){ 1480 if (!ex.length) return 1481 var max = d3.max(ex, t => t.a) || 1 1482 var pi = d3.maxIndex(ex, t => t.a) 1483 var row = d3.select(this) 1484 function paint(t){ 1485 if (t.a <= 0.02*max) return 1486 var bg = orange(t.a/max) 1487 d3.select(this).st({background: bg, color: d3.hsl(bg).l < 0.6 ? '#fff' : null}) 1488 } 1489 row.append('span.exleft').appendMany('span.t', ex.slice(0, pi)) 1490 .text(t => util.ppToken(t.t)).each(paint) 1491 row.append('span.expeak').appendMany('span.t', [ex[pi]]) 1492 .text(t => util.ppToken(t.t)).each(paint) 1493 row.append('span.exright').appendMany('span.t', ex.slice(pi + 1)) 1494 .text(t => util.ppToken(t.t)).each(paint) 1495 }) 1496 }) 1497 }) 1498} 1499 1500window.init?.() 1501 1502// âââ public/feature-coverage/init-feature-coverage.js âââ 1503window.initFeatureCoverage = async function(opts){ 1504 var sel = d3.select('.feature-coverage').html('') 1505 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1506 var d = await util.getFile(base + 'feature_coverage.json') 1507 1508 var [wlo, whi] = d.workspace_band 1509 var xMax = d.layers[d.layers.length - 1] 1510 var sBlue = util.tol.blue 1511 var sOrange = util.tol.orange // SAE features in (a) â keep distinct from the blue series 1512 var sGray = util.gray[500] 1513 1514 // direct labels: multi-line tspans + draggable via ?tune=1 (util.dragTune, 1515 // same pattern as modulation-lines). dx/dy printed by the tune panel map back 1516 // to the dx/dy passed here. 1517 var tuneItems = [] 1518 var dLabel = (svg, {x, y, dx = 0, dy = 0, anchor = 'start', lines, color, key, tuneKey}) => { 1519 var t = svg.append('g.direct-label').translate([x, y]).append('text.dlabel') 1520 .at({textAnchor: anchor, 'data-series': key}) 1521 .st({fill: color, paintOrder: 'stroke', stroke: '#EAECED', strokeWidth: 1, strokeLinejoin: 'round', // lint-ok hex-js: ggPlot bg halo 1522 fontSize: 'var(--fs-label)', fontWeight: 600}) 1523 lines.forEach((ln, li) => t.append('tspan').text(ln).at({x: 0, dy: li ? '1.15em' : 0})) 1524 var p = {x: dx, y: dy} 1525 t.translate([p.x, p.y]) 1526 tuneItems.push({sel: t, key: tuneKey, get: () => ({...p}), set: q => { p = q; t.translate([q.x, q.y]) }}) 1527 return t 1528 } 1529 1530 var row = sel.append('div.panel-row') 1531
1532 // panels first so flex/stack layout settles, then size every chart off the 1533 // measured panel width â fills the row on desktop, full width stacked on mobile 1534 var panels = [0, 1, 2].map(() => row.append('div.fc-panel')) 1535 var panelW = Math.max(240, Math.min(420, opts?._panelW || panels[0].node().clientWidth || 270)) 1536 var pMargin = () => ({left: 48, right: 8, top: 32, bottom: 32}) 1537 // titles centered over the full panel (not just the plot area) so the three 1538 // panels share one title baseline and long titles don't clip on mobile 1539 var panelTitle = (c, text) => c.svg.append('g.axis').translate([panelW/2 - c.margin.left, -10]) 1540 .append('text.axis-label.axis-title').text(text).at({textAnchor: 'middle'}) 1541 1542 // ââ (a) histogram ââââââââââââââââââââââââââââââââââââââââââââââââââ 1543 var ch = d3.conventions({ 1544 sel: panels[0].append('div'), 1545 totalWidth: panelW, height: 230, 1546 margin: pMargin(), 1547 }) 1548 var h = d.hist 1549 var nz = i => h.sae[i] > 0 || h.cov[i] > 0 1550 var i0 = h.x.findIndex((_, i) => nz(i)) 1551 var i1 = h.x.length - 1 - [...h.sae].reverse().findIndex(v => v > 0) 1552 var xs = h.x.slice(i0, i1 + 2), sae = h.sae.slice(i0, i1 + 2), cov = h.cov.slice(i0, i1 + 2) 1553 ch.x = d3.scaleLog().domain([xs[0] * 0.9, xs[xs.length-1] * 1.05]).range([0, ch.width]) 1554 ch.y = d3.scaleLog().domain([1, d3.max(sae.concat(cov))]).range([ch.height, 0]).nice().clamp(true) 1555 var xticks = [1, 2, 3, 5, 10, 20, 30, 50, 100] 1556 .filter(v => v >= ch.x.domain()[0] && v <= ch.x.domain()[1]) 1557 ch.xAxis.scale(ch.x).tickValues(xticks).tickFormat(v => '' + v) 1558 ch.yAxis.scale(ch.y).ticks(6, '~s') 1559 ch.drawAxis(); util.ggPlot(ch) 1560 util.addAxisLabel(ch, 'Kurtosis â', 'Feature count â') 1561 panelTitle(ch, `Layer ${util.layerLabel(d.hist_layer)} J-lens kurtosis`) 1562 1563 var stepLine = ys => d3.line() 1564 .x((_,i) => ch.x(xs[i])).y(v => ch.y(Math.max(v, 0.5))) 1565 .curve(d3.curveStepAfter)(ys) 1566 ch.svg.append('path.line-series').at({d: stepLine(cov), fill: 'none', stroke: sGray, strokeWidth: 2, 'data-series': 'cov'}) 1567 ch.svg.append('path.line-series').at({d: stepLine(sae), fill: 'none', stroke: sOrange, strokeWidth: 2, 'data-series': 'sae'}) 1568 1569 ch.svg.append('line.ref-line').at({ 1570 x1: ch.x(h.thr), x2: ch.x(h.thr), y1: 0, y2: ch.height, 1571 }) 1572 // hand-placed via ?tune=1 â offsets from the threshold line / each percentile dot 1573 var annoOff = {x: 3, y: -42} 1574 var anno = ch.svg.append('text.fc-anno').text(`${d.kappa_mult}Ã null max`) 1575 .at({x: ch.x(h.thr) + annoOff.x, y: ch.height - 5 + annoOff.y, textAnchor: 'start'}) 1576 tuneItems.push({sel: anno, key: 'a-kappa-anno', 1577 get: () => ({...annoOff}), 1578 set: q => { annoOff = q; anno.at({x: ch.x(h.thr) + q.x, y: ch.height - 5 + q.y}) }}) 1579 var pctOff = {90: {x: -19, y: 14}, 95: {x: -22, y: 12}, 99: {x: -20, y: 12}} 1580 h.pcts.forEach(q => { 1581 var x = ch.x(q.k), y = ch.y(Math.max(1, q.y)) 1582 ch.svg.append('circle').at({cx: x, cy: y, r: 4, fill: sOrange, stroke: '#fff', strokeWidth: 1.5, 'data-series': 'sae'}) 1583 var off = pctOff[q.p] || {x: 5, y: -6} 1584 var pctT = ch.svg.append('text.fc-pct').text('p' + q.p) 1585 .at({x: x + off.x, y: y + off.y, textAnchor: 'start'}) 1586 tuneItems.push({sel: pctT, key: 'a-p' + q.p, 1587 get: () => ({...off}), 1588 set: qq => { off = qq; pctT.at({x: x + qq.x, y: y + qq.y}) }}) 1589 }) 1590 1591 // direct labels: SAE has the long right tail; null is the sharp left peak. 1592 var iSae = sae.findIndex(v => v > 0 && v < 200) 1593 dLabel(ch.svg, {x: ch.x(xs[iSae]), y: ch.y(sae[iSae]), dx: 8, dy: -7, 1594 lines: ['SAE features'], color: sOrange, key: 'sae', tuneKey: 'a-sae'}) 1595 var iCov = cov.findIndex(v => v > 0 && v < 100) 1596 dLabel(ch.svg, {x: ch.x(xs[iCov]), y: ch.y(cov[iCov]), dx: 156, dy: 45, anchor: 'start', 1597 lines: ['Random null'], color: sGray, key: 'cov', tuneKey: 'a-null'}) 1598 1599 // ââ (b) workspace fraction lines ââââââââââââââââââââââââââââââââââââ 1600 var c = d3.conventions({ 1601 sel: panels[1].append('div'), 1602 totalWidth: panelW, height: 230, 1603 margin: pMargin(), 1604 }) 1605 var allFrac = d.lines.upper.concat(d.lines.workspace) 1606 c.x.domain([0, xMax]) 1607 c.y.domain([0, d3.max(allFrac) * 1.08]) 1608 c.xAxis.scale(c.x).tickValues(util.layerTicks(xMax + 1, 6)).tickFormat(util.layerLabel) 1609 c.yAxis.scale(c.y).ticks(5).tickFormat(d3.format('.0%')) 1610 c.drawAxis(); util.ggPlot(c) 1611 util.addAxisLabel(c, 'Layer (reindexed) â', 'Fraction of features â') 1612 panelTitle(c, 'J-space-aligned fraction by layer') 1613 1614 util.addWsBand(c, wlo, whi) 1615 1616 var line = d3.line().x((v,i) =>
1616 c.x(d.layers[i])).y(v => c.y(v)) 1617 var lineSpec = [ 1618 ['upper', sGray, 1.5, [`All (κ > ${d.kappa_mult}à null max)`], {at: 16, dy: -35, dx: 16, anchor: 'end'}, 'b-all'], 1619 ['workspace', sBlue, 2.5, ['Excluding motor', 'features'], {at: 22, dy: 32, dx: 3, anchor: 'end'}, 'b-excl-motor'], 1620 ] 1621 lineSpec.forEach(([k, col, sw, lab, L, tk]) => { 1622 var ys = d.lines[k] 1623 c.svg.append('path.line-series').at({d: line(ys), fill: 'none', stroke: col, strokeWidth: sw, 'data-series': k}) 1624 c.svg.appendMany('circle', ys.map((v,i) => ({v, i}))) 1625 .at({cx: q => c.x(d.layers[q.i]), cy: q => c.y(q.v), r: 2.2, fill: col, 'data-series': k}) 1626 dLabel(c.svg, {x: c.x(L.at), y: c.y(ys[d.layers.indexOf(L.at)]), 1627 dx: L.dx ?? 0, dy: L.dy, anchor: L.anchor, lines: lab, color: col, key: k, tuneKey: tk}) 1628 }) 1629 1630 // ââ (c) excluding-motor fraction under three weightings ââââââââââââââ 1631 // dict line is identical to (b)'s blue "Excluding motor features"; 1632 // l0/l1 re-weight by firing count / activation mass. 1633 var w = d.weighted 1634 var ct = d3.conventions({ 1635 sel: panels[2].append('div'), 1636 totalWidth: panelW, height: 230, 1637 margin: pMargin(), 1638 }) 1639 var allW = w.dict.concat(w.l0, w.l1) 1640 ct.x.domain([0, xMax]) 1641 ct.y.domain([0, d3.max(allW) * 1.08]) 1642 ct.xAxis.scale(ct.x).tickValues(util.layerTicks(xMax + 1, 6)).tickFormat(util.layerLabel) 1643 ct.yAxis.scale(ct.y).ticks(5).tickFormat(d3.format('.0%')) 1644 ct.drawAxis(); util.ggPlot(ct) 1645 util.addAxisLabel(ct, 'Layer (reindexed) â', 'Fraction â') 1646 panelTitle(ct, 'Aligned fraction, activation-weighted') 1647 util.addWsBand(ct, wlo, whi, {label: false}) 1648 1649 var lineT = d3.line().x((v,i) => ct.x(d.layers[i])).y(v => ct.y(v)) 1650 var wSpec = [ 1651 ['dict', sBlue, 2.5, null, ['Excluding motor features'], {at: 24, dy: 38, dx: -41, anchor: 'end'}, 'c-excl-motor'], 1652 ['l1', util.tol.magenta, 1.8, '4,3', ['L1-weighted'], {at: 13, dy: -72, dx: 0, anchor: 'end'}, 'c-l1'], 1653 ['l0', util.tol.teal, 1.8, '4,3', ['L0-weighted'], {at: 23, dy: 24, dx: 5, anchor: 'end'}, 'c-l0'], 1654 ] 1655 wSpec.forEach(([k, col, sw, dash, lab, L, tk]) => { 1656 var ys = w[k] 1657 ct.svg.append('path.line-series') 1658 .at({d: lineT(ys), fill: 'none', stroke: col, strokeWidth: sw, 1659 strokeDasharray: dash, 'data-series': k}) 1660 ct.svg.appendMany('circle', ys.map((v,i) => ({v, i}))) 1661 .at({cx: q => ct.x(d.layers[q.i]), cy: q => ct.y(q.v), r: 2.2, 1662 fill: col, 'data-series': k}) 1663 dLabel(ct.svg, {x: ct.x(L.at), y: ct.y(ys[d.layers.indexOf(L.at)]), 1664 dx: L.dx ?? 0, dy: L.dy, anchor: L.anchor, lines: lab, color: col, key: k, tuneKey: tk}) 1665 }) 1666 1667 util.bindSeriesHover(sel) 1668 // ?tune=1 on the standalone page makes the labels draggable; copy dx/dy back here 1669 util.dragTune(tuneItems, 'feature-coverage') 1670 1671 // rendering can add a page scrollbar and shrink the panels â re-render once at the settled width 1672 var settledW = panels[0].node().clientWidth 1673 if (!opts?._panelW && settledW && Math.abs(settledW - panelW) > 2) 1674 return window.initFeatureCoverage({...opts, _panelW: settledW}) 1675} 1676 1677window.init?.() 1678 1679// âââ public/comp-lines/init-comp-lines.js âââ 1680window.initCompLines = async function(opts){ 1681 var sel = d3.select('.comp-lines-attn-adj').html('') 1682 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1683 var d = await util.getFile(base + 'comp_lines.json') 1684 1685 // Match comp-tail-energy: jlens = orange accent, SAE percentile bins = 1686 // Blues ramp (darkâlight = topâmid), neuron = purple, random = gray 1687 // (y=1 reference line + legend row). 1688 var saeBlue = i => d3.interpolateBlues(0.9 - 0.7 * i / 2) 1689 var COLOR = { 1690 jlens_vocab: util.tol.orange, 1691 sae_top1: saeBlue(0), 1692 sae_top5: saeBlue(1), 1693 sae_mid: saeBlue(2), 1694 neuron_out: util.series[3], 1695 neuron_in: util.series[3], 1696 random: util.gray[500], 1697 } 1698 var srcs = d.sources.map(s => ({...s, color: COLOR[s.key]})) // lint-ok data-color-js: COLOR override 1699 1700 var [wlo, whi] = d.workspace_band 1701 var xLo = 4, xMax = util.DISPLAY_LAYERS.length - 1 1702 var iLo = d.layers.findIndex(L => L >= xLo) 1703 var allMed = d.panels.flatMap(p => 1704 srcs.flatMap(s => (d.data[p.key][s.key] ?? []).slice(iLo))) 1705 .filter(v => v != null) 1706 var yDom = [Math.min(0.85, d3.min(allMed) * 0.9), d3.max(allMed) * 1.1] 1707
1708 var mountW = sel.node()?.getBoundingClientRect().width || 0 1709 var stacked = mountW > 0 && mountW < 700 1710 var panelW = stacked ? Math.min(420, Math.max(240, mountW - 52)) : 290 1711 1712 var prow = sel.append('div.panel-row') 1713 d.panels.forEach((p, pi) => { 1714 var hasY = stacked || !pi 1715 var c = d3.conventions({ 1716 sel: prow.append('div.cl-panel').append('div'), 1717 width: panelW, height: 240, 1718 margin: {left: hasY ? 48 : 6, right: 4, top: 24, bottom: 32}, 1719 }) 1720 c.x.domain([xLo, xMax]) 1721 c.y = d3.scaleLog().domain(yDom).range([c.height, 0]) 1722 c.xAxis.scale(c.x).tickValues(util.layerTicks(xMax + 1, 6).filter(t => t >= xLo)).tickFormat(util.layerLabel) 1723 c.yAxis.scale(c.y).ticks(5, '~g') 1724 c.drawAxis(); util.ggPlot(c) 1725 if (!hasY) c.svg.select('.y').selectAll('text').remove() 1726 var rw = {attn_v_var: ' (read)', attn_qk_var: ' (read)', attn_w_var: ' (write)'}[p.key] || '' 1727 util.addAxisLabel(c, 'Source layer (reindexed) â', hasY ? 'Median composition ratio â' : '', p.title + rw) 1728 util.addWsBand(c, wlo, whi, {label: pi === d.panels.length - 1 && 'Workspace layers'}) 1729 c.svg.append('line.ref-line') 1730 .at({x1: 0, x2: c.width, y1: c.y(1), y2: c.y(1), stroke: COLOR.random}) 1731 1732 var line = d3.line().defined((v, i) => v != null && d.layers[i] >= xLo) 1733 .x((v, i) => c.x(d.layers[i])).y(v => c.y(v)) 1734 srcs.slice().reverse().forEach(s => { 1735 var vals = d.data[p.key][s.key] 1736 if (!vals) return 1737 c.svg.append('path.line-series').at({ 1738 d: line(vals), fill: 'none', 'data-series': s.key, 1739 stroke: s.color, strokeWidth: s.key === 'jlens_vocab' ? 2.2 : 1.8, 1740 }) 1741 c.svg.appendMany('circle', 1742 vals.map((v, i) => ({v, i})).filter(m => m.v != null && d.layers[m.i] >= xLo)) 1743 .at({cx: m => c.x(d.layers[m.i]), cy: m => c.y(m.v), r: 2.2, fill: s.color}) // lint-ok data-color-js 1744 }) 1745 }) 1746 1747 // Side legend column. neuron_out/in collapse to one "Neuron" row. 1748 var seen = new Set() 1749 var legRows = srcs.filter(s => !seen.has(s.label) && seen.add(s.label)) 1750 .concat([{key: 'random', label: 'Random direction', color: COLOR.random}]) 1751 prow.append('div.cl-attn-legend') 1752 .appendMany('div.cl-leg-row', legRows) 1753 .at({'data-series': s => s.key}) 1754 .each(function(s){ 1755 d3.select(this).append('span.cl-swatch').st({background: s.color}) // lint-ok data-color-js 1756 d3.select(this).append('span').text(s.label) 1757 }) 1758 util.bindSeriesHover(sel) 1759} 1760 1761window.init?.() 1762 1763// âââ public/comp-strata/init-comp-strata.js âââ 1764window.initCompStrata = async function(opts){ 1765 var variant = opts?.variant ?? 'sae6' 1766 var sel = d3.select(`.comp-strata[data-variant="${variant}"]`).html('') 1767 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1768 var d = await util.getFile(base + 'comp_strata.json') 1769 var v = d[variant] 1770 1771 // sae6 bins are an ordinal lens-readability percentile â single-hue ramp. 1772 // Blues by default (matches the matplotlib original; light end stays 1773 // legible). ?ramp=viridis|orange to compare. 1774 var rampName = new URLSearchParams(location.search).get('ramp') || 'blues' 1775 var RAMPS = { 1776 blues: t => d3.interpolateBlues(0.9 - 0.7 * t), 1777 viridis: t => d3.interpolateViridis(0.15 + 0.75 * t), 1778 orange: t => util.tint(util.tol.orange, 0.05 + 0.75 * t), 1779 } 1780 var ramp = RAMPS[rampName] || RAMPS.blues 1781 var COLOR = variant === 'sae6' 1782 ? Object.fromEntries(v.bins.map((b, i) => 1783 [b.key, ramp(i / (v.bins.length - 1))])) 1784 : {jlens_vocab: util.lensColors.jacobian, neuron_out: util.series[3]} 1785 1786 var keys = v.bins.map(b => b.key) 1787 var [wlo, whi] = d.workspace_band 1788 var xMax = util.DISPLAY_LAYERS.length - 1 1789 var SIDES = [ 1790 {key: 'read', title: 'MLP input (read)'}, 1791 {key: 'write', title: 'MLP output (write)'}, 1792 ] 1793 1794 var prow = sel.append('div.panel-row') 1795 SIDES.forEach((side, pi) => { 1796 var rows = d.layers.map((L, i) => { 1797 var r = {layer: L} 1798 keys.forEach(k => r[k] = v.data[side.key][k][i]) 1799 return r 1800 }) 1801 var stack = d3.stack().keys(keys)(rows) 1802 1803 var c = d3.conventions({ 1804 sel: prow.append('div.cs-panel').append('div'), 1805 width: 320, height: 220, 1806 margin: {left: pi ? 6 : 50, right: pi ? 92 : 6, top: 24, bottom: 32}, 1807 }) 1808 c.x.domain([d.layers[0], xMax]) 1809 c.y.domain([0, 1]) 1810 c.xAxis.tickValues(util.layerTicks(xMax + 1, 6)).tickFormat(util.layerLabel) 1811 c.yAxis.ticks(4).tickFormat(d3.format('.0%')) 1812 c.drawAxis(); util.ggPlot(c) 1813 if (pi) c.svg.select('.y').selectAll('text').remove() 1814 util.addAxisLabel(c, 'Source layer (reindexed) â', 1815 pi ? '' : 'Neurons best-matched â', side.title, 0, -3) 1816 1817 var area = d3.area() 1818 .x((p, i) =>
1818 c.x(d.layers[i])) 1819 .y0(p => c.y(p[0])).y1(p => c.y(p[1])) 1820 // own <g> so bindSeriesHover's .raise() stays under edges/ref-lines 1821 c.svg.append('g').appendMany('path.cs-area', stack) 1822 .at({d: area, fill: s => COLOR[s.key], 'data-series': s => s.key}) 1823 c.svg.appendMany('path.cs-edge', stack.slice(0, -1)) 1824 .at({d: s => d3.line().x((p,i) => c.x(d.layers[i])) 1825 .y(p => c.y(p[1]))(s), fill: 'none'}) 1826 1827 // uniform-prior reference grid: 1/n lines on top of the fills 1828 d3.range(1, keys.length).forEach(k => 1829 c.svg.append('line.ref-line') 1830 .at({x1: 0, x2: c.width, y1: c.y(k/keys.length), y2: c.y(k/keys.length)})) 1831 1832 // workspace-band: bracket at the bottom of the plot interior, ticks 1833 // descending toward the x-tick numbers; label just above the bar. 1834 var bx0 = c.x(wlo), bx1 = c.x(whi), bh = 5, by = c.height - bh - 1 1835 c.svg.append('path.ws-bracket') 1836 .at({d: `M${bx0},${by+bh} V${by} H${bx1} V${by+bh}`}) 1837 c.svg.append('text.ws-label').text('Workspace layers') 1838 .translate([(bx0 + bx1) / 2, by - 14]).st({textAnchor: 'middle'}) 1839 1840 if (pi !== 1) return 1841 var iEnd = d.layers.length - 1 1842 var anchors = stack.map(s => ({ 1843 key: s.key, y: c.y((s[iEnd][0] + s[iEnd][1]) / 2), 1844 label: v.bins.find(b => b.key === s.key).label, 1845 })) 1846 util.spreadLabels?.(anchors, 13, 0, c.height) 1847 // light-tint label text is unreadable; clamp label color to ramp(â¤0.4). 1848 var labelColor = k => variant === 'sae6' 1849 ? ramp(Math.min(0.4, keys.indexOf(k) / (keys.length - 1))) 1850 : COLOR[k] 1851 anchors.forEach(a => util.directLabel(c.svg, { 1852 x: c.width, y: a.y, dx: 6, dy: 4, anchor: 'start', 1853 text: a.label, color: labelColor(a.key), key: a.key, 1854 })) 1855 if (variant === 'sae6') 1856 c.svg.append('text').text('by J-lens kurtosis') 1857 .translate([c.width + 6, -10]) 1858 .st({fontSize: 'var(--fs-label)', fill: 'var(--text-light)'}) 1859 }) 1860 util.bindSeriesHover(sel) 1861} 1862 1863window.init?.() 1864 1865// âââ public/comp-tail-energy/init-comp-tail-energy.js âââ 1866window.initCompTailEnergy = async function(opts){ 1867 var sel = d3.select('.comp-tail-energy').html('') 1868 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1869 var d = await util.getFile(base + 'comp_tail_energy.json') 1870 1871 // Match the matplotlib + comp-strata palette: SAE percentile bins = Blues 1872 // ramp (darkâlight = topâbottom); jlens = orange so it separates from the 1873 // blue SAE stack; neuron = purple. Random sits at y=1 by construction â 1874 // drawn as a reference line, listed in the legend. 1875 var saeKeys = d.families.map(f => f.key).filter(k => k.startsWith('sae_')) 1876 var saeRamp = i => d3.interpolateBlues(0.9 - 0.7 * i / (saeKeys.length - 1)) 1877 var COLOR = { 1878 jlens_vocab: util.tol.orange, 1879 neuron_out: util.series[3], 1880 random: util.gray[500], 1881 ...Object.fromEntries(saeKeys.map((k, i) => [k, saeRamp(i)])), 1882 } 1883 var LW = k => k === 'jlens_vocab' ? 2.2 : k === 'neuron_out' ? 1.8 : 1.6 1884 1885 var [wlo, whi] = d.workspace_band 1886 var xLo = d.layers[0], xMax = util.DISPLAY_LAYERS.length - 1 1887 var allMed = ['read', 'write'].flatMap(s => 1888 d.families.flatMap(f => d.data[s][f.key]).filter(v => v != null)) 1889 var yMax = d3.max(allMed) * 1.05 1890 1891 var SIDES = [ 1892 {key: 'read', title: 'MLP input weights (read)'}, 1893 {key: 'write', title: 'MLP output weights (write)'}, 1894 ] 1895
1896 var mountW = sel.node()?.getBoundingClientRect().width || 0 1897 var stacked = mountW > 0 && mountW < 700 1898 var panelW = stacked ? Math.min(420, Math.max(260, mountW - 52)) : 360 1899 1900 var prow = sel.append('div.panel-row') 1901 SIDES.forEach((side, pi) => { 1902 var hasY = stacked || !pi 1903 var c = d3.conventions({ 1904 sel: prow.append('div.cte-panel').append('div'), 1905 width: panelW, height: 250, 1906 margin: {left: hasY ? 44 : 6, right: 4, top: 24, bottom: 32}, 1907 }) 1908 c.x.domain([xLo, xMax]) 1909 c.y.domain([0, yMax]) 1910 c.xAxis.tickValues(util.layerTicks(xMax + 1, 6).filter(t => t >= xLo)).tickFormat(util.layerLabel) 1911 c.yAxis.ticks(5) 1912 c.drawAxis(); util.ggPlot(c) 1913 if (!hasY) c.svg.select('.y').selectAll('text').remove() 1914 util.addAxisLabel(c, 'Source layer (reindexed) â', hasY ? 'Tail-energy ratio â' : '', side.title) 1915 util.addWsBand(c, wlo, whi, {label: pi === 1 && 'Workspace layers'}) 1916 c.svg.append('line.ref-line') 1917 .at({x1: 0, x2: c.width, y1: c.y(1), y2: c.y(1), stroke: COLOR.random}) 1918 1919 var line = d3.line().defined(v => v != null) 1920 .x((v, i) => c.x(d.layers[i])).y(v => c.y(v)) 1921 d.families.slice().reverse().forEach(f => { 1922 var vals = d.data[side.key][f.key] 1923 c.svg.append('path').at({ 1924 d: line(vals), fill: 'none', 1925 stroke: COLOR[f.key], strokeWidth: LW(f.key), 1926 }) 1927 c.svg.appendMany('circle', vals.map((v, i) => ({v, i})).filter(m => m.v != null)) 1928 .at({cx: m => c.x(d.layers[m.i]), cy: m => c.y(m.v), r: 2.0, fill: COLOR[f.key]}) // lint-ok data-color-js 1929 }) 1930 1931 }) 1932 1933 // Legend column to the right of the panels. 1934 var legRows = [...d.families, {key: 'random', label: 'Random direction'}] 1935 var leg = prow.append('div.cte-legend') 1936 leg.appendMany('div.cte-leg-row', legRows) 1937 .each(function(f){ 1938 d3.select(this).append('span.cte-swatch').st({background: COLOR[f.key]}) // lint-ok data-color-js 1939 d3.select(this).append('span').text(f.label) 1940 }) 1941} 1942 1943window.init?.() 1944 1945// âââ public/mlp-gain/init-mlp-gain.js âââ 1946window.initMlpGain = async function(opts){ 1947 var sel = d3.select('.mlp-gain').html('') 1948 var base = (opts?.datapath || './').replace(/\/?$/, '/') 1949 var d = await util.getFile(base + 'mlp_gain.json') 1950 1951 // left = lens comparisons (paper palette); right = SAE strata (Blues ramp, 1952 // matches comp-strata so the readability ordinal reads the same way). 1953 var saeKeys = d.panels[1].families.map(f => f.key) 1954 var blues = t => d3.interpolateBlues(0.9 - 0.6 * t) 1955 var COLOR = { 1956 jlens_vocab: util.tol.orange, 1957 neuron_out: util.gray[600], 1958 ...Object.fromEntries(saeKeys.map((k, i) => [k, blues(i / (saeKeys.length - 1))])), 1959 } 1960 1961 var [wlo, whi] = d.workspace_band 1962 var xMax = util.DISPLAY_LAYERS.length - 1 1963 var allKeys = d.panels.flatMap(p => p.families.map(f => f.key)) 1964 var allV = allKeys.flatMap(k => d.data[k]) 1965 var yDom = [Math.min(0.85, d3.min(allV) * 0.95), d3.max(allV) * 1.1] 1966
1967 var mountW = sel.node()?.getBoundingClientRect().width || 0 1968 var stacked = mountW > 0 && mountW < 700 1969 var panelW = stacked ? Math.min(420, Math.max(240, mountW - 56)) : 340 1970 1971 var LABEL = { 1972 jlens_vocab: {at: 14, dy: -10, dx: -2, anchor: 'end'}, 1973 neuron_out: {at: 3, dy: -10, dx: 4, anchor: 'start'}, 1974 } 1975 1976 var prow = sel.append('div.panel-row') 1977 d.panels.forEach((p, pi) => { 1978 var hasY = stacked || !pi 1979 var c = d3.conventions({ 1980 sel: prow.append('div.mg-panel').append('div'), 1981 width: panelW, height: 250, 1982 margin: {left: hasY ? 50 : 6, right: pi ? 110 : 8, top: 24, bottom: 32}, 1983 }) 1984 c.x.domain([0, xMax]) 1985 c.y = d3.scaleLog().domain(yDom).range([c.height, 0]) 1986 c.xAxis.scale(c.x).tickValues(util.layerTicks(xMax + 1, 6)).tickFormat(util.layerLabel) 1987 c.yAxis.scale(c.y).ticks(5, '~g') 1988 c.drawAxis(); util.ggPlot(c) 1989 if (!hasY) c.svg.select('.y').selectAll('text').remove() 1990 util.addAxisLabel(c, 'Source layer (reindexed) â', hasY ? 'Median MLP block gain â' : '', p.title) 1991 1992 util.addWsBand(c, wlo, whi, {label: pi === 1 && 'Workspace layers'}) 1993 c.svg.append('line.ref-line').at({x1: 0, x2: c.width, y1: c.y(1), y2: c.y(1)}) 1994 if (!pi) c.svg.append('text').text('Random direction') 1995 .at({x: c.width - 4, y: c.y(1) - 5, textAnchor: 'end'}) 1996 .st({fill: 'var(--text-light)', fontSize: 'var(--fs-label)'}) 1997 1998 var line = d3.line().x((v, i) => c.x(d.layers[i])).y(v => c.y(v)) 1999 p.families.slice().reverse().forEach(f => { 2000 var vals = d.data[f.key], col = COLOR[f.key] 2001 c.svg.append('path.line-series').at({ 2002 d: line(vals), fill: 'none', 'data-series': f.key, 2003 stroke: col, strokeWidth: f.key === 'jlens_vocab' ? 2.2 : 1.8, 2004 }) 2005 c.svg.appendMany('circle', vals.map((v, i) => ({v, i}))) 2006 .at({cx: m => c.x(d.layers[m.i]), cy: m => c.y(m.v), r: 2.2, fill: col}) // lint-ok data-color-js 2007 }) 2008 2009 if (!pi) p.families.forEach(f => { 2010 var L = LABEL[f.key]; if (!L) return 2011 var li = d.layers.indexOf(L.at) 2012 if (li < 0) li = d3.bisectLeft(d.layers, L.at) 2013 util.directLabel(c.svg, { 2014 x: c.x(d.layers[li]), y: c.y(d.data[f.key][li]), 2015 dx: L.dx, dy: L.dy, anchor: L.anchor, 2016 text: f.label, color: COLOR[f.key], key: f.key, 2017 }) 2018 }) 2019 else { 2020 var leg = c.svg.append('g.mg-leg').translate([c.width + 10, 8]) 2021 leg.append('text.leg-title').text('J-lens kurtosis').at({y: -2}) 2022 leg.append('text.leg-title').text('percentile').at({y: 10}) 2023 p.families.forEach((f, i) => { 2024 var g = leg.append('g').translate([0, 26 + i * 15]) 2025 g.append('line').at({x1: 0, x2: 14, stroke: COLOR[f.key], strokeWidth: 2.5}) 2026 g.append('text').text(f.label).at({x: 19, dy: '.32em'}) 2027 .st({fill: util.gray[700]}) 2028 }) 2029 } 2030 }) 2031 util.bindSeriesHover(sel) 2032} 2033 2034window.init?.() 2035 2036// âââ public/lens-similarity/init-lens-similarity.js âââ 2037window.initLensSimilarity = async function(opts){ 2038 var sel = d3.select('.lens-similarity').html('') 2039 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2040 var d = await util.getFile(base + 'lens_similarity.json') 2041 2042 // Per-pair line colour. Jacobian-involving pairs take the *other* lens's 2043 // util.lensColors hue (matching the per-method palette in line-panels); 2044 // the no-jacobian logitâtuned pair is neutral gray. 2045 var COLOR = { 2046 'jacobian-logit': util.lensColors.logit, 2047 'jacobian-tuned': util.lensColors.tuned, 2048 'logit-tuned': util.gray[600], 2049 } 2050 d.pairs = d.pairs.map(p => ({...p, color: COLOR[p.key]})) // lint-ok data-color-js: COLOR override above 2051 2052 // Direct labels â anchored on panel 0 (cosine) where the three lines have 2053 // the most vertical separation in the mid layers. 2054 var LABEL = { 2055 'jacobian-logit': {at: 14, dy: -10, dx: -4, anchor: 'end'}, 2056 'jacobian-tuned': {at: 17, dy: 14, dx: 2, anchor: 'start'}, 2057 'logit-tuned': {at: 6, dy: -10, dx: 0, anchor: 'middle'}, 2058 } 2059 2060 var [wlo, whi] = d.workspace_band 2061 var xMax = d.layers[d.layers.length - 1] 2062 // cos/agree are bounded [0,1] â no negative axis space, no headroom past 100%; 2063 // dots at 0/1 sit on the boundary 2064 var yDom = { 2065 cos: [0, 1], 2066 kl: [0, d3.max(d.pairs.flatMap(p => d.data.kl[p.key])) * 1.08], 2067 agree: [0, 1], 2068 } 2069 2070 var prow = sel.append('div.panel-row')
2071 d.panels.forEach((p, pi) => { 2072 var cell = prow.append('div.ls-panel') 2073 var c = d3.conventions({ 2074 sel: cell.append('div'), 2075 width: 290, height: 230, 2076 margin: {left: pi ? 42 : 48, right: 6, top: 24, bottom: 32}, 2077 }) 2078 c.x.domain([0, xMax]) 2079 c.y.domain(yDom[p.key]) 2080 // cos/agree are bounded [0,1] â nice() would expand the small pad to [-0.1, 1.1] 2081 if (p.key === 'kl') c.y.nice() 2082 c.xAxis.scale(c.x).tickValues(util.layerTicks(xMax + 1, 6)).tickFormat(util.layerLabel) 2083 c.yAxis.scale(c.y).ticks(5) 2084 c.drawAxis(); util.ggPlot(c) 2085 if (pi) c.svg.select('.x .tick text').remove() 2086 util.addAxisLabel(c, 'Layer (reindexed) â', p.ylabel + ' â', p.title) 2087 2088 util.addWsBand(c, wlo, whi, {label: pi === d.panels.length - 1 && 'Workspace layers'}) 2089 2090 var line = d3.line().defined(v => v != null) 2091 .x((v,i) => c.x(d.layers[i])).y(v => c.y(v)) 2092 d.pairs.forEach(s => { 2093 var ys = d.data[p.key][s.key] 2094 c.svg.append('path.line-series').at({ 2095 d: line(ys), fill: 'none', 'data-series': s.key, 2096 stroke: s.color, strokeWidth: 1.9, 2097 }) 2098 c.svg.appendMany('circle', ys.map((v,i) => ({v, i})).filter(m => m.v != null)) 2099 .at({cx: m => c.x(d.layers[m.i]), cy: m => c.y(m.v), r: 2.2, fill: s.color}) // lint-ok data-color-js 2100 }) 2101 2102 if (pi) return 2103 d.pairs.forEach(s => { 2104 var ys = d.data[p.key][s.key], L = LABEL[s.key] 2105 var li = d.layers.indexOf(L.at) 2106 if (li < 0) return 2107 // â falls back to a font whose glyph sits on the baseline â render an en dash instead 2108 util.directLabel(c.svg, { 2109 x: c.x(L.at), y: c.y(ys[li]), 2110 dx: L.dx, dy: L.dy, anchor: L.anchor, 2111 text: s.label.replace(' â ', ' â '), color: s.color, key: s.key, 2112 }) 2113 }) 2114 }) 2115 util.bindSeriesHover(sel) 2116} 2117 2118window.init?.() 2119 2120// âââ public/transcoder-arith/init-transcoder-arith.js âââ 2121window.initTranscoderArith = async function(opts){ 2122 var sel = d3.select('.transcoder-arith') 2123 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2124 var d = await util.getFile(base + 'arith.json') 2125 2126 var ML0 = 72, MLn = 22, MRz = 22 2127 2128 function render(){ 2129 sel.html('') 2130 var row = sel.append('div.panel-row') 2131 // stacked (mobile) mode: panel-row goes flex-direction:column via the container query 2132 var stacked = getComputedStyle(row.node()).flexDirection === 'column' 2133 var cw = sel.node().offsetWidth || 910 2134 // stacked panels fill the container width; desktop keeps the fixed 240px panels 2135 var s = stacked ? Math.max(1.6, Math.min(3.4, (cw - ML0 - MRz) / 100)) : 2.4 2136 var W = 100 * s 2137 var orange = d3.scaleSequential(d3.interpolateOranges).domain([0, 1.4]).clamp(true) 2138 var prgn = d3.scaleDiverging(d3.interpolatePRGn).domain([-1, 0, 1]).clamp(true) 2139 var ticks20 = d3.range(0, 100, 20) 2140 2141 function vgrid(c){ 2142 d3.range(0, 101, 20).forEach(t => c.svg.append('line') 2143 .at({x1: c.x(t), x2: c.x(t), y1: 0, y2: c.height, stroke: '#fff', strokeWidth: 0.7})) 2144 } 2145 function frame(c){ 2146 c.svg.append('rect').at({width: c.width, height: c.height, fill: 'none', stroke: util.gray[400], strokeWidth: 0.6}) 2147 } 2148 function barLabel(svg, name, h){ 2149 var t = svg.append('text').at({x: -28, y: h/2 - 3, textAnchor: 'end', fill: util.gray[700]}) 2150 t.append('tspan.ta-barname').text(name) 2151 var f = t.append('tspan.ta-formula').at({x: -28, dy: 11, fill: util.gray[500]}) 2152 f.append('tspan').text('· J · W') 2153 f.append('tspan').text('U').at({baselineShift: 'sub', fontSize: '70%'}) // lint-ok inline-font-js: subscript 2154 } 2155 function sectionLabel(svg, txt, w, h){ 2156 svg.append('text.mlabel').text(txt) 2157 .at({x: w + 16, y: h/2, textAnchor: 'middle', fill: util.gray[500], 2158 transform: `rotate(90 ${w + 16} ${h/2})`}) 2159 } 2160 2161 d.features.forEach((f, fi) => { 2162 // when stacked, every panel is redrawn with its own y-axis labels, bar labels, 2163 // and section labels so each fills the width and they all share the same left edge 2164 var first = stacked || fi === 0 2165 var lastp = stacked || fi === d.features.length - 1 2166 var ML = stacked ? ML0 : (first ? ML0 : MLn) 2167 var MR = lastp ? MRz : 4 2168 var cell = row.append('div.ta-panel') 2169 cell.append('div.fig-title').text(f.title).st({marginLeft: ML, marginRight: MR}) 2170 2171 // operand activation: 100Ã100, row=a, col=b 2172 var c = d3.conventions({ 2173 sel: cell.append('div'), 2174 width: W, height: W, layers: 'sc', 2175 margin: {left: ML, right: MR, top: 2, bottom: 20}, 2176 }) 2177 c.x.domain([0, 100]); c.y.domain([0, 100]) 2178 c.xAxis.tickValues(ticks20).tickFormat(d => d).tickSize(3) 2179 c.yAxis.tickValues(ticks20).tickFormat(d => d).tickSize(3) 2180 c.drawAxis() 2181 c.svg.insert('rect', ':first-child').at({width: c.width, height: c.height, fill: util.gray[100]}) 2182 util.addAxisLabel(c, 'Operand b â', first ? 'Operand a â' : '') 2183 var ctx = c.layers[1] 2184 for (var i = 0; i < 100; i++) for (var j = 0; j < 100; j++){ 2185 var v = f.heatmap[i][j] 2186 if (v <= 0) continue 2187 ctx.fillStyle = orange(v) 2188 ctx.fillRect(j*s, (99-i)*s, s, s) 2189 }
2190 d3.range(0, 101, 20).forEach(t => c.svg.append('line') 2191 .at({x1: 0, x2: c.width, y1: c.y(t), y2: c.y(t), stroke: '#fff', strokeWidth: 0.7})) 2192 vgrid(c); frame(c) 2193 if (lastp) sectionLabel(c.svg, 'avg activation', c.width, c.height) 2194 2195 cell.append('div.ta-gap') 2196 2197 // lensed bars: 10Ã100 each, row=hundreds digit, col=last two digits of sum 2198 d.bars.forEach((b, bi) => { 2199 var last = bi === d.bars.length - 1 2200 var cb = d3.conventions({ 2201 sel: cell.append('div'), 2202 width: W, height: 10*s, layers: 'sc', 2203 margin: {left: ML, right: MR, top: 0, bottom: last ? 30 : 12}, 2204 }) 2205 cb.x.domain([0, 100]); cb.y.domain([0, 10]) 2206 cb.xAxis.tickValues(ticks20) 2207 .tickFormat(last ? d => '_' + String(d).padStart(2, '0') : () => '').tickSize(3) 2208 cb.yAxis.tickValues([0.5, 9.5]) 2209 .tickFormat(d => Math.floor(d) + '__').tickSize(0) 2210 cb.drawAxis() 2211 cb.svg.selectAll('.y .tick text').at({fontSize: 'var(--fs-label)', fill: util.gray[500], x: -3}) 2212 if (last) util.addAxisLabel(cb, 'Predicted sum (last two digits) â', '') 2213 if (first) barLabel(cb.svg, b.name || b.label, cb.height) 2214 var g = f[b.key].grid, cx = cb.layers[1] 2215 for (var h = 0; h < 10; h++) for (var j = 0; j < 100; j++){ 2216 cx.fillStyle = prgn(g[h][j]) 2217 cx.fillRect(j*s, (9-h)*s, s, s) 2218 } 2219 vgrid(cb); frame(cb) 2220 if (lastp && bi === 0) sectionLabel(cb.svg, 'J-lens effect', cb.width, d.bars.length * (10*s + 3)) 2221 }) 2222 }) 2223 util.colorCaption(sel, {promotes: prgn(0.7), suppresses: prgn(-0.7)}) 2224 return {stacked, w: cw} 2225 } 2226 2227 var cur = render() 2228 var mount = sel.node() 2229 mount.__taRO?.disconnect() 2230 mount.__taRO = new ResizeObserver(util.throttleDebounce(() => { 2231 var w = mount.offsetWidth 2232 var stacked = getComputedStyle(sel.select('.panel-row').node()).flexDirection === 'column' 2233 if (stacked !== cur.stacked || (stacked && Math.abs(w - cur.w) > 4)) cur = render() 2234 }, 120)) 2235 mount.__taRO.observe(mount.closest('figure') || mount) 2236} 2237 2238window.init?.() 2239 2240// âââ public/transcoder-translation/init-transcoder-translation.js âââ 2241window.initTranscoderTranslation = async function(opts){ 2242 var sel = d3.select('.transcoder-translation').html('') 2243 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2244 var d = await util.getFile(base + 'translation.json') 2245 2246 // Orange is reserved for activation strength in this paper â categorical series 2247 // use Tol vibrant from util.tol. Override the bake-supplied colors so a future 2248 // change to util.tol propagates without re-baking. 2249 var catOverride = {0: util.tol.blue, 1: util.tol.magenta, 2: util.tol.teal} 2250 var catColor = c => { 2251 var idx = Object.keys(d.cats).indexOf(c) 2252 return catOverride[idx] ?? d.cats[c]?.color ?? util.gray[500] 2253 } 2254 2255 var row = sel.append('div.panel-row') 2256 row.append('div.tt-ylabel').text('Rank â') 2257 d.features.forEach(f => { 2258 var cell = row.append('div.tt-panel') 2259 var t = cell.append('div.fig-title') 2260 // panel titles get sentence case; f.en stays lowercase in the data (it's the literal word) 2261 t.append('span').text(f.en.charAt(0).toUpperCase() + f.en.slice(1)) 2262 t.append('span.arrow').text('â') 2263 t.append('span').text(f.fr) 2264 2265 var cols = cell.append('div.tt-cols') 2266 ;[['Encoder', f.encoder], ['Decoder', f.decoder]].forEach(([name, toks]) => { 2267 var box = cols.append('div.tt-box') 2268 var hdr = box.append('div.tt-hdr') 2269 hdr.append('span.tt-name').text(name) 2270 var fm = hdr.append('span.tt-formula') 2271 fm.append('span').text(' · J · W') 2272 fm.append('sub').text('U') 2273 box.append('div.tt-list').appendMany('div.tt-row', toks).each(function(t){ 2274 var s = d3.select(this) 2275 s.append('span.tt-rank').text(t.rank) 2276 var tx = util.ppToken(t.tok) 2277 s.append('span.tt-tok').text(tx).at({title: tx}) 2278 .st({color: catColor(t.cat)}) 2279 }) 2280 }) 2281 }) 2282 2283 var leg = sel.append('div.tt-legend') 2284 Object.entries(d.cats).forEach(([key, c], idx) => { 2285 var item = leg.append('div.legend-item') 2286 item.append('span.legend-sq').st({background: catOverride[idx] ?? c.color}) // lint-ok data-color-js: c.color overridden by catOverride 2287 item.append('span').text(c.label) 2288 }) 2289 2290 util.colorCaption(sel, { 2291 'multilingual concept': util.tol.blue, 2292 'French context': util.tol.magenta, 2293 'French word': util.tol.teal, 2294 }) 2295} 2296 2297window.init?.() 2298 2299// âââ public/attn-panel/init-attn-panel.js âââ 2300!function(){ 2301 var WEIGHT_TYPES = ['Q', 'K', 'V', 'O'] 2302 var QKVO_HDR = 'top jlens tokens' 2303 var PATTERN_HDR = '<span>attention pattern from query token</span><span>â</span>' 2304 2305 // white-at-zero tint so unattended tokens stay neutral; full weight = tol.orange 2306 function attnOrange(w){ return util.tint(util.tol.orange, 1 - Math.max(0, Math.min(1, w))) } 2307 2308 function renderHead(sel, h, ramp, isMain){ 2309 var ahp = sel.append('div.ahp.card') 2310 ahp.append('div.fig-title').text(h.label + ' Attention Head') 2311 var body = ahp.append('div.body') 2312 2313 var left = body.append('div') 2314 left.append('div.mlabel.pattern-right').html(PATTERN_HDR) 2315 left.append('div.snips').appendMany('div.snip', h.snippets).each(function(s){ 2316 d3.select(this).append('span.row').appendMany('span.tok', s.tokens) 2317 .text(d => d) 2318 .st({background: (_, i) => ramp(s.w[i])}) 2319 .classed('tok-hit', (_, i) => !isMain && i === s.q) 2320 }) 2321 2322 var right = body.append('div') 2323 right.append('div.mlabel').html(QKVO_HDR) 2324 right.append('div.qkvo').appendMany('div.col', WEIGHT_TYPES).each(function(wt){ 2325 var col = d3.select(this) 2326 col.append('div.wt').html(`W<sub>${wt}</sub>`) 2327 col.appendMany('div.t', h.qkvo[wt]).text(d => d).at({title: d => d}) 2328 }) 2329 } 2330 2331 async function render(sel, opts, keys, ramp, isMain){ 2332 sel.html('') 2333 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2334 .replace(/attn-panel-appendix\/$/, 'attn-panel/') 2335 var d = await util.getFile(base + 'heads.json') 2336 keys.forEach(k => renderHead(sel, d.heads[k], ramp, isMain)) 2337 } 2338 2339 window.initAttnPanel = opts => render(d3.select('.attn-panel'), opts, ['british'], attnOrange, true) 2340 window.initAttnPanelAppendix = opts => 2341 render(d3.select('.attn-panel-appendix'), opts, ['async', 'temperature', 'honorific'], attnOrange) 2342}() 2343 2344window.init?.() 2345 2346// âââ public/jlens-circuit-graph/init-jlens-circuit-graph.js âââ 2347window.initJlensCircuitGraph = async function({datapath}){ 2348 var sel = d3.select('.jlens-circuit-graph').html('') 2349 var d = await util.getFile(datapath + 'data.json') 2350 2351 var row = sel.append('div.panel-row') 2352 2353 // ââ left panel: curated attribution graph ââ 2354 var gp = row.append('div.graph-panel') 2355 gp.append('div.fig-title').text('J-lens attribution graph') 2356 gp.append('div.prompt-box').text(d.prompt) 2357 2358 var BW = 128, BH = 48, STACK = 3 2359 var X = col => 70 + col*185, Y = r => 28 + r*88
2360 var W = X(d3.max(d.graph.nodes, n => n.col)) + BW/2 + STACK*2.5 + 8 2361 var H = Y(d3.max(d.graph.nodes, n => n.row)) + BH/2 + STACK*2.5 + 6 2362 var svg = gp.append('svg').at({viewBox: `0 0 ${W} ${H}`}) 2363 2364 var byId = {} 2365 d.graph.nodes.forEach(n => byId[n.id] = {...n, x: X(n.col), y: Y(n.row)}) 2366 2367 svg.append('defs').append('marker') 2368 .at({id: 'jlens-cg-arrow', viewBox: '0 0 10 10', refX: 8.5, refY: 5, 2369 markerWidth: 12, markerHeight: 12, orient: 'auto', markerUnits: 'userSpaceOnUse'}) 2370 .append('path.arrowhead').at({d: 'M0,0L10,5L0,10z'}) 2371 2372 // trunk-style edges: every input to a box merges below it and enters 2373 // through a single arrowhead; tributaries join with rounded breaks 2374 var bySrc = d3.group(d.graph.edges, e => e.source) 2375 bySrc.forEach(es => { 2376 es.sort((a, b) => byId[a.target].x - byId[b.target].x) 2377 es.forEach((e, i) => e._sOff = (i - (es.length - 1)/2)*22) 2378 }) 2379 d3.group(d.graph.edges, e => e.target).forEach((es, tgt) => { 2380 var t = byId[tgt], r = 10 2381 var yStub = t.y + BH/2 + STACK*2, yMerge = yStub + 20 2382 svg.append('path.edge').at({d: `M${t.x},${yMerge - r} L${t.x},${yStub}`, markerEnd: 'url(#jlens-cg-arrow)'}) 2383 es.forEach(e => { 2384 var s = byId[e.source] 2385 var x0 = s.x + (e._sOff || 0), x1 = t.x, sx = x1 > x0 ? 1 : -1 2386 // exit from the source's top when it sits below the merge level, 2387 // from its bottom otherwise, so the line never crosses a box 2388 var fromTop = s.y - BH/2 > yMerge + 12 2389 var y0 = fromTop ? s.y - BH/2 : s.y + BH/2 + STACK*2 2390 var path = Math.abs(x1 - x0) < 6 2391 ? `M${x0},${y0} L${x1},${yMerge - r}` 2392 : `M${x0},${y0} L${x0},${yMerge + (fromTop ? r : -r)} Q${x0},${yMerge} ${x0 + sx*r},${yMerge} ` + 2393 `L${x1 - sx*r},${yMerge} Q${x1},${yMerge} ${x1},${yMerge - r}` 2394 svg.append('path.edge').at({d: path}) 2395 }) 2396 }) 2397 2398 // supernode boxes â stacked-card look, atom tokens in small gray underneath 2399 var boxFill = n => n.kind == 'reminder' ? util.tint(util.brand.manilla, 0.62) : util.brand.manilla 2400 var node = svg.appendMany('g.node', d.graph.nodes.map(n => byId[n.id])) 2401 .translate(n => [n.x - BW/2, n.y - BH/2]) 2402 .classed('reminder', n => n.kind == 'reminder') 2403 node.each(function(n){ 2404 var g = d3.select(this)
2405 d3.range(STACK, 0, -1).forEach(i => 2406 g.append('rect.stack').at({x: i*2.5, y: i*2.5, width: BW, height: BH, rx: 8}) 2407 .st({fill: boxFill(n), stroke: util.gray[500]})) 2408 g.append('rect.stack').at({width: BW, height: BH, rx: 8}) 2409 .st({fill: boxFill(n), stroke: util.gray[500]}) 2410 }) 2411 node.append('text.blabel').text(n => n.label) 2412 .at({x: BW/2, y: n => n.atoms.length ? BH/2 + 1 : BH/2 + 5, textAnchor: 'middle'}) 2413 node.filter(n => n.atoms.length).append('text.alabel') 2414 .text(n => n.atoms.join(' · ')).at({x: BW/2, y: BH/2 + 15, textAnchor: 'middle'}) 2415 2416 var leg = gp.append('div.legend-row') 2417 ;[['J-space supernode', util.brand.manilla], 2418 ['non-J-space remainder term', util.tint(util.brand.manilla, 0.62)]].forEach(([t, c]) => { 2419 var li = leg.append('div.legend-item') 2420 li.append('div.legend-sq').st({background: c, border: '1px solid ' + util.gray[500]}) 2421 li.append('span').text(t) 2422 }) 2423 var li = leg.append('div.legend-item') 2424 li.append('svg').at({width: 22, height: 10}) 2425 .append('path.edge').at({d: 'M0,5 L14,5', markerEnd: 'url(#jlens-cg-arrow)'}) 2426 li.append('span').text('attribution edge') 2427 2428 // ââ right panel: the original matplotlib swap plot, shipped as swaps.svg ââ 2429 // title rendered by the page (same .fig-title as the left panel) so the two 2430 // panel headers read as siblings; the svg itself carries no title 2431 var bp = row.append('div.bars-panel') 2432 bp.append('div.fig-title').text('Coordinate swaps: probability of the implied answer') 2433 bp.append('img.swaps-img').at({ 2434 src: datapath + 'swaps.svg', 2435 alt: 'Probability of the implied counterfactual answer before vs after each coordinate swap', 2436 }) 2437} 2438 2439window.init?.() 2440 2441// âââ public/attn-broadcast-reselect/init-attn-broadcast-reselect.js âââ 2442window.initAttnBroadcastReselect = async function(opts){ 2443 var sel = d3.select('.attn-broadcast-reselect').html('') 2444 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2445 var d = await util.getFile(base + 'data.json') 2446 2447 // axis extent from all x/y values (plus a small pad so labels fit). 2448 // Gain ratio can't fall below 1's left pad and MRR delta can't be negative 2449 // in any meaningful way â domains start at exactly 1 and 0; whiskers that 2450 // dip past those bounds are clamped to the boundary. 2451 var xExt = d3.extent(d.populations.flatMap(p => p.x)) 2452 var yExt = d3.extent(d.populations.flatMap(p => p.y)) 2453 var pad = 0.1 2454 var clampX = v => Math.max(1, v) 2455 var clampY = v => Math.max(0, v) 2456 2457 sel.append('div.fig-title').text('Broadcast-head gain and label preservation by population') 2458 2459 var c = d3.conventions({ 2460 sel: sel.append('div'), 2461 width: 460, height: 380, 2462 margin: {left: 56, right: 12, top: 18, bottom: 44}, 2463 }) 2464 c.x.domain([1, xExt[1] + pad]) 2465 c.y.domain([0, yExt[1] + 0.04]) 2466 c.xAxis.ticks(6); c.yAxis.ticks(6) 2467 c.drawAxis(); util.ggPlot(c) 2468 var yLabOffset = -5 2469 util.addAxisLabel(c, 'Gain[P] / gain[random] â', 'MRR[P] â MRR[random] â', '', 0, yLabOffset) 2470 2471 // ?tune=1 on the standalone page makes the y-axis label and title draggable (util.dragTune) 2472 var yLabG = d3.select(c.svg.select('.y .axis-label').node().parentNode) 2473 var titleSel = sel.select('.fig-title').st({position: 'relative'}) 2474 util.dragTune([ 2475 {sel: yLabG, key: 'y-label', 2476 get: () => ({x: yLabOffset - 30, y: c.height / 2}), 2477 set: p => yLabG.translate([p.x, p.y])}, 2478 {sel: titleSel, key: 'title', 2479 get: () => ({x: 0, y: 0}), 2480 set: p => titleSel.st({left: p.x + 'px', top: p.y + 'px'})}, 2481 ], 'attn-broadcast-reselect') 2482 2483 // baseline lines: x=1 (random gain), y=0 (random mrr) 2484 c.svg.append('line.ref-line') 2485 .at({x1: c.x(1), x2: c.x(1), y1: 0, y2: c.height}) 2486 c.svg.append('line.ref-line') 2487 .at({x1: 0, x2: c.width, y1: c.y(0), y2: c.y(0)}) 2488 2489 var relabel = {'Lens vectors (J)': 'J-lens vectors', 'J, rotated': 'J-lens rotated'} 2490 d.populations.forEach(p => { 2491 var hasIQR = p.x.length === 3 2492 var x50 = hasIQR ? p.x[1] : p.x[0], y50 = hasIQR ? p.y[1] : p.y[0] 2493 var mx = c.x(x50), my = c.y(y50) 2494 if (hasIQR) { 2495 c.svg.append('line.abr-whisker') 2496 .at({x1: c.x(clampX(p.x[0])), x2: c.x(clampX(p.x[2])), y1: my, y2: my, stroke: p.color}) 2497 c.svg.append('line.abr-whisker') 2498 .at({x1: mx, x2: mx, y1: c.y(clampY(p.y[0])), y2: c.y(clampY(p.y[2])), stroke: p.color}) 2499 } 2500 c.svg.append('circle.abr-mean') 2501 .at({cx: mx, cy: my, r: 6, fill: p.color})
2502 var a = p.anchor 2503 c.svg.append('text.abr-label') 2504 .text(relabel[p.label] || p.label) 2505 .translate([mx + a.dx, my + a.dy]) 2506 .st({textAnchor: a.ta, fill: p.color}) 2507 }) 2508 2509} 2510 2511window.init?.() 2512 2513// âââ public/ablation-bars/init-ablation-bars.js âââ 2514window.initAblationBars = async function({datapath}){ 2515 var sel = d3.select('.ablation-bars').html('') 2516 var d = await util.getFile(datapath + 'bars.json') 2517 var mm = s => s.replace(/\bsonnet\b/gi, 'Sonnet 4.5').replace(/\bhaiku\b/gi, 'Haiku 4.5') 2518 var trColor = tr => tr.hatch ? util.gray[400] 2519 : util.ablationColors[tr.name] ?? tr.color // lint-ok data-color-js: fallback 2520 2521 // title + legend in HTML so they stay visible while the wide SVG h-scrolls on mobile 2522 sel.append('div.fig-title').text(mm(d.title)) 2523 sel.append('div.legend-row').appendMany('div.legend-item', d.traces).each(function(tr){ 2524 d3.select(this).append('span.legend-sq').st({background: trColor(tr)}) 2525 d3.select(this).append('span').text(mm(tr.name)) 2526 }) 2527 2528 var nT = d.tasks.length 2529 var c = d3.conventions({ 2530 sel: sel.append('div.ab-scroll').append('div'), 2531 width: 1020, 2532 height: 320, 2533 margin: {left: 52, right: 8, top: 22, bottom: 88}, 2534 layers: 's', 2535 }) 2536 2537 var x0 = d3.scaleBand().domain(d3.range(nT)).range([0, c.width]) 2538 .paddingInner(0.16).paddingOuter(0.05) 2539 var x1 = d3.scaleBand().domain(d3.range(d.traces.length)) 2540 .range([0, x0.bandwidth()]).paddingInner(0.12) 2541 c.y.domain([0, 1]) 2542 2543 // axes â gray panel + white gridlines from the shared ggPlot helper 2544 c.x.domain([0, 1]) 2545 c.yAxis.ticks(6).tickFormat(d3.format('.0%')) 2546 c.drawAxis() 2547 c.svg.select('.x').remove() 2548 util.ggPlot(c) 2549 util.addAxisLabel(c, '', mm(d.ylabel), '', 0, -10) 2550 2551 // 2-line x-tick labels 2552 c.svg.append('g.x-ticks').translate([0, c.height + 6]) 2553 .appendMany('text.tick-label', d3.range(nT)) 2554 .translate(i => [x0(i) + x0.bandwidth()/2, 0]) 2555 .at({textAnchor: 'middle'}) 2556 .each(function(i){ 2557 var parts = d.display[i].split(/ (.+)/) 2558 d3.select(this).append('tspan').text(parts[0]).at({x: 0, dy: '0.9em'}) 2559 if (parts[1]) d3.select(this).append('tspan').text(parts[1]).at({x: 0, dy: '1.15em'}) 2560 }) 2561 2562 // bars + asymmetric error bars 2563 var g = c.svg.appendMany('g.task', d3.range(nT)).translate(i => [x0(i), 0]) 2564 d.traces.forEach((tr, j) => { 2565 var fill = trColor(tr) 2566 g.append('rect.bar').at({ 2567 x: x1(j), width: x1.bandwidth(), fill, 2568 y: i => c.y(tr.y[i]), height: i => c.height - c.y(tr.y[i]), 2569 2570 }) 2571 var ex = x1(j) + x1.bandwidth()/2, cap = Math.min(2, x1.bandwidth()*0.35) 2572 g.append('path.err').at({d: i => 2573 `M${ex},${c.y(tr.lo[i])} V${c.y(tr.hi[i])}` + 2574 ` M${ex-cap},${c.y(tr.lo[i])} h${2*cap}` + 2575 ` M${ex-cap},${c.y(tr.hi[i])} h${2*cap}`, 2576 stroke: util.gray[700], 2577 }) 2578 }) 2579 2580 // group brackets + 2-line captions 2581 var br = c.svg.append('g.brackets').translate([0, c.height]) 2582 var yTip = 38, yBar = 46, yTxt = 52 2583 d.groups.forEach(gr => { 2584 var x0p = x0(gr.x0) - x0.step()*0.03 2585 var x1p = x0(gr.x1) + x0.bandwidth() + x0.step()*0.03 2586 br.append('path').at({d: `M${x0p},${yTip} V${yBar} H${x1p} V${yTip}`}) 2587 var t = br.append('text').translate([(x0p+x1p)/2, yTxt]).at({textAnchor: 'middle'}) 2588 gr.caption.split('<br>').forEach((line, k) => 2589 t.append('tspan').text(line).at({x: 0, dy: k ? '1.15em' : '0.9em'})) 2590 }) 2591} 2592 2593window.init?.() 2594 2595// âââ public/ablation-examples/init-ablation-examples.js âââ 2596window.initAblationExamples = async function({datapath}){ 2597 var pctfirst = new URLSearchParams(location.search).has('pctfirst') 2598 var sel = d3.select('.ablation-examples').html('').classed('pctfirst', pctfirst) 2599 var chips = await util.getFile(datapath + 'chips.json') 2600 2601 // Linear KLâOranges; the colormap's 0-end is a warm cream (#fff5eb), so for 2602 // t<0.05 lerp from true white into Oranges(0.0465) â continuous at the join, 2603 // and tâ¥0.05 follows the Oranges hue path unchanged. 2604 var orng = d3.interpolateOranges, EPS = 0.05 2605 var ramp = t => t < EPS 2606 ? d3.interpolateRgb('#fff', orng(0.93*EPS))(t/EPS) 2607 : orng(0.93*t) 2608 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 2609 var allow = s => esc(s) 2610 .replaceAll('<b>','<b>').replaceAll('</b>','</b>') 2611 .replaceAll('<i>','<i>').replaceAll('</i>','</i>') 2612 .replaceAll('<br>','<br>') 2613 // marker-only tokens (â â½ âª) strip to '' â show the raw marker, not â 2614 var lbl = s => { var p = util.ppCell(s); return p === 'â ' ? s : p } 2615 2616 // Drop prediction rows whose token renders as the replacement glyph (U+FFFD) 2617 // or decodes to empty after marker stripping â they're partial/invalid byte 2618 // sequences from the tokenizer, not real predictions. 2619 var isJunk = t => /\uFFFD/.test(t) || util.ppToken(t).trim() === '' 2620 function pred(rows, n){ 2621 rows = (rows || []).filter(([t]) => !isJunk(t)) 2622 return d3.range(n).map(i => { 2623 var [t, p] = rows[i] || [' ', 0] 2624 var tx = esc(util.ppCell(t)), pct = rows[i] ? (p*100).toFixed(0)+'%' : '' 2625 return `<div class="row" title="${tx}"><div class="bar" style="width:${(p*100).toFixed(1)}%"></div>` + 2626 (pctfirst ? `<span class="p">${pct}</span><span class="t">${tx}</span>` 2627 : `<span class="t">${tx}</span><span class="p">${pct}</span>
2627`) + `</div>` 2628 }).join('') 2629 } 2630 2631 sel.append('div.chip-grid').appendMany('div.abl-chip', chips).each(function(D){ 2632 var chip = d3.select(this) 2633 var head = chip.append('div.ch-head') 2634 head.append('span.ch-title').text(D.title) 2635 .append('span.ch-tag').text(' ' + (D.tag || '')) 2636 if (D.analysis) head.append('span.ch-help').html( 2637 `Analysis <span class="q">?</span><span class="tip">${allow(D.analysis)}</span>`) 2638 if (D.anno) chip.append('div.ch-anno').html(allow(D.anno)) 2639 2640 var strip = chip.append('div.ch-strip.prompt-block') 2641 var elide = D.lo > 1 2642 if (elide) strip.append('span.ellipsis').text('[...]') 2643 var prevNl = false 2644 var spans = strip.appendMany('span.ct', d3.range(D.toks.length)) 2645 .each(function(i){ 2646 var t = D.toks[i], x = D.kl[i] / D.vmax 2647 if (i === 0 && elide) t = t.replace(/^\s+/, '') 2648 var disp = util.ppToken(t, true) // strip case/space markers, keep \n 2649 disp = disp.replace(/\n+/g, m => 'â'.repeat(m.length) + '\n') 2650 if (prevNl) disp = disp.replace(/^(â+)\n/, '$1') 2651 prevNl = t.endsWith('\n') 2652 // Marker-only tokens (â cap, â½ space) strip to '' â keep the marker 2653 // glyph so the span has width and stays clickable. 2654 if (!disp.trim()) disp = t || 'â ' 2655 var bg = ramp(x) 2656 d3.select(this).text(disp) 2657 .st({background: bg, color: d3.hsl(bg).l < 0.6 ? '#fff' : null}) 2658 }) 2659 2660 var sub = chip.append('div.ch-sub') 2661 var nSel = d3.max(D.sel, s => s.length), nPred = d3.max(D.base, b => b.length) 2662 var cSel = sub.append('div.ch-col') 2663 var hSel = cSel.append('h6.mlabel').html('Top Ablated <span class="pos"></span>') 2664 var dSel = cSel.append('div.c-sel') 2665 var cBase = sub.append('div.ch-col') 2666 cBase.append('h6.mlabel').html('Before <span class="unit">(prob)</span>') 2667 var dBase = cBase.append('div.c-base') 2668 var cAbl = sub.append('div.ch-col') 2669 var hAbl = cAbl.append('h6.mlabel').html('After <span class="kl"></span>') 2670 var dAbl = cAbl.append('div.c-abl') 2671 2672 var posLbl = hSel.select('.pos'), klLbl = hAbl.select('.kl') 2673 var cur = -1 2674 function show(i){ 2675 if (i === cur) return 2676 spans.classed('tok-hit', j => j === i) 2677 cur = i 2678 posLbl.text(`"${lbl(D.raw[i])}"`) 2679 var k = D.kl[i] 2680 klLbl.text('(KL=' + (k >= 10 ? k.toFixed(1) : k.toFixed(2)) + ')') 2681 var half = Math.ceil(nSel / 2) 2682 var sel_filtered = (D.sel[i] || []).filter(t => t == null || !isJunk(t)) 2683 dSel.html(d3.range(nSel).map(r => { 2684 var t = sel_filtered[r], col = r < half ? 1 : 2, row = (r % half) + 1 2685 var tx = t == null ? '' : esc(util.ppCell(t)) 2686 return `<div class="sel-row" title="${tx}" style="grid-column:${col};grid-row:${row}">` + 2687 `<span class="rk">${r+1}</span>${tx}</div>` 2688 }).join('')) 2689 dBase.html(pred(D.base[i], nPred)) 2690 dAbl.html(pred(D.abl[i], nPred)) 2691 } 2692 spans.on('mouseenter', (e, i) => show(i)) 2693 strip.on('mouseleave', () => show(D.default)) 2694 show(D.default) 2695 }) 2696 2697 var leg = sel.append('div.abl-legend') 2698 leg.append('span.leg-label').text('Token shading = KL after ablating that position') 2699 leg.append('span.leg-end').text('max') 2700 leg.append('svg').at({width: 120, height: 10}) 2701 .appendMany('rect', d3.range(40)) 2702 .at({x: i => i*3, width: 4, height: 10, fill: i => ramp(1 - i/39)}) 2703 leg.append('span.leg-end').text('0') 2704} 2705 2706window.init?.() 2707 2708// âââ public/ablation-strength/init-ablation-strength.js âââ 2709window.initAblationStrength = async ({datapath}) => { 2710 const sel = d3.select('.ablation-strength').html('') 2711 const d = await util.getFile(datapath + 'table.json') 2712 const pct = v => (v * 100).toFixed(0) + '%' 2713 2714 sel.append('div.fig-title').html( 2715 `Ablation strength <span class='sub'>multihop n=${d.n_items}, ` + 2716 `clean ${pct(d.clean_acc)} · PT n=${d.n_pos.toLocaleString()}</span>` 2717 ) 2718 const hasLayers = d.rows[0]?.layers != null 2719 const tbl = sel.append('table') 2720 tbl.append('thead').append('tr').html( 2721 '<th class="mlabel">ablation</th>' + (hasLayers ? '<th class="mlabel">layers ablated</th>' : '') + 2722 '<th class="mlabel">multihop acc</th><th class="mlabel">PT top-1 match</th>' 2723 ) 2724 tbl.append('tbody').appendMany('tr', d.rows).each(function (r) { 2725 const tr = d3.select(this).classed('control', r.name === 'random') 2726 var rc = util.ablationColors[r.name] ?? r.color // lint-ok data-color-js: fallback 2727 tr.append('td').html( 2728 `<span class='swatch' style='background:${rc}'></span>${r.name}` + 2729 (r.name === 'random' ? `<span class='control-note'>control</span>` : '') 2730 ) 2731 if (hasLayers) tr.append('td.layers').text(r.layers) 2732 tr.append('td').text(pct(r.mh_acc)) 2733 tr.append('td').text(pct(r.pt_acc)) 2734 }) 2735} 2736 2737window.init?.() 2738 2739// âââ public/blackmail-clamp/init-blackmail-clamp.js âââ 2740!function(){ 2741 2742async function loadData(opts){ 2743 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2744 return await util.getFile(base + 'data.json') 2745} 2746 2747var CONDS = [ 2748 {key: 'clean', label: 'No ablation', long: 'Baseline'}, 2749 {key: 'clamped_active', label: 'Ablated', long: 'After ablating eval awareness\nJ-lens vectors'}, 2750] 2751 2752var METRICS = [ 2753 {key: 'ea', lines: ['Verbalized', 'eval awareness'], label: 'Verbalized eval awareness', color: util.series[0], 2754 exLabel: 'Verbalized Eval Awareness'}, 2755 {key: 'bl', lines: ['Blackmail', 'rate'], label: 'Blackmail rate', color: util.series[5], 2756 exLabel: 'Attempted Blackmail'}, 2757] 2758 2759function drawPanel(sel, cond, d){ 2760 var prompts = [...new Set(d.per_prompt.map(r => r.prompt))].sort() 2761 var c = d3.conventions({ 2762 sel, 2763 width: 240, height: 230, 2764 margin: {left: 56, top: 38, right: 10, bottom: 32}, 2765 }) 2766 var x = d3.scaleBand().domain(METRICS.map(d => d.key)).range([0, c.width]).padding(0.32) 2767 c.y.domain([0, 1]) 2768 c.yAxis.ticks(4).tickFormat(d3.format('.0%')) 2769 c.drawAxis() 2770 c.svg.select('.x').remove() 2771 util.ggPlot(c) 2772 util.addAxisLabel(c, '', '') 2773 var lines = cond.long.split('\n') 2774 var tt = c.svg.append('g.axis').translate([c.width/2, -10 - (lines.length - 1)*14]) 2775 .append('text.axis-label.axis-title').at({textAnchor: 'middle'})
2776 lines.forEach((ln, i) => tt.append('tspan').text(ln).at({x: 0, dy: i ? 14 : 0})) 2777 2778 METRICS.forEach(metric => { 2779 var a = d.agg[cond.key] 2780 var p = a[metric.key] / a.n 2781 var ci = a[metric.key + '_ci'] 2782 var bx = x(metric.key), bw = x.bandwidth() 2783 var cx = bx + bw/2 2784 c.svg.append('rect') 2785 .at({x: bx, width: bw, y: c.y(p), height: c.height - c.y(p), fill: metric.color}) 2786 util.barWhisker(c.svg, {x: cx, lo: c.y(ci[0]), hi: c.y(ci[1]), stroke: util.gray[900]}) 2787 prompts.forEach((pr, j) => { 2788 var row = d.per_prompt.find(r => r.prompt === pr && r.condition === cond.key) 2789 if (!row) return 2790 var v = row[metric.key] 2791 var jitter = 0.45 * bw * ((j / Math.max(prompts.length - 1, 1)) - 0.5) 2792 c.svg.append('circle.jpt').datum({row, v, metric}).at({ 2793 cx: cx + jitter, cy: c.y(v / row.n), 2794 r: 2.2, fillOpacity: 0, stroke: '#000', strokeWidth: 1, 2795 }) 2796 }) 2797 var lab = c.svg.append('text') 2798 .at({x: cx, y: c.height + 14, textAnchor: 'middle'}) 2799 .st({fontSize: 'var(--fs-body)'}) 2800 metric.lines.forEach((ln, i) => 2801 lab.append('tspan').text(ln).at({x: cx, dy: i ? 12 : 0})) 2802 }) 2803} 2804 2805function drawCard(sel, ex, metric){ 2806 sel.append('div.ex-label').text(metric.exLabel) 2807 var body = sel.append('div.excerpt') 2808 var h = ex.hilite 2809 if (h && h[0] >= 0){ 2810 body.append('span').text(ex.text.slice(0, h[0])) 2811 body.append('mark').text(ex.text.slice(h[0], h[1])) 2812 .st({background: util.tint(metric.color, 0.85)}) 2813 body.append('span').text(ex.text.slice(h[1])) 2814 } else { 2815 body.text(ex.text) 2816 } 2817} 2818 2819window.initBlackmailClamp = async function(opts){ 2820 var sel = d3.select('.blackmail-clamp').html('') 2821 var d = await loadData(opts) 2822 // 2Ã2 grid: each row = (panel, excerpt card). Grid row height is driven 2823 // by the panel SVG; the card stretches to match, so the two columns stay 2824 // vertically aligned regardless of margin tweaks. 2825 var grid = sel.append('div.bc-row') 2826 CONDS.forEach((cond, i) => { 2827 drawPanel(grid.append('div.panel'), cond, d) 2828 drawCard(grid.append('div.card'), d.excerpts[i], METRICS[i]) 2829 }) 2830} 2831 2832}() 2833window.init?.() 2834 2835// âââ public/lensapp-panel/init-lensapp-panel.js âââ 2836window.initLensappPanel = async function(opts){ 2837 var slug = opts.slug 2838 var sel = d3.select(`.lensapp-panel[data-slug="${slug}"]`).html('') 2839 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2840 var d = await util.getFile(base + slug + '.json') 2841 2842 // Same orange ramp as feature-examples; text flips to white once bg is dark. 2843 var orange = t => d3.interpolateOranges(0.1 + 0.55*t) 2844 function exRow(host, ex, gmax, cls){ 2845 var row = host.append('div.exrow.prompt-block' + (cls ? '.' + cls : '')) 2846 .at({title: ex.chunks.map(([s]) => s).join('')}) 2847 if (ex.lo > 0) row.append('span.ell').text('â¦') 2848 ex.chunks.forEach(([s, a]) => { 2849 var sp = row.append('span.t').text(s) 2850 if (a > 0){ 2851 var t = Math.max(0, Math.min(1, a / (gmax || 1))) 2852 var bg = orange(t) 2853 sp.st({background: bg}).classed('dark', d3.hsl(bg).l < 0.6) 2854 } 2855 }) 2856 if (ex.hi < ex.n) row.append('span.ell').text('â¦') 2857 } 2858 2859 var titleCase = s => s.replace(/\w+/g, (w, i) => 2860 i && /^(a|an|and|or|the|in|of|on|to|for)$/i.test(w) ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1)) 2861 2862 var card = sel.append('div.card') 2863 card.append('div.fig-title').text(titleCase(d.label)) 2864 var cols = card.append('div.cols') 2865 2866 var left = cols.append('div') 2867 left.append('div.mlabel').text('J-lens top tokens') 2868 left.append('div.toks').appendMany('span', d.top_tokens).text(t => t) 2869 left.append('div.mlabel.gap').text('Activating examples') 2870 d.examples.forEach(ex => exRow(left, ex, d.gmax)) 2871 if (d.prompt_acts){ 2872 left.append('div.mlabel.gap').text('â¦on an agentic misalignment eval (right)') 2873 exRow(left, d.prompt_acts, d.prompt_acts.gmax, 'prompt-row') 2874 } 2875 2876 var right = cols.append('div') 2877 right.append('div.mlabel').text('Steering prompt') 2878 right.append('div.prompt').text(d.prompt) 2879 right.append('div.mlabel.gap').text('Steering completions') 2880 var roll = right.append('div.roll')
2881 d.rollouts.forEach(r => { 2882 var lab = roll.append('div.rlab') 2883 lab.append('div').text(r.label) 2884 if (r.ann) lab.append('div.ann').text(r.ann) 2885 roll.append('div.rbracket') 2886 var rtxt = roll.append('div.rtxt').text(r.text) 2887 if (r.trunc !== false) rtxt.append('span.trunc').text(' [â¦]') 2888 }) 2889} 2890 2891window.init?.() 2892 2893// âââ public/reflection-training/init-reflection-training.js âââ 2894window.initReflectionTraining = async function(opts){ 2895 var sel = d3.select(opts?.sel || '.reflection-training').html('') 2896 var base = (opts?.datapath || './').replace(/\/?$/, '/') 2897 var d = await util.getFile(base + 'reflection.json') 2898 // Per-eval wording lives in the baked JSON (reflection-fabrication vs 2899 // reflection-deception reuse this init); fall back to the fabrication text. 2900 var L = d.labels || {} 2901 // 'no substantive report' is baked as a light gray that washes into the 2902 // ggPlot panel bg â darken render-side so the sliver and legend stay visible 2903 d.split.classes.forEach(cl => { if (cl.cls == 'no substantive report') cl.color = util.gray[700] }) 2904 2905 // haiku â util.modelColors.haiku per the shared modelâcolor map; orange is 2906 // otherwise reserved for activation shading. reflection-finetune is a new 2907 // experiment family â use the jlens series color (blue). 2908 var modelColor = {haiku: util.modelColors.haiku, reflection: util.tol.blue} 2909 var modelLabel = {haiku: ['Claude', 'Haiku 4.5'], reflection: ['reflection', 'fine-tune']} 2910 var H = 310 2911 2912 var figTitle = opts?.figTitle ?? L.figTitle 2913 if (figTitle) sel.append('div.fig-title').text(figTitle) 2914 var row = sel.append('div.panel-row') 2915 2916 // ââ A: dishonesty headline ââ 2917 var pA = row.append('div.panel') 2918 pA.append('div.fig-title').text('A: ' + (L.titleA ?? 'Reflection training reduces dishonesty')) 2919 pA.append('div.psub').text(L.subA ?? 2920 `Dishonesty score on the eval (27 cases à 5 reps; n=${d.eval.haiku.n}, n=${d.eval.reflection.n}).`) 2921 var ms = ['haiku', 'reflection'] 2922 var ymax = d3.max(ms, m => d.eval[m].hi) + 0.04 2923 var cA = d3.conventions({ 2924 sel: pA.append('div'), width: 244, height: H, 2925 margin: {left: 52, top: 18, right: 6, bottom: 38}, 2926 }) 2927 var xb = d3.scaleBand().domain(ms).range([0, cA.width]).padding(0.3) 2928 cA.y.domain([0, ymax]) 2929 cA.xAxis.tickFormat(m => '').tickSize(0) 2930 cA.yAxis.ticks(5) 2931 cA.drawAxis(); util.ggPlot(cA) 2932 cA.svg.selectAll('.x .tick path').remove() 2933 util.addAxisLabel(cA, '', L.yA ?? 'Dishonesty score â', '', 0, -8) 2934 ms.forEach(m => { 2935 var e = d.eval[m], cx = xb(m) + xb.bandwidth()/2 2936 cA.svg.append('rect').at({x: xb(m), width: xb.bandwidth(), y: cA.y(e.dishonesty), 2937 height: cA.height - cA.y(e.dishonesty), fill: modelColor[m]}) // lint-ok data-color-js: shared modelâcolor map 2938 util.barWhisker(cA.svg, {x: cx, lo: cA.y(e.lo), hi: cA.y(e.hi)}) 2939 cA.svg.append('text.num').text(e.dishonesty.toFixed(2)) 2940 .at({x: cx, y: cA.y(e.hi) - 6, textAnchor: 'middle', fontSize: 'var(--fs-small)', fill: util.gray[700]}) 2941 modelLabel[m].forEach((s, i) => cA.svg.append('text').text(s) 2942 .at({x: cx, y: cA.height + 14 + i*12, textAnchor: 'middle', fontSize: 'var(--fs-small)', fill: util.gray[600]})) 2943 }) 2944 2945 // ââ B: spike-rate token table ââ 2946 var nTok = d.tokens.n_show ?? 18 2947 var rows = d.tokens.rows.slice(0, nTok) 2948 var pB = row.append('div.panel') 2949 pB.append('div.fig-title').text('B: ' + (L.titleB ?? 'Workspace content shifts with finetuning')) 2950 pB.append('div.psub').text( 2951 (L.subB ?? 2952 `Top {n} tokens by increase in J-lens top-${d.tokens.topk} ` + 2953 `spike rate over the last ${d.tokens.n_tail} prompt tokens.` 2954 ).replace('{n}', nTok)) 2955 var tab = pB.append('table.rktab') 2956 var hr = tab.append('thead') 2957 hr.append('tr').html( 2958 '<th rowspan=2>token</th><th colspan=3>% prompts</th><th colspan=3>% positions</th>') 2959 hr.append('tr').html( 2960 '<th>haiku</th><th></th><th>refl.</th><th>haiku</th><th></th><th>refl.</th>') 2961 tab.append('tbody').appendMany('tr', rows).html(r => 2962 `<td class='tk'>${util.ppToken?.(r.tok) ?? r.tok}</td>` + 2963 `<td>${(r.base_pcase*100).toFixed(0)}</td><td class='arr'>â</td>` + 2964 `<td>${(r.refl_pcase*100).toFixed(0)}</td>` + 2965 `<td>${(r.base_ppos*100).toFixed(1)}</td><td class='arr'>â</td>` + 2966 `<td>${(r.refl_ppos*100).toFixed(1)}</td>`) 2967 2968 // ââ C: grader split ââ 2969 var pC = row.append('div.panel') 2970 pC.append('div.fig-title').text('C: ' + (L.titleC ?? 'Workspace ablation reverts behavior')) 2971 pC.append('div.psub').text(L.subC ?? 2972 `Ablate the top-10 ethics directions per position at workspace layers.`) 2973 var split = pC.append('div.split-wrap') 2974 var n = d.split.conds.length 2975 var cC = d3.conventions({ 2976 sel: split.append('div'), width: 290, height: H - 58, 2977 margin: {left: 52, top: 30, right: 6, bottom: 38}, 2978 }) 2979 var xs = d3.scaleBand().domain(d3.range(n)).range([0, cC.width]).padding(0.18) 2980 cC.y.domain([0, 1]) 2981 cC.xAxis.tickValues(d3.range(n)).tickFormat(() => '').tickSize(0) 2982 cC.yAxis.ticks(5).tickFormat(d3.format('.0%')) 2983 cC.drawAxis(); util.ggPlot(cC) 2984 cC.svg.selectAll('.x .tick path').remove() 2985 util.addAxisLabel(cC, '', 'Fraction â', L.axisC ?? 'Mean dishonesty', 0, -8, -8) 2986 var cum = new Array(n).fill(0)
2987 d.split.classes.forEach(cl => { 2988 cl.frac.forEach((f, i) => { 2989 cC.svg.append('rect').at({x: xs(i), width: xs.bandwidth(), 2990 y: cC.y(cum[i] + f), height: cC.y(cum[i]) - cC.y(cum[i] + f), fill: cl.color}) // lint-ok data-color-js: ordinal goodâbad ramp 2991 cum[i] += f 2992 }) 2993 }) 2994 d.split.conds.forEach((c, i) => { 2995 var cx = xs(i) + xs.bandwidth()/2 2996 cC.svg.append('text.num').text(d.split.dishonesty[i].toFixed(2)) 2997 .at({x: cx, y: -6, textAnchor: 'middle', fontSize: 'var(--fs-small)', fill: util.gray[700]}) 2998 ;[c.model, c.cond].forEach((s, j) => cC.svg.append('text').text(s) 2999 .at({x: cx, y: cC.height + 14 + j*12, textAnchor: 'middle', fontSize: 'var(--fs-small)', fill: util.gray[600]})) 3000 }) 3001 var leg = split.append('div.split-legend') 3002 // cl.color: bake-side semantic greenâred ordinal ramp; lint-ok data-color-js 3003 leg.appendMany('div.lrow', d.split.classes).html(cl => 3004 `<span class='legend-sq' style='background:${cl.color}'></span>` + // lint-ok data-color-js 3005 `${cl.cls} <span class='rub'>(${cl.dishonesty == null ? 'â' : cl.dishonesty.toFixed(1)})</span>`) 3006} 3007 3008window.init?.() 3009 3010// âââ public/dn-tracecond/init-dn-tracecond.js âââ 3011window.initDnTracecond = async function(opts){ 3012 var sel = d3.select('.dn-tracecond').html('') 3013 var base = (opts?.datapath || './').replace(/\/?$/, '/') 3014 var d = await util.getFile(base + 'data.json') 3015 3016 // Same condition palette as the selfreport/ablation figures: gray 3017 // baseline, red J-space ablation (nofinal = darker red), blue-tint 3018 // controls. Any condition not in the data is dropped. 3019 var allConds = [ 3020 {key: 'clean', label: 'baseline', color: util.gray[400]}, 3021 {key: 'ablate', label: 'J-space ablated', color: util.tol.red}, 3022 {key: 'nofinal', label: 'J-space ablated (final pos. spared)', color: '#7a1f1f'}, 3023 {key: 'ortho', label: 'non-J-space control', color: util.tint(util.tol.blue, 0.25)}, 3024 {key: 'random', label: 'random control', color: util.tint(util.tol.blue, 0.55)}, 3025 ] 3026 var params = new URLSearchParams(location.search) 3027 var hide = (params.get('hide') || '').split(',') 3028 var CONDS = allConds.filter(c => 3029 d.curves[d.trials[0]][c.key]?.length && !hide.includes(c.key)) 3030 3031 // Shared y across the three panels so the eye can compare. 3032 var lo = d3.min(d.trials, tt => d3.min(CONDS, 3033 cn => d3.min(d.curves[tt][cn.key], p => p.mean - p.se))) 3034 var xi = Object.fromEntries(d.gaps.map((g, j) => [g, j])) 3035 3036 var row = sel.append('div.tc-row') 3037 drawSetup(row.append('div.tc-setup'), d.example) 3038 d.trials.forEach((t, i) => drawPanel(row.append('div.tc-panel'), t, i)) 3039 drawLegend(sel) 3040 3041 function drawSetup(sel, ex){ 3042 sel.append('div.fig-title').text('A') 3043 sel.append('div.mlabel').text('Demonstrations') 3044 var lines = sel.append('div.tc-lines.demo') 3045 ex.demos.forEach(([cue, fill, out]) => addLine(lines, cue, fill, out, false)) 3046 lines.append('div.tc-ellip').text('â¦') 3047 sel.append('div.mlabel').text('Test') 3048 var test = sel.append('div.tc-lines') 3049 addLine(test, ex.test[0], ex.test[1], ex.test[2], true) 3050 var brace = sel.append('div.tc-brace') 3051 brace.append('span.bspan').text('cue') 3052 brace.append('span.bspan.fill').text(`k filler words`) 3053 brace.append('span.bspan').text('outcome') 3054 3055 function addLine(lines, cue, fill, out, isTest){ 3056 var ln = lines.append('div.tc-line') 3057 ln.append('span.tc-cue').text(cue) 3058 fill.forEach(w => ln.append('span.tc-fill').text(w)) 3059 ln.append('span.tc-out').classed('test', isTest).text(isTest ? '?' : out) 3060 } 3061 } 3062 3063 function drawPanel(sel, t, i){ 3064 sel.append('div.fig-title') 3065 .text(String.fromCharCode(66 + i) + ' ' + t + ' demos / pair') 3066 var pw = d.trials.length > 3 ? 168 : 220 3067 var c = d3.conventions({ 3068 sel: sel.append('div'), width: pw, height: 240, 3069 margin: {left: i ? 14 : 50, right: 8, top: 6, bottom: 36}, 3070 }) 3071 c.x.domain([0, d.gaps.length - 1]) 3072 c.y.domain([lo * 1.05, 0]).clamp(true) 3073 c.xAxis.tickValues(d3.range(d.gaps.length)).tickFormat(j => d.gaps[j]) 3074 c.yAxis.ticks(5) 3075 c.drawAxis() 3076 if (i) c.svg.select('.y').selectAll('text').remove() 3077 util.ggPlot(c)
3078 util.addAxisLabel(c, 'gap k (filler words)', i ? '' : 'log P(correct outcome)') 3079 3080 var px = p => c.x(xi[p.k]) 3081 var line = d3.line().x(px).y(p => c.y(p.mean)) 3082 var area = d3.area().x(px) 3083 .y0(p => c.y(p.mean - p.se)).y1(p => c.y(p.mean + p.se)) 3084 CONDS.forEach(cn => { 3085 var pts = d.curves[t][cn.key] 3086 c.svg.append('path').at({d: area(pts), fill: cn.color, opacity: 0.18}) 3087 c.svg.append('path') 3088 .at({d: line(pts), fill: 'none', stroke: cn.color, strokeWidth: 2.2}) 3089 c.svg.appendMany('circle', pts) 3090 .at({r: 2.6, fill: cn.color}) 3091 .translate(p => [px(p), c.y(p.mean)]) 3092 }) 3093 } 3094 3095 function drawLegend(sel){ 3096 var leg = sel.append('div.legend-row').st({justifyContent: 'center'}) 3097 CONDS.forEach(cn => { 3098 var item = leg.append('div.legend-item') 3099 item.append('span.swatch').st({background: cn.color}) 3100 item.append('span').text(cn.label) 3101 }) 3102 } 3103} 3104 3105window.init?.() 3106 3107// âââ public/dn-exclusion/init-dn-exclusion.js âââ 3108window.initDnExclusion = async function(opts){ 3109 var sel = d3.select('.dn-exclusion').html('') 3110 var base = (opts?.datapath || './').replace(/\/?$/, '/') 3111 var d = await util.getFile(base + 'data.json') 3112 3113 // Bar palette: gray baseline, red primed-concept early, lighter red 3114 // primed-concept late, blue other-concept matched-norm control. 3115 var COLORS = { 3116 'clean': util.gray[400], 3117 'target_p913': util.tol.red, 3118 'target_p1822': '#d99494', 3119 'sibling_p913': util.tint(util.tol.blue, 0.45), 3120 } 3121 3122 var row = sel.append('div.ex-row') 3123 drawSetup(row.append('div.ex-setup'), d.example) 3124 ;[['incl', 'B', 'Name the primed answer'], 3125 ['excl', 'C', 'Avoid the primed answer']].forEach(([task, letter, title], i) => { 3126 var bars = d.tasks[task] 3127 var bw = 104 3128 var psel = row.append('div.ex-panel') 3129 psel.append('div.fig-title').text(letter + ' ' + title) 3130 var c = d3.conventions({ 3131 sel: psel.append('div'), 3132 width: bw * bars.length, height: 278, 3133 margin: {left: 46, right: 8, top: 6, bottom: 38}, 3134 }) 3135 c.x.domain([-0.6, bars.length - 0.4]) 3136 c.y.domain([0, 1]) 3137 c.xAxis.tickValues(d3.range(bars.length)).tickFormat(j => '') 3138 c.yAxis.ticks(5) 3139 c.drawAxis() 3140 util.ggPlot(c) 3141 util.addAxisLabel(c, '', 'P(primed answer)') 3142 // multi-line tick labels 3143 c.svg.select('.x').selectAll('.tick').append('foreignObject') 3144 .at({x: -bw/2 + 3, y: 4, width: bw - 6, height: 36}) 3145 .append('xhtml:div').at({class: 'ex-xtick'}).text((_, j) => bars[j].label) 3146 3147 var bf = 0.45 3148 bars.forEach((b, j) => { 3149 c.svg.append('rect').at({ 3150 x: c.x(j - bf/2), width: c.x(j + bf/2) - c.x(j - bf/2), 3151 y: c.y(b.mean), height: c.y(0) - c.y(b.mean), 3152 fill: COLORS[b.key], 3153 }) 3154 // 95% CI 3155 c.svg.append('path').at({ 3156 d: `M${c.x(j)},${c.y(b.mean - b.ci)} V${c.y(b.mean + b.ci)} ` + 3157 `M${c.x(j) - 4},${c.y(b.mean - b.ci)} h8 ` + 3158 `M${c.x(j) - 4},${c.y(b.mean + b.ci)} h8`, 3159 stroke: '#333', strokeWidth: 1.2, fill: 'none', 3160 }) 3161 // per-item dots 3162 var n = b.vals.length 3163 c.svg.appendMany('circle', b.vals).at({ 3164 r: 1.8, fill: 'none', stroke: '#444', strokeWidth: 0.5, 3165 }).translate((v, k) => [ 3166 c.x(j - bf*0.4 + bf*0.8 * k/(n-1)), c.y(v), 3167 ]) 3168 }) 3169 }) 3170 3171 function drawSetup(sel, ex){ 3172 sel.append('div.fig-title').text('A') 3173 sel.append('div.mlabel').text('Priming sentence') 3174 sel.append('div.ex-box.prime-box').text(`Here is a sentence: "${ex.sentence}"`) 3175 var imp = sel.append('div.ex-implies') 3176 imp.append('div.ex-arrow').text('â') 3177 imp.append('div.ex-implies-label').text('implies') 3178 var pr = imp.append('div.ex-prime-row') 3179 pr.append('div.ex-prime').text(ex.prime) 3180 pr.append('span.ex-anno').text('â Ablate this J-lens vector') 3181 var qrow = sel.append('div.ex-qrow') 3182 ;[['Name', ex.name_q], ['Avoid', ex.avoid_q]].forEach(([h, q]) => { 3183 var col = qrow.append('div.ex-qcol') 3184 col.append('div.mlabel').text(h) 3185 col.append('div.ex-box').text(q) 3186 }) 3187 sel.append('div.mlabel').text('Prefilled response') 3188 var pf = sel.append('div.ex-box.ex-prefill') 3189 pf.append('span').text(ex.prefill) 3190 pf.append('span.ex-meas').text('[sample next token]') 3191 } 3192} 3193 3194window.init?.() 3195 3196// âââ public/refl-training-examples/init-refl-training-examples.js âââ 3197window.initReflTrainingExamples = async function(opts){ 3198 var sel = d3.select('.refl-training-examples').html('') 3199 var base = (opts?.datapath || './').replace(/\/?$/, '/') 3200 var d = await util.getFile(base + 'data.json') 3201 3202 var panels = sel.append('div.rte-panels') 3203 .appendMany('div.rte-panel', d.examples) 3204 3205 var titles = panels.append('div.fig-title') 3206 titles.append('span.rte-chip').text((e, i) => String.fromCharCode(65 + i)) 3207 titles.append('span').text(e => e.label.replace(/^\([a-z]\)\s*/i, '')) 3208 3209 panels.append('div.rte-ctx').text(e => e.context) 3210 3211 // Preceding transcript: one role-boxed turn per snippet entry. Bracketed 3212 // tool-call summaries and thinking-tag lines render as muted italic; the 3213 // remaining lines are the literal code/output in mono. 3214 panels.append('div.rte-snippet') 3215 .appendMany('div.rte-turn', e => e.snippet || []) 3216 .each(function(t){ 3217 var turn = d3.select(this).classed('rte-turn-' + t.role.toLowerCase(), true) 3218 turn.append('div.rte-role').text(t.role === 'tool' ? 'Tool result' : t.role) 3219 turn.append('pre.rte-text').selectAll('span') 3220 .data(t.text.split('\n')).enter().append('span') 3221 .attr('class', s => (s.trimStart().startsWith('â¨') || /thinking>/.test(s)) ? 'rte-summary' : null) 3222 .text((s, i) => (i ? '\n' : '') + s) 3223 }) 3224 3225 // Reflection probe = the Human turn that elicits the reflection. 3226 var probe = panels.append('div.rte-turn.rte-turn-human') 3227 probe.append('div.rte-role').text('Human') 3228 probe.append('div.rte-probe').text(e => e.probe) 3229 3230 // Reflection = the Assistant's response, shown as prose (sans), not mono. 3231 var refl = panels.append('div.rte-turn.rte-turn-assistant.rte-reflbox') 3232 refl.append('div.rte-role').text('Assistant') 3233 refl.append('div.rte-refl') 3234 .appendMany('span', e => e.reflection) 3235 .attr('class', s => s === 'â¦' ? 'rte-ellip' : null) 3236 .text(s => s === 'â¦' ? ' [â¦] ' : s + ' ') 3237} 3238 3239window.init?.() 3240 3241// âââ public/modulation-readout/init-modulation-readout.js âââ 3242window.initModulationReadout = async (opts) => { 3243 var sel = d3.select('.modulation-readout').html('')
3244 var d = await util.getFile(opts.datapath + 'modulation.json') 3245 var n = d.panels.length 3246 var pp = t => util.ppCell?.(t) ?? (util.ppToken(t) || t) 3247 // raw byte-fallback tokens ("\xe2\x80"â¦) and zero-width chars aren't readable â skip them 3248 var isJunk = t => /\\x[0-9a-fA-F]{2}/.test(t) || !util.ppToken(t).replace(/[\u200b\u200c\u200d\ufeff]/g, '').trim() 3249 var cleanTopk = lys => lys.map(l => { 3250 var topk = l.topk.filter(k => !isJunk(k.t)) 3251 return Object.assign({}, l, {topk, top1: topk[0]?.t ?? l.top1}) 3252 }) 3253 3254 var esc = s => String(s).replace(/[&<>"']/g, c => 3255 ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])) 3256 3257 // back-compat: old data ships only p.layers at one position 3258 d.panels.forEach(p => { 3259 if (p.positions) return 3260 p.positions = [p.layers]; p.tokens = [p.key_tok_str]; p.key_pos = 0 3261 var i = p.display_prompt.lastIndexOf(p.key_tok_str) 3262 p.display_prefix = p.display_prompt.slice(0, i) 3263 p.display_suffix = p.display_prompt.slice(i + p.key_tok_str.length) 3264 }) 3265 3266 // clip the linebreak panel's long prompt after "â¦on this continent," so the 3267 // three prompt blocks end up about the same height; the trailing newline is 3268 // kept (it's the lens-read position). Handles both bake shapes â marker 3269 // display_parts and the all-tokens layout. 3270 d.panels.forEach(p => { 3271 var cut 3272 if (p.display_parts) { 3273 cut = p.display_parts.findIndex(s => s.includes('on this continent,')) 3274 if (cut < 0) return 3275 p.display_parts = p.display_parts.slice(0, cut + 1) 3276 } else { 3277 cut = p.tokens.findIndex(t => /continent/.test(t)) 3278 if (cut < 0) return 3279 while (cut < p.tokens.length - 1 && !/\n/.test(p.tokens[cut])) cut++ 3280 p.display_suffix = '' 3281 } 3282 p.tokens = p.tokens.slice(0, cut + 1) 3283 p.positions = p.positions.slice(0, cut + 1) 3284 if (p.hl_range) p.hl_range = [Math.min(p.hl_range[0], cut), Math.min(p.hl_range[1], cut + 1)] 3285 p.key_pos = Math.min(p.key_pos, cut) 3286 }) 3287 3288 d.panels.forEach(p => { p.positions = p.positions.map(cleanTopk) }) 3289 // drop the leading turn-marker newlines so the prompt box doesn't open with 3290 // blank lines above "Human:". Token 0 stays in place so position indexing holds. 3291 d.panels.forEach(p => { 3292 if (p.display_prefix) p.display_prefix = p.display_prefix.replace(/^\n+/, '') 3293 else if (p.tokens?.[0]) p.tokens[0] = p.tokens[0].replace(/^\n+/, '') 3294 }) 3295 3296 function spanify(s, p) { 3297 var out = esc(s) 3298 if (p.focus_clause) { 3299 var fc = esc(p.focus_clause) 3300 out = out.replace(fc, `<span class="hl">${fc}</span>`) 3301 } 3302 return out.replace(/(â¨[^â©]*â©|â¦)/g, m => `<span class="elide">${m}</span>`) 3303 } 3304 3305 // Per-panel default layer (display-label â pub index via DISPLAY_LAYERS). 3306 var DEFAULT_DISPLAY = [79, 88, 71] 3307 d.panels.forEach((p, pi) => { 3308 var disp = DEFAULT_DISPLAY[pi] 3309 if (disp == null) return 3310 var pub = util.DISPLAY_LAYERS.indexOf(disp) 3311 if (pub >= 0) p.default_layer = pub 3312 }) 3313 3314 var row = sel.append('div.mod-row').st({'--ncol': n}) 3315 d.panels.forEach(p => row.append('div.fig-title').text(p.title)) 3316 3317 // panels 0/1 share a teacher-forced response â sync token clicks end-aligned, 3318 // but only where the two prompts actually overlap (same token at the aligned 3319 // slot). Clicks outside the overlap, or on any other panel, just select locally. 3320 var SYNC = [0, 1] 3321 var states = [], renders = [], tokSels = [], aliasSels = [] 3322 3323 function syncTok(from, i){ 3324 if (!SYNC.includes(from)) return 3325 var off = d.panels[from].tokens.length - 1 - i 3326 SYNC.forEach(ti => { 3327 if (ti === from) return 3328 var j = d.panels[ti].tokens.length - 1 - off 3329 if (j < 0 || !d.panels[ti].positions[j]) return 3330 if (d.panels[ti].tokens[j] !== d.panels[from].tokens[i]) return 3331 states[ti].pos = j; states[ti].src = null; renders[ti]() 3332 }) 3333 } 3334 function pickTok(pi, i, src){ 3335 states[pi].pos = i; states[pi].src = src; renders[pi](); syncTok(pi, i) 3336 } 3337 3338 d.panels.forEach((p, pi) => { 3339 var prompt = row.append('div.display-prompt.prompt-block') 3340 aliasSels[pi] = null 3341 3342 if (p.display_parts){ 3343 // scattered scan tokens (linebreak âµ markers): text parts interleaved 3344 // with one clickable marker per lens position 3345 var nodes = []
3346 p.display_parts.forEach((part, i) => { 3347 prompt.append('span.nodata').html(spanify(part, p)) 3348 if (i < p.tokens.length) nodes.push( 3349 prompt.append('span.ptok').text(p.tokens[i]) 3350 .on('click', () => pickTok(pi, i, null)).node()) 3351 }) 3352 tokSels[pi] = d3.selectAll(nodes) 3353 return 3354 } 3355 3356 // human-turn copy of the response sentence (inside the Write "..." quote): 3357 // clickable aliases that select the matching response position 3358 var sent = p.tokens.slice(2).join('') 3359 var qi = sent.length > 1 ? p.display_prefix.indexOf(sent.trim()) : -1 3360 if (qi >= 0){ 3361 prompt.append('span.nodata').html(spanify(p.display_prefix.slice(0, qi), p)) 3362 var cursor = qi, anodes = [] 3363 p.tokens.forEach((t, ti) => { 3364 if (ti < 2) return 3365 var len = (ti === 2 ? t.replace(/^ /, '') : t).length 3366 anodes.push(prompt.append('span.ptok-alias') 3367 .text(p.display_prefix.slice(cursor, cursor + len)) 3368 .datum(ti) 3369 .on('click', (e, ti2) => pickTok(pi, ti2, 'human')).node()) 3370 cursor += len 3371 }) 3372 aliasSels[pi] = d3.selectAll(anodes) 3373 prompt.append('span.nodata').html(spanify(p.display_prefix.slice(cursor), p)) 3374 } else { 3375 prompt.append('span.nodata').html(spanify(p.display_prefix, p)) 3376 } 3377 3378 var hr = p.hl_range 3379 tokSels[pi] = prompt.appendMany('span.ptok', d3.range(p.tokens.length)) 3380 .text(i => p.tokens[i]) 3381 .classed('hl', i => hr && i >= hr[0] && i < hr[1]) 3382 .classed('nodata', i => !p.positions[i]?.length) 3383 .on('click', (e, i) => { if (p.positions[i]?.length) pickTok(pi, i, null) }) 3384 prompt.append('span.nodata').html(spanify(p.display_suffix, p)) 3385 }) 3386 3387 d.panels.forEach((p, pi) => { 3388 var pair = row.append('div.lenspair') 3389 var swoop = pair.append('svg.swoop') 3390 var tbl = pair.append('table.lens') 3391 tbl.append('tr').html('<th>Layer</th><th>J-lens top-1</th>') 3392 var keyRows = p.positions[p.key_pos] 3393 var trs = tbl.appendMany('tr.ly', keyRows) 3394 trs.append('th').text(l => util.layerLabel(l.layer)) 3395 var tds = trs.append('td') 3396 3397 // English gloss for the (rare) non-Latin top-1; shown inline so the panel 3398 // tables stay aligned across all three families. 3399 var GLOSS = {'дÑм': 'think'} 3400 var glossOf = t => { var k = pp(t).trim(); return /[A-Za-z]/.test(k) ? null : GLOSS[k] } 3401 3402 var detail = pair.append('div.detail') 3403 var dlyr = detail.append('div.dlyr') 3404 var dtoks = detail.append('div') 3405 3406 var st = states[pi] = {pos: p.key_pos, layer: p.default_layer, src: null} 3407 3408 function render(){ 3409 var lys = p.positions[st.pos] 3410 tokSels[pi].classed('tok-hit', (_, i) => i === st.pos) 3411 // outline the human-turn copy only when it was the click origin 3412 if (aliasSels[pi]) aliasSels[pi].classed('on', ti => st.src === 'human' && ti === st.pos) 3413 trs.data(lys) 3414 tds.data(lys).each(function(l){ 3415 var s = d3.select(this).text(pp(l.top1)) 3416 var g = glossOf(l.top1) 3417 if (g) s.append('span.gloss').text(` (${g})`) 3418 }) 3419 var l = lys.find(x => x.layer === st.layer) || lys[0] 3420 st.layer = l.layer 3421 trs.classed('on', x => x.layer === st.layer) 3422 dlyr.text(`Layer ${util.layerLabel(st.layer)}`) 3423 // show as many top-k rows as there are layer rows so the detail box and 3424 // the lens table come out the same height (no dead white space) 3425 dtoks.html('').appendMany('div.tok', l.topk.slice(0, lys.length)) 3426 .text(k => pp(k.t)) 3427 .st({'--p': k => k.p + '%'}) 3428 requestAnimationFrame(drawSwoop) 3429 } 3430 renders[pi] = render 3431 3432 function drawSwoop(){ 3433 var pr = pair.node().getBoundingClientRect() 3434 var tr = trs.filter('.on').node().getBoundingClientRect() 3435 var dr = dlyr.node().getBoundingClientRect() 3436 // start 1px inside the selected row's outline so the connector reads as 3437 // flush (the swoop now paints above the table â see style.css z-index) 3438 var x0 = Math.round(tr.right - pr.left) - 1 3439 var y0 = Math.round(tr.top + tr.height/2 - pr.top) 3440 var x1 = Math.round(dr.left - pr.left) - 1 3441 var y1 = Math.round(dr.top + dr.height/2 - pr.top) 3442 var mx = Math.round((x0 + x1) / 2) 3443 swoop.html('').append('path').at({ 3444 d: `M${x0},${y0} C${mx},${y0} ${mx},${y1} ${x1},${y1}`, 3445 fill: 'none', stroke: 'var(--text)', strokeWidth: 1}) 3446 } 3447 3448 // Layer-row hover is per-panel only (token clicks still sync via SYNC) s
3448o 3449 // each panel can sit at its own DEFAULT_DISPLAY layer. 3450 trs.on('mouseenter', (e, l) => { st.layer = l.layer; render() }) 3451 render() 3452 }) 3453} 3454 3455window.init?.() 3456 3457// âââ public/modulation-lines/init-modulation-lines.js âââ 3458window.initModulationLines = async ({datapath}) => { 3459 const sel = d3.select('.modulation-lines').html('') 3460 const d = await util.getFile(datapath + 'lines.json') 3461 d.panels.forEach(p => p.series.forEach((s, i) => s.color = util.series[i % util.series.length])) 3462 3463 sel.append('div.head').append('div.fig-title').text(d.title) 3464 3465 const innerW = 300, H = 320 3466 const row = sel.append('div.panels') 3467 // when the panels stack (narrow container), give every panel the wide margin 3468 // + y-axis title so the stacked plot areas line up vertically 3469 const stacked = getComputedStyle(row.node()).flexDirection === 'column' 3470 3471 // labelPos: [x, y, lines] â px in panel-0 plot coords, hand-placed via ?tune=1 drag (copy panel offsets back here) 3472 var labelPos = { 3473 'Category instance': [11, 32, ['Category', 'instance']], 3474 'Math expression': [143, 39, ['Math', 'expression']], 3475 'Line width': [142, 174, ['Line', 'width']], 3476 } 3477 3478 // ?tune=1 on the standalone page makes these labels draggable (util.dragTune) 3479 var tuneKeys = {'Category instance': 'cat-instance', 'Math expression': 'math-expr', 'Line width': 'line-width'} 3480 var tuneItems = [] 3481 3482 d.panels.forEach((p, pi) => { 3483 // The first panel carries the rotated y label; its left margin has to 3484 // cover the label's text height plus the addAxisLabel offset so the 3485 // figure's overflow-x: clip doesn't shave it. 3486 const M = pi === 0 || stacked 3487 ? {left: 56, right: 16, top: 28, bottom: 36} 3488 : {left: 42, right: 16, top: 28, bottom: 36} 3489 const c = d3.conventions({ 3490 sel: row.append('div'), width: innerW, 3491 height: H - M.top - M.bottom, margin: M, 3492 }) 3493 c.x = d3.scalePoint().domain(d.models).range([0, c.width]).padding(0.28) 3494 c.xAxis.scale(c.x) 3495 c.y.domain([0, 1]) 3496 c.yAxis.tickFormat(d3.format('.0%')).tickValues(d3.range(0, 1.01, 0.2)) 3497 3498 c.drawAxis() 3499 util.ggPlot(c) 3500 c.svg.selectAll('.x .tick path').remove() 3501 util.addAxisLabel(c, '', pi === 0 || stacked ? d.ylabel : '', p.label, 0, -14) 3502 3503 const line = d3.line().x((_, i) => c.x(d.models[i])).y(v => c.y(v)) 3504 p.series.forEach(s => { 3505 c.svg.append('path.series.line-series').at({d: line(s.y), stroke: s.color, 'data-series': s.label}) 3506 c.svg.appendMany('circle.pt', s.y) 3507 .at({r: 3.5, fill: s.color}) 3508 .translate((v, i) => [c.x(d.models[i]), c.y(v)]) 3509 }) 3510 3511 if (pi === 0) p.series.forEach(s => { 3512 var [x, y, lines] = labelPos[s.label] 3513 var t = c.svg.append('text.dlabel') 3514 .translate([x, y]) 3515 .at({textAnchor: 'start', 'data-series': s.label}) 3516 .st({fill: s.color, paintOrder: 'stroke', stroke: '#EAECED', strokeWidth: 1, strokeLinejoin: 'round'}) // lint-ok hex-js: ggPlot bg halo 3517 lines.forEach((ln, li) => t.append('tspan').text(ln).at({x: 0, dy: li ? '1.15em' : '.32em'})) 3518 tuneItems.push({sel: t, key: tuneKeys[s.label], 3519 get: () => ({x, y}), 3520 set: q => t.translate([q.x, q.y])}) 3521 }) 3522 }) 3523 util.bindSeriesHover(sel) 3524 util.dragTune(tuneItems, 'modulation-lines') 3525} 3526 3527window.init?.() 3528 3529// âââ public/modulation-prompts/init-modulation-prompts.js âââ 3530window.initModulationPrompts = async ({datapath}) => { 3531 const sel = d3.select('.modulation-prompts').html('') 3532 const d = await util.getFile(datapath + 'data.json') 3533 if (!d3.select('.tooltip').size()) d3.select('body').append('div.tooltip.tooltip-hidden') 3534 const tt = d3.select('.tooltip') 3535 3536 sel.append('div.head').append('div.fig-title').text(d.title) 3537 3538 const leg = sel.append('div.legend-row') 3539 d.groups.forEach(g => { 3540 const it = leg.append('div.legend-item') 3541 it.append('span.legend-sq').st({background: g.color}) 3542 it.append('span').text(g.label) 3543 }) 3544 3545 const innerW = 264, innerH = 180 3546 const grid = sel.append('div.mp-scroll').append('div.grid') 3547 // deterministic per-dot offset so the figure is stable across reloads 3548 const jitter = (i, n) => n <= 1 ? 0 : (i / (n - 1) - 0.5) * 0.62 3549
3550 d.panels.forEach(p => { 3551 const isLeft = p.mi === 0, isTop = p.fi === 0 3552 const fam = d.families[p.fi] 3553 const M = { 3554 left: isLeft ? 60 : 28, 3555 right: 8, 3556 top: isTop ? 22 : 6, 3557 bottom: p.fi === d.families.length - 1 ? 30 : 18, 3558 } 3559 const c = d3.conventions({ 3560 sel: grid.append('div.cell'), 3561 width: innerW, height: innerH, margin: M, layers: 's', 3562 }) 3563 const x = d3.scaleBand().domain(d3.range(d.groups.length)) 3564 .range([0, c.width]).paddingInner(0.28).paddingOuter(0.14) 3565 c.y.domain([0, 1]) 3566 c.yAxis.ticks(5).tickFormat(d3.format('.0%')) 3567 c.x.domain([0, 1]) 3568 c.drawAxis() 3569 c.svg.select('.x').remove() 3570 util.ggPlot(c) 3571 3572 // column header (model) on top row; row header (family + metric) on left col 3573 if (isTop) c.svg.append('text.col-head') 3574 .at({x: c.width/2, y: -8, textAnchor: 'middle'}).text(d.models[p.mi]) 3575 if (isLeft) util.addAxisLabel(c, '', `${fam.label} (${fam.metric})`, '', 0, -14) 3576 3577 // bars (group means) â focus/dismissal heights are the modulation-lines values 3578 p.groups.forEach((g, gi) => { 3579 if (g.mean == null) return 3580 c.svg.append('rect.bar').at({ 3581 x: x(gi), width: x.bandwidth(), 3582 y: c.y(g.mean), height: c.height - c.y(g.mean), 3583 fill: d.groups[gi].color, fillOpacity: 0.45, 3584 }) 3585 }) 3586 3587 // dots (per-phrasing) with hover tooltip 3588 p.groups.forEach((g, gi) => { 3589 const cx = x(gi) + x.bandwidth()/2 3590 c.svg.appendMany('circle.dot', g.dots) 3591 .at({r: 3.2, fill: d.groups[gi].color, stroke: 'white', strokeWidth: 0.8}) 3592 .translate((dot, i) => [cx + jitter(i, g.dots.length) * x.bandwidth(), c.y(dot.y)]) 3593 .on('mouseover', (e, dot) => { 3594 tt.classed('tooltip-hidden', false).html('') 3595 tt.append('div.mp-tt-var').text(dot.v) 3596 tt.append('div.mp-tt-text').text(`"${dot.t || ''}"`) 3597 tt.append('div.mp-tt-val').text(d3.format('.1%')(dot.y)) 3598 }) 3599 .on('mousemove', e => { 3600 var bb = tt.node().getBoundingClientRect() 3601 tt.st({ 3602 left: d3.clamp(8, e.clientX - bb.width/2, innerWidth - bb.width - 8) + 'px', 3603 top: (e.clientY + 14) + 'px', 3604 }) 3605 }) 3606 .on('mouseleave', () => tt.classed('tooltip-hidden', true)) 3607 }) 3608 3609 // none floor 3610 if (p.none != null) c.svg.append('line.floor').at({ 3611 x1: 0, x2: c.width, y1: c.y(p.none), y2: c.y(p.none), 3612 }) 3613 3614 // x ticks (group labels) on bottom row only 3615 if (p.fi === d.families.length - 1) 3616 c.svg.append('g.x-ticks').translate([0, c.height + 6]) 3617 .appendMany('text.tick-label', d.groups) 3618 .at({textAnchor: 'middle', x: (g, i) => x(i) + x.bandwidth()/2, dy: '0.9em'}) 3619 .text(g => g.label) 3620 }) 3621} 3622 3623window.init?.() 3624 3625// âââ public/latent-patching/init-latent-patching.js âââ 3626window.initLatentPatching = async function(opts){ 3627 var sel = d3.select(opts?.sel || '.latent-patching').html('') 3628 var base = (opts?.datapath || './').replace(/\/?$/, '/') 3629 var d = await util.getFile(base + 'data.json') 3630 3631 // Match flex-generalization-example: orange = the swapped/patched concept. 3632 var CLEAN = util.gray[500], PATCHED = util.tol.orange 3633 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 3634 var allow = s => esc(s) 3635 .replaceAll('<b>','<b>').replaceAll('</b>','</b>') 3636 .replaceAll('<i>','<i>').replaceAll('</i>','</i>') 3637 .replaceAll('<br>','<br>') 3638 3639 var panels = sel.appendMany('div.lp-panel', d.panels) 3640 3641 // ââ left: title + prompt + swap ââ 3642 var left = panels.append('div.lp-left') 3643 left.append('div.fig-title').text(p => p.title) 3644 var prompt = left.append('div.lp-prompt') 3645 // Mark the read position (last prompt token) â that's where the top-5 3646 // log-probs in the tables are measured. Last word, or last char for the 3647 // no-space Chinese prompt. 3648 var splitHit = p => p.prompt.match(/^([\s\S]*?)([A-Za-z0-9]+|.)$/).slice(1) 3649 prompt.append('span').text(p => splitHit(p)[0]) 3650 prompt.append('span.tok-hit').text(p => splitHit(p)[1]) 3651 left.filter(p => p.gloss).append('div.lp-gloss').text(p => p.gloss) 3652 left.append('div.lp-swaps').html(p => 3653 'J-lens coordinate swap: ' + p.swap.map(s => 3654 `${esc(s[0])} â <span style="color:${PATCHED}">${esc(s[1])}</span>`).join(', ')) 3655 3656 // ââ right: clean / patched bar tables + annotation ââ 3657 var right = panels.append('div.lp-right') 3658 function col(side, label, color, barBg){ 3659 var c = right.append('div.lp-col.bar-table').classed(side, true) 3660 c.append('h6.mlabel').html((p, i) => 3661 `<span class="swatch" style="background:${color}"></span> ${esc(label)}` + 3662 (i == 0 && side == 'clean' ? ` <span class="hdr-sub">(log prob)</span>` : '')) 3663 c.appendMany('div.bar-row', p => { 3664 var rows = p[side], maxP = d3.max([...p.clean, ...p.patched], e => Math.exp(e.lp)) 3665 return rows.map(e => ({...e, w: Math.exp(e.lp) / maxP * 100})) 3666 }).at({title: e => util.ppCell(e.t).trim()}) 3667 .html(e => `<div class="bar" style="width:${e.w.toFixed(1)}%;background:${barBg}"></div>` + 3668 `<span class="t">${esc(util.ppCell(e.t).trim())}</span><span class="val">${e.lp.toFixed(2)}</span>`) 3669 } 3670 col('clean', 'Clean', CLEAN, util.gray[200]) 3671 col('patched', 'Swapped', PATCHED, util.tint(PATCHED, 0.65)) 3672 right.append('div.lp-anno').html(p => allow(p.anno)) 3673 3674 if (sel.classed('latent-patching')) util.colorCaption(sel, {clean: util.gray[600], swapped: PATCHED}) 3675} 3676 3677window.init?.() 3678 3679// âââ public/multihop-swap-success/init-multihop-swap-success.js âââ 3680window.initMultihopSwapSuccess = async function(opts){ 3681 var sel = d3.select('.multihop-swap-success').html('') 3682 var base = (opts?.datapath || './').replace(/\/?$/, '/') 3683 var d = await util.getFile(base + 'data.json') 3684 3685 // when the panel-row stacks (container < 768px), each panel redraws to fill 3686 // the available width so both align and keep full-size labels
3687 var fig = sel.node().closest('figure') 3688 var cs = fig && getComputedStyle(fig) 3689 var avail = fig ? fig.clientWidth - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0) : 0 3690 var stackW = avail && avail < 768 ? avail : 0 3691 3692 // Side-by-side at native size fits inside class='wide'; .panel-row's 3693 // generic 768px @container threshold would otherwise stack the panels and 3694 // trigger a fitMount downscale (which is what made everything tiny). 3695 var row = sel.append('div.panel-row').st({flexDirection: 'row'}) 3696 drawBars(row.append('div.bars'), d) 3697 drawOnset(row.append('div.onset'), d.onset) 3698 3699 function drawBars(sel, d){ 3700 var c = d3.conventions({ 3701 sel, width: stackW ? stackW - 72 : 280, height: 280, 3702 margin: {left: 58, right: 14, top: 32, bottom: 32}, 3703 }) 3704 var x = d3.scaleBand().domain(d.models.map(m => m.label)) 3705 .range([0, c.width]).paddingInner(0.35).paddingOuter(0.18) 3706 c.y.domain([0, 1]) 3707 c.yAxis.ticks(5).tickFormat(d3.format('.0%')) 3708 c.drawAxis() 3709 c.svg.select('.x').remove() 3710 util.ggPlot(c) 3711 util.addAxisLabel(c, '', 'Swap success rate â', 'How often the swap works', 0, -8) 3712 3713 c.svg.appendMany('text.xlab', d.models) 3714 .text(m => m.label).translate(m => [x(m.label) + x.bandwidth()/2, c.height + 16]) 3715 .at({textAnchor: 'middle'}) 3716 3717 var g = c.svg.appendMany('g.mbar', d.models) 3718 .translate(m => [x(m.label), 0]) 3719 g.append('rect').at({ 3720 width: x.bandwidth(), y: m => c.y(m.p), height: m => c.height - c.y(m.p), 3721 fill: util.gray[400], 3722 }) 3723 g.each(function(m){ 3724 util.barWhisker(d3.select(this), {x: x.bandwidth()/2, lo: c.y(m.p - m.se), hi: c.y(m.p + m.se)}) 3725 }) 3726 g.append('text.val').text(m => d3.format('.0%')(m.p)) 3727 .translate(m => [x.bandwidth()/2, c.y(m.p + m.se) - 5]) 3728 .at({textAnchor: 'middle'}) 3729 } 3730 3731 function drawOnset(sel, on){ 3732 var kinds = [ 3733 {key: 'intermediate', label: 'Intermediate swap', color: util.tol.blue}, 3734 {key: 'target', label: 'Answer swap', color: util.tol.red}, 3735 ] 3736 var c = d3.conventions({ 3737 sel, width: stackW ? stackW - 66 : 420, height: 280, 3738 margin: {left: 58, right: 8, top: 32, bottom: 32}, 3739 }) 3740 var lo = d3.min(kinds, k => d3.min(on.l, (_, i) => on.mean[k.key][i] - on.se[k.key][i])) 3741 var hi = d3.max(kinds, k => d3.max(on.l, (_, i) => on.mean[k.key][i] + on.se[k.key][i])) 3742 c.x.domain(d3.extent(on.l)) 3743 c.y.domain([lo, hi]).nice() 3744 c.xAxis.ticks(Math.ceil(on.l.length/2)).tickFormat(util.layerLabel) 3745 c.yAxis.ticks(5) 3746 c.drawAxis() 3747 util.ggPlot(c) 3748 util.addAxisLabel(c, 'Swap window upper layer (reindexed) â', 'Î log-prob (expected â original) â', 'When the swap takes effect', 0, -8) 3749 3750 var line = d3.line().x((_, i) => c.x(on.l[i])).y(v => c.y(v)) 3751 var area = d3.area().x((_, i) => c.x(on.l[i])) 3752 .y0((d, i) => c.y(d.m - d.s)).y1((d, i) => c.y(d.m + d.s)) 3753 kinds.forEach(k => { 3754 var band = on.l.map((_, i) => ({m: on.mean[k.key][i], s: on.se[k.key][i]})) 3755 c.svg.append('path.se') 3756 .at({d: area(band), fill: k.color, stroke: 'none'}) 3757 .st({opacity: 0.18}) 3758 }) 3759 kinds.forEach(k => { 3760 c.svg.append('path.mean') 3761 .at({d: line(on.mean[k.key]), fill: 'none', stroke: k.color, strokeWidth: 3}) 3762 }) 3763 3764 var labelPos = { // [anchorIdx, dx, dy, textAnchor] â hand-placed 3765 intermediate: [6, -8, -34, 'end'], 3766 target: [8, 0, 36, 'middle'], 3767 } 3768 kinds.forEach(k => { 3769 var [ai, dx, dy, anchor] = labelPos[k.key] 3770 c.svg.append('text.dlabel').text(k.label) 3771 .translate([c.x(on.l[ai]) + dx, c.y(on.mean[k.key][ai]) + dy]) 3772 .at({dy: '.32em', textAnchor: anchor}) 3773 .st({fill: k.color, paintOrder: 'stroke', stroke: '#EAECED', strokeWidth: 2, strokeLinejoin: 'round'}) // lint-ok hex-js: ggPlot bg halo 3774 }) 3775 } 3776} 3777 3778window.init?.() 3779 3780// âââ public/repeat-switch/init-repeat-switch.js âââ 3781window.initRepeatSwitch = async function(opts){ 3782 var sel = d3.select('.repeat-switch').html('') 3783 var base = (opts?.datapath || './').replace(/\/?$/, '/') 3784 var d = await util.getFile(base + 'data.json') 3785 3786 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 3787 var bandLbl = `L${util.layerLabel(d.layers[0])}â${util.layerLabel(d.layers[1])}` 3788 3789 sel.append('div.fig-title').text('Swapping the decoded strategy between prompts flips the modelâs choice') 3790 3791 // val = log-prob. Probability bars only on Model output rows (sh
3791owBars) â the 3792 // lens-median probs are mostly <1% and render as slivers. 3793 function barTable(host, entries, mark, cls, showBars){ 3794 var t = host.append('div.rs-bars.bar-table' + (cls || '')) 3795 t.appendMany('div.bar-row', entries).at({title: e => util.ppCell(e.t).trim()}).html(e => { 3796 var c = mark ? (e.abl ? ' abl' : e.don ? ' don' : '') : '' 3797 var bar = showBars ? `<div class="bar" style="width:${(Math.exp(e.lp)*100).toFixed(1)}%"></div>` : '' 3798 return bar + 3799 `<span class="t${c}">${esc(util.ppCell(e.t).trim())}</span>` + 3800 `<span class="val">${e.lp.toFixed(2)}</span>` 3801 }) 3802 } 3803 3804 var grid = sel.append('div.rs-grid') 3805 d.panels.forEach((p, i) => { 3806 var col = grid.append('div.rs-panel') 3807 col.append('div.mlabel.rs-panel-label').text(`${p.label} prompt`) 3808 var body = esc(p.promptBody) 3809 var dot = body.lastIndexOf('.') 3810 body = body.slice(0, dot) + '<span class="tok-hit">.</span>' + body.slice(dot + 1) 3811 col.append('div.rs-prompt.prompt-block').html( 3812 `<span class="prompt-role">Human:</span> ${body}\n\n` + 3813 `<span class="prompt-role">Assistant:</span> <span class="prompt-gen">${esc(p.cleanChoice)}</span>`) 3814 3815 var tbl = col.append('div.rs-table') 3816 tbl.append('div.rs-corner') 3817 tbl.append('div.rs-colhead').text('Original') 3818 tbl.append('div.rs-colhead').text('After plan swap') 3819 3820 tbl.append('div.rs-rowlab').html(`<span class="rs-lab1">J-lens decode at "."</span><br>${bandLbl} median`) 3821 barTable(tbl, p.lensBefore, true) 3822 barTable(tbl, p.lensAfter, true) 3823 3824 tbl.append('div.rs-rowlab.rs-sep').text('Model output') 3825 barTable(tbl, p.outBefore, false, '.rs-sep', true) 3826 barTable(tbl, p.outAfter, false, '.rs-sep', true) 3827 }) 3828 3829 // Legend lives in the gdoc figcaption â color the cue words to match the 3830 // .abl (red strikethrough) / .don (teal) token styling in the tables. 3831 util.colorCaption(sel, {removed: 'var(--tol-red)', installed: 'var(--tol-teal)'}) 3832} 3833 3834window.init?.() 3835 3836// âââ public/verbal-report/init-verbal-report.js âââ 3837window.initVerbalReport = async function(opts){ 3838 var sel = d3.select('.verbal-report').html('') 3839 var base = (opts?.datapath || './').replace(/\/?$/, '/') 3840 var d = await util.getFile(base + 'data.json') 3841 3842 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 3843 var ttSel = d3.select('.tooltip') 3844 3845 var main = sel.append('div.vr-main') 3846 3847 // ââ example block (left) ââ 3848 var ex = d.example 3849 var top = main.append('div.vr-example') 3850 top.append('div.fig-title').text(`"Think of a ${ex.category}"`) 3851 top.append('div.vr-prompt.prompt-block').html( 3852 `<span class="prompt-role">Human:</span> ${esc(ex.promptBody)}\n\n` + 3853 `<span class="prompt-role">Assistant</span><span class="tok-hit">:</span>`) 3854 3855 function barTable(host, label, entries, isLens){ 3856 var t = host.append('div.vr-bars.bar-table').classed('vr-lens', isLens) 3857 var h = t.append('h6') 3858 h.append('span.swatch').st({background: isLens ? util.tol.orange : util.gray[400], marginRight: 5}) 3859 h.append('span').text(label) 3860 var max = d3.max(entries, e => Math.exp(e.lp)) 3861 var bg = isLens ? util.tint(util.tol.orange, 0.7) : 'var(--gray-200)' 3862 t.appendMany('div.bar-row', entries).at({title: e => util.ppCell(e.t).trim()}).html(e => 3863 `<div class="bar" style="width:${(Math.exp(e.lp)/max*100).toFixed(1)}%;background:${bg}"></div>` + 3864 `<span class="t">${esc(util.ppCell(e.t).trim())}</span><span class="val">${e.lp.toFixed(2)}</span>`) 3865 } 3866 top.append('div.mlabel.vr-section').text('Original') 3867 var c1 = top.append('div.vr-cols') 3868 barTable(c1, `J-lens ${ex.rhoLayer}`, ex.lensTop, true) 3869 barTable(c1, "Model's next-token logits", ex.outTop) 3870 top.append('div.mlabel.vr-section').text(`Swapping "${ex.swapAnswer}" â "${ex.swapTarget}"`) 3871 var c2 = top.append('div.vr-cols') 3872 barTable(c2, `J-lens ${ex.rhoLayer}`, ex.lensTopAfter, true) 3873 barTable(c2, "Model's next-token logits", ex.outTopAfter) 3874 3875 // ââ quant block (right column) ââ 3876 var q = d.quant 3877 var bot = main.append('div.vr-quant') 3878 bot.append('div.fig-title').text(`Across ${q.categories?.length || 14} categories`) 3879 3880 // Ï at three layers 3881 var cL = d3.conventions({ 3882 sel: bot.append('div'), width: 200, height: 150, 3883 margin: {left: 50, right: 8, top: 2, bottom: 28}, 3884 }) 3885 var xL = d3.scaleBand().domain(q.rhos.map(r =>
3885 r.L)).range([0, cL.width]) 3886 .paddingInner(0.35).paddingOuter(0.18) 3887 cL.y.domain([0, 1]).clamp(true) 3888 cL.yAxis.ticks(5) 3889 cL.drawAxis() 3890 cL.svg.select('.x').remove() 3891 util.ggPlot(cL) 3892 util.addAxisLabel(cL, 'Layer â', '') 3893 var yLab = cL.svg.select('.y').append('g').translate([-38, cL.height/2]) 3894 .append('text.axis-label').at({textAnchor: 'middle', transform: 'rotate(-90)'}) 3895 yLab.append('tspan').text('Lens vs output rank').at({x: 0}) 3896 yLab.append('tspan').text('Spearman Ï â').at({x: 0, dy: '1.1em'}) 3897 cL.svg.appendMany('text.xlab', q.rhos) 3898 .text(r => r.L).translate(r => [xL(r.L) + xL.bandwidth()/2, cL.height + 14]) 3899 .at({textAnchor: 'middle'}) 3900 cL.svg.appendMany('rect.vr-med', q.rhos) 3901 .at({x: r => xL(r.L), width: xL.bandwidth(), y: r => cL.y(d3.median(r.rhos)), 3902 height: r => cL.height - cL.y(d3.median(r.rhos)), fill: util.gray[400]}) 3903 var rng = d3.randomUniform.source(d3.randomLcg(0))(-0.18, 0.18) 3904 var trunc = s => s && s.length > 200 ? s.slice(0, 200) + 'â¦' : (s || '') 3905 function appendTopk(host, lt){ 3906 if (!lt?.tokens?.length) return 3907 var toks = host.append('div.toks') 3908 lt.tokens.forEach((t, ti) => { 3909 var pp = util.ppToken?.(t) ?? t 3910 toks.append('span.tok').text(pp).classed('hl', lt.hit_idx?.includes(ti)) 3911 }) 3912 } 3913 function placeTT(e){ 3914 var bb = ttSel.node().getBoundingClientRect() 3915 ttSel.st({left: d3.clamp(20, e.clientX - bb.width/2, innerWidth - bb.width - 20) + 'px', 3916 top: (innerHeight > e.clientY + 20 + bb.height ? e.clientY + 20 : e.clientY - bb.height - 20) + 'px'}) 3917 } 3918 q.rhos.forEach(r => { 3919 var x0 = xL(r.L), w = xL.bandwidth() 3920 var cats = r.cats || [], prompts = r.prompts || [], topks = r.lens_topk || [] 3921 cL.svg.appendMany('circle.jpt', r.rhos.map((v, i) => ({v, L: r.L, cat: cats[i], pr: prompts[i], lt: topks[i]}))) 3922 .at({cx: () => x0 + w/2 + rng()*w, cy: o => cL.y(o.v), r: 2, 3923 fillOpacity: 0, stroke: '#000', strokeWidth: 1}) 3924 }) 3925 var lastL 3926 util.nearestHover(cL.svg, cL.svg.selectAll('circle.jpt'), { 3927 radius: 40, 3928 onHover: (o, el, e) => { 3929 if (el !== lastL){ 3930 lastL = el 3931 ttSel.classed('tooltip-hidden', false).html('') 3932 ttSel.append('div').html(`<b>${o.L}</b> · ${o.cat || ''} · Ï = ${o.v}`) 3933 if (o.pr) ttSel.append('div.prompt-block').text(trunc(o.pr)) 3934 appendTopk(ttSel, o.lt) 3935 } 3936 placeTT(e) 3937 }, 3938 onLeave: () => { lastL = null; ttSel.classed('tooltip-hidden', true) }, 3939 }) 3940 3941 // beforeâafter slope (log y, rank=1 at top) 3942 var cR = d3.conventions({ 3943 sel: bot.append('div'), width: 200, height: 150, 3944 margin: {left: 44, right: 12, top: 2, bottom: 28}, 3945 }) 3946 var xR = d3.scalePoint().domain(['before', 'after']).range([0, cR.width]).padding(0.3) 3947 cR.y = d3.scaleLog().domain([1, 1000]).range([0, cR.height]).clamp(true) 3948 cR.yAxis.scale(cR.y).tickValues([1, 10, 100, 1000]).tickFormat(d => d >= 1000 ? '1k+' : d) 3949 cR.drawAxis() 3950 cR.svg.select('.x').remove() 3951 util.ggPlot(cR) 3952 util.addAxisLabel(cR, '', 'Candidate output rank â') 3953 cR.svg.appendMany('text.xlab', ['No swap', 'After swap']) 3954 .text(s => s).translate((s,i) => [xR(i ? 'after' : 'before'), cR.height + 14]) 3955 .at({textAnchor: 'middle'}) 3956 var rngR = d3.randomUniform.source(d3.randomLcg(1))(-0.5, 0.5) 3957 var jw = xR.step() * 0.14 3958 q.pairs.forEach(p => { p.j0 = rngR()*jw; p.j1 = rngR()*jw }) 3959 var slopes = cR.svg.appendMany('path.slope', q.pairs) 3960 .at({d: p => `M${xR('before') + p.j0},${cR.y(p.before)} L${xR('after') + p.j1},${cR.y(p.after)}`, 3961 stroke: util.gray[600], strokeWidth: 0.6, opacity: 0.35}) 3962 .st({mixBlendMode: 'multiply'}).nodes() 3963 var cB = cR.svg.appendMany('circle.jpt', q.pairs.map((p, i) => ({p, i, side: 'before'}))) 3964 .at({cx: o => xR('before') + o.p.j0, cy: o => cR.y(o.p.before), r: 2, 3965 fillOpacity: 0, stroke: '#000', strokeWidth: 1}).nodes() 3966 var cA = cR.svg.appendMany('circle.jpt', q.pairs.map((p, i) => ({p, i, side: 'after'}))) 3967 .at({cx: o => xR('after') + o.p.j1, cy: o => cR.y(o.p.after), r: 2, 3968 fillOpacity: 0, stroke: '#000', strokeWidth: 1}).nodes() 3969 var lastR, hovEls = [] 3970 var clearHov = () =>
3970 { hovEls.forEach(n => n.classList.remove('hov')); hovEls = [] } 3971 util.nearestHover(cR.svg, cR.svg.selectAll('circle.jpt'), { 3972 radius: 40, 3973 onHover: (o, el, e) => { 3974 if (el !== lastR){ 3975 lastR = el 3976 clearHov() 3977 hovEls = [slopes[o.i], cB[o.i], cA[o.i]] 3978 hovEls.forEach(n => n.classList.add('hov')) 3979 var p = o.p 3980 ttSel.classed('tooltip-hidden', false).html('') 3981 ttSel.append('div').html(`<b>${p.cat}</b>${p.word ? ' · ' + esc(p.word) : ''}`) 3982 ttSel.append('table.tt-pair').html( 3983 `<tr><td class=k>no swap</td><td>rank <b>${p.before}</b></td></tr>` + 3984 `<tr><td class=k>after swap</td><td>rank <b>${p.after}</b></td></tr>`) 3985 if (p.prompt) ttSel.append('div.prompt-block').text(trunc(p.prompt)) 3986 appendTopk(ttSel, p.lens_topk) 3987 } 3988 placeTT(e) 3989 }, 3990 onLeave: () => { lastR = null; clearHov(); ttSel.classed('tooltip-hidden', true) }, 3991 }) 3992} 3993 3994window.init?.() 3995 3996// âââ public/verbal-introspection/init-verbal-introspection.js âââ 3997window.initVerbalIntrospection = async function(opts){ 3998 var sel = d3.select('.verbal-introspection').html('') 3999 var base = (opts?.datapath || './').replace(/\/?$/, '/') 4000 var d = await util.getFile(base + 'data.json') 4001 4002 var SLOT = util.tol.blue, GREY = util.gray[500], HL = 'var(--hl-yellow)' 4003 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 4004 4005 var main = sel.append('div.vi-main') 4006 var left = main.append('div.vi-left') 4007 var right = main.append('div.vi-right') 4008 4009 // ââ prompt box ââ 4010 left.append('div.fig-title').text('Verbal introspection of injected workspace content') 4011 var box = left.append('div.vi-prompt.prompt-block') 4012 d.turns.forEach((t, i) => { 4013 var p = box.append('div.vi-turn') 4014 p.append('span.prompt-role').text(t.role + ': ') 4015 if (i === d.spanTurnIdx){ 4016 var s = p.append('span.hl') 4017 s.append('span').text(d.spanPre) 4018 s.append('span.tok-hit').text(',') 4019 s.append('span').text(d.spanPost) 4020 } else if (i === d.turns.length - 1){ 4021 p.append('span').text(d.lastBody) 4022 p.append('span.hit-c.tok-hit').text('"') 4023 } else { 4024 p.append('span').text(t.body) 4025 } 4026 }) 4027 4028 // ââ 3 bar tables ââ 4029 var cols = left.append('div.vi-cols') 4030 function table(l1, l2, rows, swatch, barColor, isLens){ 4031 var c = cols.append('div.vi-col.bar-table').classed('vi-lens', !!isLens) 4032 var h = c.append('h6') 4033 var l = h.append('div') 4034 l.append('span.swatch').st({background: swatch, marginRight: 5}) 4035 l.append('span').html(l1) 4036 h.append('div.cond').html(l2) 4037 var maxP = d3.max(rows, e => Math.exp(e.lp)) 4038 c.appendMany('div.bar-row', rows).at({title: e => util.ppCell(e.t).trim()}).html(e => 4039 `<div class="bar" style="width:${(Math.exp(e.lp)/maxP*100).toFixed(1)}%;background:${barColor}"></div>` + 4040 `<span class="t">${esc(util.ppCell(e.t).trim())}</span><span class="val">${e.lp.toFixed(2)}</span>`) 4041 } 4042 table(`J-lens ${esc(d.lensLabel)} at <span class="tok-hit">,</span>`, 'Original', d.tables.lensTgt, util.tol.orange, util.tint(util.tol.orange, 0.7), true) 4043 table('Next-token logits at <span class="hit-c tok-hit">"</span>', 'Original', d.tables.outClean, util.gray[400], 'var(--gray-200)') 4044 table('Next-token logits at <span class="hit-c tok-hit">"</span>', `After <span class="hl">injecting</span> "${esc(d.exampleConcept)}"`, d.tables.outTgt, util.tol.red, util.tint(util.tol.red, 0.7)) 4045 4046 // ââ right: median RR ± IQR vs strength ââ 4047 right.append('div.fig-title').text(`Injection across ${d.n} concepts`) 4048 var m = {left: 50, right: 14, top: 8, bottom: 36} 4049 var ch = d3.clamp(220, left.node().offsetHeight - right.node().offsetHeight - m.top - m.bottom, 520) 4050 var c = d3.conventions({ 4051 sel: right.append('div'), width: 280, height: ch, 4052 margin: m, 4053 }) 4054 c.x.domain([0, d3.max(d.curve.s)]) 4055 c.y = d3.scaleLog().domain([1e-4, 1]).range([c.height, 0]).clamp(true) 4056 c.xAxis.tickValues([0, 0.01, 0.02]).tickFormat(v => v.toFixed(2)) 4057 c.yAxis.scale(c.y).tickValues([1e-4, 1e-3, 1e-2, 1e-1, 1]) 4058 .tickFormat(v => v >= 1 ? '1' : v.toFixed(-Math.log10(v))) 4059 c.drawAxis() 4060 util.ggPlot(c) 4061 util.addAxisLabel(c, 'Steering strength s â', 'Median reciprocal rank of injected concept â') 4062 d3.select(c.svg.select('.y .axis-label').node().parentNode).translate([-38, c.height/2 - 15]) 4063 4064 var area = d3.area().x((_, i) => c.x(d.curve.s[i])).y0(v => c.y(v[0])).y1(v => c.y(v[1])) 4065 var line = d3.line().x((_, i) => c.x(d.curve.s[i])).y(v => c.y(v)) 4066 // labelPos: [anchorIdx, dx, dy, textAnchor] â hand-placed 4067 ;[ 4068 ['other', GREY, 'Other positions', [5, -6, -16, 'end']], 4069 ['slot', SLOT, 'At the open quote', [4, -26, 16, 'end']], 4070 ].forEach(([k, col, lab, [ai, dx, dy, anchor]]) => { 4071 var q = d.curve[k] 4072 c.svg.append('path').at({d: area(q.q25.map((lo, i) => [lo, q.q75[i]])), fill: col, opacity: 0.18}) 4073 c.svg.append('path.line-series').at({d: line(q.q50), fill: 'none', stroke: col, strokeWidth: 1.8, 'data-series': k}) 4074 // filled median anchors â the curve's "key statistic" marker per style guide 4075 c.svg.appendMany('circle', q.q50).at({cx: (_, i) => c.x(d.curve.s[i]), cy: v =>
4075 c.y(v), r: 2.4, fill: col}) 4076 c.svg.append('text.dlabel').text(lab) 4077 .at({x: c.x(d.curve.s[ai]) + dx, y: c.y(q.q50[ai]) + dy, dy: '.32em', textAnchor: anchor, fontSize: 'var(--fs-small)', 'data-series': k}) 4078 .st({fill: col, paintOrder: 'stroke', stroke: '#fff', strokeWidth: 1, strokeLinejoin: 'round'}) 4079 }) 4080 util.bindSeriesHover(right) 4081 util.dragTuneLabels(sel, 'verbal-introspection') 4082 util.colorCaption(sel, { 4083 'user turn': {bg: HL}, 4084 'open quotation mark': {bg: util.tint(SLOT, 0.85)}, 4085 'every other position': {bg: util.tint(GREY, 0.85)}, 4086 }) 4087} 4088 4089window.init?.() 4090 4091// âââ public/top-down-summoning/init-top-down-summoning.js âââ 4092window.initTopDownSummoning = async function(opts){ 4093 var sel = d3.select(opts?.sel || '.top-down-summoning').html('') 4094 var base = (opts?.datapath || './').replace(/\/?$/, '/') 4095 var spec = opts?.spec || 'main' 4096 var d = await util.getFile(base + spec + '.json') 4097 4098 var ORANGE = util.tol.orange 4099 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 4100 var strength = r => r == null ? 0 : r <= 2 ? 1.0 : r <= 5 ? 0.6 : 0.3 4101 var DARK = d3.color(ORANGE).darker(0.6).formatHex() 4102 var bg = (r, hex) => r == null ? 'transparent' 4103 : strength(r) >= 1 ? DARK : util.tint(hex, 1 - strength(r)) 4104 // flip to white text when the activation fill gets dark enough that black text 4105 // muddies (matches the activation-ramp rule in CLAUDE-FIGURE-STYLE-GUIDE.md) 4106 var hot = r => r != null && strength(r) > 0.6 4107 4108 // older bake shape: per-panel q1/q2 instead of a conds list 4109 d.panels.forEach(p => { 4110 if (p.conds) return 4111 p.conds = ['q1', 'q2'].map(c => ({kind: c, 4112 label: c == 'q1' ? 'Next-word question' : 'Property question', 4113 q: p[c].q, gen: p[c].gen, stim: p[c].stim, 4114 counts: [{targets: p.targets, nHit: p[c].nHit, nTot: p[c].nTot}]})) 4115 }) 4116 4117 var nRows = d3.max(d.panels, p => 1 + d3.sum(p.conds, c => (c.label ? 1 : 0) + 2)) 4118 var grid = sel.append('div.tds-grid').classed('tds-3col', d.panels.length == 3) 4119 var panels = grid.appendMany('div.tds-panel', d.panels).st({gridRow: 'span ' + nRows}) 4120 panels.append('div.mlabel.tds-title').text(p => p.title) 4121 4122 panels.each(function(p){ 4123 var pn = d3.select(this) 4124 p.conds.forEach(c => { 4125 if (c.label) pn.append('div.mlabel.tds-cond').text(c.label) 4126 var tx = pn.append('div.tds-tx.prompt-block') 4127 tx.append('span.prompt-role').text('Human: ') 4128 tx.append('span.q-diff').text(c.q) 4129 tx.append('span').text(' "') 4130 tx.appendMany('span.t', c.stim) 4131 .text(s => s.t) 4132 .at({title: s => s.r == null ? null : 'rank ' + s.r}) 4133 .classed('hot', s => hot(s.r)) 4134 .st({background: s => bg(s.r, ORANGE)}) 4135 tx.append('span').text('\n\n') 4136 tx.append('span.prompt-role').text('Assistant: ') 4137 tx.append('span').text(c.gen) 4138 // one short line: merge target sets when their hit counts match 4139 var counts = c.counts.every(ct => ct.nHit == c.counts[0].nHit) 4140 ? [{targets: c.counts.flatMap(ct => ct.targets), nHit: c.counts[0].nHit}] 4141 : c.counts 4142 pn.append('div.tds-count').html(counts.map(ct => 4143 `${ct.targets.map(t => `"${esc(t)}"`).join(' / ')} in lens on ` + 4144 `${ct.nHit} token${ct.nHit == 1 ? '' : 's'}`).join('<br>')) 4145 }) 4146 }) 4147 4148 util.colorCaption(sel, { 4149 ...(spec == 'main' ? {'different question': {bg: 'var(--hl-yellow)'}} : {}), 4150 'J-lens top-10': {bg: DARK, color: '#fff'}, 4151 }) 4152} 4153 4154window.init?.() 4155 4156// âââ public/flex-gen-systematic/init-flex-gen-systematic.js âââ 4157window.initFlexGenSystematic = async function(opts){ 4158 var spec = opts?.spec || 'systematic' 4159 var sel = d3.select(opts?.sel || '.flex-gen-systematic').html('') 4160 var base = (opts?.datapath || './').replace(/\/?$/, '/') 4161 var d = await util.getFile(base + spec + '.json') 4162 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 4163 4164 if (spec === 'appendix') return renderAppendix(sel, d, esc) 4165 4166 // âââ systematic: dot-stem left, scatter right âââ 4167 var row = sel.append('div.fgs-row') 4168 // Categorical palette from the visual vocab (avoid orange + red â see style guide). 4169 var catPalette = [util.tol.blue, util.tol.teal, util.tol.magenta, util.gray[500]] 4170 var col = Object.fromEntries(d.catOrder.map((c, i) =>
4170 [c, catPalette[i % catPalette.length]])) 4171 4172 // ââ left: per-function HIT, â=α=1 / Ã=α=2 ââ 4173 var rows = [] 4174 d.catOrder.forEach(c => { 4175 var sub = d.pts.filter(p => p.cat === c).sort((a, b) => b.hit2 - a.hit2) 4176 rows.push(...sub) 4177 }) 4178 var H = 320, W = 280, M = {left: 78, right: 8, top: 6, bottom: 30} 4179 var cL = d3.conventions({sel: row.append('div'), width: W, height: H, margin: M}) 4180 // Top-1 rate is a proportion â no negative axis space, no headroom past 100%; 4181 // markers at 0/1 sit on the boundary 4182 cL.x.domain([0, 1]) 4183 var y = d3.scalePoint().domain(d3.range(rows.length)).range([0, cL.height]).padding(0.5) 4184 cL.xAxis.tickValues([0, 0.5, 1.0]).tickFormat(d3.format('.0%')) 4185 cL.drawAxis(); cL.svg.select('.y').remove() 4186 util.ggPlot(cL) 4187 util.addAxisLabel(cL, 'Top-1 rate â', '') 4188 // category divider lines 4189 for (var i = 1; i < d.catOrder.length; i++) 4190 cL.svg.append('line').at({x1: 0, x2: cL.width, y1: (y(i*4-1)+y(i*4))/2, y2: (y(i*4-1)+y(i*4))/2, 4191 stroke: util.gray[300], strokeWidth: 0.5}) 4192 var g = cL.svg.appendMany('g.frow', rows).translate((p, i) => [0, y(i)]) 4193 g.append('line').at({x2: p => cL.x(p.hit1/d.nPer), stroke: p => col[p.cat], strokeWidth: 1.2}) 4194 g.append('line').at({x2: p => cL.x(p.hit2/d.nPer), stroke: p => col[p.cat], strokeWidth: 1.0, 4195 strokeDasharray: '2,2'}) 4196 // α=1 filled "key statistic" marker (3.5 ~ small-side of the 1.8-2.2 stroked 4197 // range; filled so it reads as the anchor against the à at α=2) 4198 g.append('circle').at({cx: p => cL.x(p.hit1/d.nPer), r: 3.5, fill: p => col[p.cat]}) 4199 g.append('path').at({d: 'M-4,-4L4,4M-4,4L4,-4', transform: p => `translate(${cL.x(p.hit2/d.nPer)},0)`, 4200 stroke: p => col[p.cat], strokeWidth: 1.5, fill: 'none'}) 4201 g.append('text').text(p => p.func).at({x: -6, dy: '.32em', textAnchor: 'end', 4202 fontSize: 'var(--fs-label)', fill: util.gray[700]}) 4203 // legend â categorical dots (mirror the data markers' filled circles) 4204 var leg = cL.svg.append('g').translate([cL.width - 60, cL.height - 86]) 4205 d.catOrder.forEach((c, i) => { 4206 var lg = leg.append('g').translate([0, i*12]) 4207 lg.append('circle').at({r: 3.5, fill: col[c]}) 4208 lg.append('text').text(c).at({x: 8, dy: '.32em', fontSize: 'var(--fs-label)', fill: util.gray[700]}) 4209 }) 4210 ;[['α=1', 'circle'], ['α=2', 'x']].forEach(([lab, mk], i) => { 4211 var lg = leg.append('g').translate([0, 52 + i*12]) 4212 if (mk === 'circle') lg.append('circle').at({r: 3.5, fill: '#fff', stroke: util.gray[700], strokeWidth: 1}) 4213 else lg.append('path').at({d: 'M-3,-3L3,3M-3,3L3,-3', stroke: util.gray[700], strokeWidth: 1.4}) 4214 lg.append('text').text(lab).at({x: 8, dy: '.32em', fontSize: 'var(--fs-label)', fill: util.gray[700]}) 4215 }) 4216 4217 // ââ right: csum vs Îlp scatter ââ 4218 var W2 = 360, M2 = {left: 50, right: 14, top: 6, bottom: 36} 4219 var cR = d3.conventions({sel: row.append('div'), width: W2, height: H, margin: M2}) 4220 var xExt = d3.extent(d.pts, p => p.csum), xSpan = xExt[1] - xExt[0] 4221 var yExt = d3.extent(d.pts, p => p.dlpc), ySpan = yExt[1] - yExt[0] 4222 cR.x.domain([xExt[0] - 0.06*xSpan, xExt[1] + 0.18*xSpan]) 4223 cR.y.domain([Math.min(yExt[0], 0) - 0.06*ySpan, yExt[1] + 0.08*ySpan]) 4224 cR.xAxis.ticks(5); cR.yAxis.ticks(5) 4225 cR.drawAxis(); util.ggPlot(cR) 4226 util.addAxisLabel(cR, 'Clean workspace loading (cos at arg + cos at readout) â', 4227 'Swap effect at α=1 (Îlp_target â Îlp_spon) â', '', 6, -6) 4228 // y=0 ref + fit line 4229 cR.svg.append('line').at({x1: 0, x2: cR.width, y1: cR.y(0), y2: cR.y(0), stroke: util.gray[300], strokeWidth: 0.7}) 4230 var xs = cR.x.domain() 4231 cR.svg.append('line').at({x1: cR.x(xs[0]), x2: cR.x(xs[1]), 4232 y1: cR.y(d.fit.m*xs[0]+d.fit.b), y2: cR.y(d.fit.m*xs[1]+d.fit.b), 4233 stroke: util.gray[400], strokeWidth: 1.1, strokeDasharray: '4,3'}) 4234 // error bars + dots + labels â whiskers in series color, faded so dots dominate 4235 var pg = cR.svg.appendMany('g.pt', d.pts).translate(p => [cR.x(p.csum), cR.y(p.dlpc)]) 4236 pg.append('line').at({x1: p => cR.x(p.csum-p.csumStd)-cR.x(p.csum), x2: p => cR.x(p.csum+p.csumStd)-cR.x(p.csum), 4237 stroke: p => util.tint(col[p.cat], 0.6), strokeWidth: 1}) 4238 pg.append('line').at({y1: p => cR.y(p.dlpc-p.dlpcSem)-cR.y(p.dlpc), y2: p => cR.y(p.dlpc+p.dlpcSem)-cR.y(p.dlpc), 4239 stroke: p => util.tint(col[p.cat], 0.6), strokeWidth: 1}) 4240 // circles on cR.svg (not inside translated g.pt) so nearestHover reads cx/cy 4241 cR.svg.appendMany('circle.jpt', d.pts) 4242 .at({cx: p => cR.x(p.csum), cy: p => cR.y(p.dlpc), r: 2.2, 4243 fillOpacity: 0, stroke: p => col[p.cat], strokeWidth: 1}) 4244 // label placement: try 4 diagonals, greedily pick the one with most clearance 4245 // from other points + already-placed label boxes. Diagonal keeps text off the 4246 // CI crosshair (which runs horiz+vert through the point). 4247 var px = d.pts.map(p => [cR.x(p.csum), cR.y(p.dlpc)]) 4248 var diag = [[6, -4, 'start'], [-6, -4, 'end'], [6, 11, 'start'], [-6, 11, 'end']] 4249 var charW = 6, lineH = 10, pad = 4, placed = [] 4250 function labelBox(i, [dx, dy, anc]){ 4251 var w = d.pts[i].func.length * charW 4252 var x0 = anc === 'start' ? px[i][0] + dx : px[i][0] + dx - w
4253 return [x0, px[i][1] + dy - lineH, x0 + w, px[i][1] + dy] 4254 } 4255 function boxDist(b, x, y){ 4256 var dx = Math.max(b[0] - x, 0, x - b[2]), dy = Math.max(b[1] - y, 0, y - b[3]) 4257 return Math.hypot(dx, dy) 4258 } 4259 function overlap(a, b){ return a[0] < b[2]+pad && a[2]+pad > b[0] && a[1] < b[3]+pad && a[3]+pad > b[1] } 4260 function score(i, cand){ 4261 var bx = labelBox(i, cand), s = 1e9 4262 px.forEach(([x, y], j) => { if (j !== i) s = Math.min(s, boxDist(bx, x, y)) }) 4263 placed.forEach(b => { if (overlap(bx, b)) s -= 1e3 }) 4264 if (bx[2] > cR.width || bx[0] < 0) s -= 200 4265 return s 4266 } 4267 var nudge = {} 4268 d3.range(d.pts.length).sort((a, b) => px[a][0] - px[b][0]).forEach(i => { 4269 var best = d3.greatest(diag, c => score(i, c)) 4270 nudge[d.pts[i].func] = best 4271 placed.push(labelBox(i, best)) 4272 }) 4273 pg.append('text').text(p => p.func) 4274 .at({x: p => nudge[p.func][0], dy: p => nudge[p.func][1], 4275 textAnchor: p => nudge[p.func][2], 4276 fontSize: 'var(--fs-label)', fill: util.gray[600]}) 4277 .st({paintOrder: 'stroke', stroke: '#EAECED', strokeWidth: 1, strokeLinejoin: 'round'}) // lint-ok hex-js: ggPlot bg halo 4278 cR.svg.append('text.num').text(`r = ${d.r >= 0 ? '+' : ''}${d.r.toFixed(2)}`) 4279 .at({x: 8, y: 14, fontSize: 'var(--fs-body)', fill: util.gray[700]}) 4280 .st({fontVariantNumeric: 'tabular-nums'}) 4281 4282 var ttSel = d3.select('.tooltip'), last 4283 util.nearestHover(cR.svg, cR.svg.selectAll('circle.jpt'), { 4284 radius: 40, 4285 onHover: (p, el, e) => { 4286 if (el !== last){ 4287 last = el 4288 ttSel.classed('tooltip-hidden', false).html('') 4289 ttSel.append('div').html(`<b style="color:${col[p.cat]}">${esc(p.func)}</b> · ${p.cat}`) 4290 ttSel.append('div').html( 4291 `top-1: <b>${p.hit1}/${d.nPer}</b> (α=1), ${p.hit2}/${d.nPer} (α=2)<br>` + 4292 `loading: ${p.csum.toFixed(2)} · Îlp: ${p.dlpc.toFixed(2)}`) 4293 } 4294 var bb = ttSel.node().getBoundingClientRect() 4295 ttSel.st({left: d3.clamp(20, e.clientX - bb.width/2, innerWidth - bb.width - 20) + 'px', 4296 top: (innerHeight > e.clientY + 20 + bb.height ? e.clientY + 20 : e.clientY - bb.height - 20) + 'px'}) 4297 }, 4298 onLeave: () => { last = null; ttSel.classed('tooltip-hidden', true) }, 4299 }) 4300} 4301 4302function renderAppendix(sel, d, esc){ 4303 // sentence-case the baked lowercase category/function names (matches flex-gen-example's .capitalize()) 4304 var cap = s => s.charAt(0).toUpperCase() + s.slice(1) 4305 d.cats.forEach(cat => { 4306 var cs = sel.append('div.fga-cat') 4307 cs.append('div.fga-cat-h').text(cap(cat.name)) 4308 var row = cs.append('div.fga-row') 4309 cat.funcs.forEach(F => { 4310 var grd = row.append('div.fga-grid') 4311 grd.append('div.fga-grid-h').html(`${esc(cap(F.name))} <span class="n">${F.nHit}/12 top-1</span>`) 4312 var tbl = grd.append('table.g') 4313 var thr = tbl.append('tr') 4314 thr.append('th.r') 4315 cat.args.forEach(a => thr.append('th') 4316 .html(`<span class="o mlabel">${esc(a)}</span><span class="a">${esc(F.answers[a])}</span>`).at({title: a})) 4317 cat.args.forEach((sp, ri) => { 4318 var tr = tbl.append('tr') 4319 tr.append('th.r').text(sp).at({title: sp}) 4320 F.cells[ri].forEach(c => { 4321 var td = tr.append('td').text(c.t).at({title: c.t + (c.rk ? ` (rank ${c.rk})` : '')}) 4322 if (c.diag) td.classed('diag', true) 4323 else { 4324 var cls = c.rk === 1 ? 'hit' : c.rk <= 5 ? 'near' : 'miss' 4325 td.classed(cls, true) 4326 if (c.rk > 1) td.append('span.rk').text(c.rk) 4327 } 4328 }) 4329 }) 4330 }) 4331 }) 4332 var leg = sel.append('div.fga-legend') 4333 leg.append('span').text('Rank of correct answer:') 4334 ;[['hit', '1'], ['near', '2â5'], ['miss', '>5'], ['diag', 'No swap']].forEach(([cls, lab]) => { 4335 var it = leg.append('span.item') 4336 it.append('span.sw').classed(cls, true) 4337 it.append('span').text(lab) 4338 }) 4339} 4340 4341window.init?.() 4342 4343// âââ public/selectivity-linecount/init-selectivity-linecount.js âââ 4344window.initSelectivityLinecount = async function(opts){ 4345 var sel = d3.select(opts?.sel || '.selectivity-linecount').html('') 4346 var base = (opts?.datapath || './').replace(/\/?$/, '/') 4347 var d = await util.getFile(base + 'data.json') 4348 sel.st({'--ncol': d.conds.length}) 4349 4350 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 4351 var orange = t => d3.interpolateOranges(0.1 + 0.55*t) 4352 var condFill = co => /^Automatic/.test(d.condLabel[co]) ? util.gray[300] : util.tol.blue 4353 4354 // ââ 3-column example ââ 4355 sel.append('div.fva-row3.titles').appendMany('div.fig-title', d.example) 4356 .text(e => e.title) 4357 sel.append('div.fva-note').text('Model output in bold') 4358 4359 var tops = sel.append('div.fva-row3').appendMany('div', d.example) 4360 var pr = tops.append('div.fva-prompt.prompt-block') 4361 pr.each(function(e){ 4362 var vmax = Math.max(1, d3.max(e.tokens, t => t.d)) 4363 d3.select(this).appendMany('span', e.tokens).each(function(t){ 4364 var s = d3.select(this).attr('class', t.cls || null) 4365 var txt = t.nl ? esc(t.s).replaceAll('\n', "<span class='nl'>âµ</span>\n") : esc(t.s) 4366 if (t.d > 0) s.classed('ramp', true) 4367 .st({background: orange(t.d / vmax)}) 4368 s.html(txt) 4369 }) 4370 }) 4371 tops.append('div.fva-legend').text(e => `${e.density} ${d.densityNoun || 'number-tokens in lens'}`) 4372 4373 // The continue column reports the model's output token (a newline) rather 4374 // than the column it wraps at â apply here so the figure is correct even 4375 // against a data.json baked before the corresponding bake-script change. 4376 d.example.filter(e => e.cond == 'continue').forEach(e => { 4377 e.ansLabel = 'output:'; e.ans = 'âµ' 4378 }) 4379 // swapLabel is now the section heading (was previously repeated under each 4380 // column). Baked data.json before this change has the short form 4381 // "Swap the lens count 40sâ60s:"; rephrase it here so the figure reads 4382 // correctly without waiting on a re-bake. 4383 var m = /(\d)0s.*?(\d)0s/.exec(d.swapLabel || '') 4384 var swapHeading = m 4385 ? `Swap J-lens vectors for numbers in the ${m[1]}0s for numbers in the ${m[2]}0s` 4386 : d.swapLabel 4387 sel.append('div.fig-title.mid').text(swapHeading) 4388 var bots = sel.append('div.fva-row3').appendMany('div.fva-swp', d.example) 4389 bots.append('div.row2').html(e => 4390 `<span class='lab'>${esc(e.ansLabel)}</span>` + 4391 `<span><b class='out'>${esc(e.ans)}</b>` + 4392 `<span class='vd'>before swap</span></span>`) 4393 bots.append('div.row2').html(e => 4394 `<span class='lab'>after swap:</span>` + 4395 `<span><b class='out'>${esc(e.swap)}</b>` + 4396 `<span class='vd'>${esc(e.verdict)}</span></span>`) 4397 4398 // ââ 3 bar panels ââ 4399 sel.append('div.fva-sep').text(`Across all n=${d.n} snippets`) 4400 // match Fig 20's selectivity-language barChart: same y-label wording, % 4401 // ticks on every panel, multi-line rotated label (not addAxisLabel arrow) 4402 var YLAB = { 4403 'P(correct answer) â': 'Task success rate', 4404 'Count tokens in lens â': 'Rate of count appearing in\nJ-lens on prompt tokens', 4405 'P(answer follows swap) â': 'Rate of answer following swap', 4406 } 4407 var br = sel.append('div.fva-bars').appendMany('div', d.bars) 4408 var colW = br.node().offsetWidth 4409 var M = {left: 58, right: 8, top: 22, bottom: 50} 4410 br.each(function(b){ 4411 var c = d3.conventions({ 4412 sel: d3.select(this), width: colW - M.left - M.right, height: 200, margin: M, 4413 })
4414 var x = d3.scaleBand().domain(d.conds).range([0, c.width]) 4415 .paddingInner(0.3).paddingOuter(0.14) 4416 var ylim = b.ylim || [0, d3.max(b.vals, v => (v.mean||0) + (v.sem||0)) * 1.12] 4417 c.y.domain(ylim) 4418 c.yAxis.ticks(5).tickFormat(d3.format('.0%')).tickSize(4) 4419 c.drawAxis(); c.svg.select('.x').remove() 4420 util.ggPlot(c) 4421 var ylines = (YLAB[b.ylabel] || b.ylabel.replace(/\s*â\s*$/, '')).split('\n') 4422 var yfs = ylines.length > 1 ? 12 : 14 4423 var yx = -44 - (ylines.length-1) * (yfs+1) 4424 var ytxt = c.svg.append('text').at({x: yx, y: c.height/2}) 4425 .attr('transform', `rotate(-90 ${yx} ${c.height/2})`) 4426 .st({fontSize: yfs, fill: util.gray[700], textAnchor: 'middle'}) 4427 ylines.forEach((l, i) => ytxt.append('tspan').at({x: yx, dy: i ? yfs+1 : 0}).text(l)) 4428 c.svg.append('text.panel-title').text(b.title) 4429 .at({x: 0, y: -8, fontSize: 'var(--fs-small)', fontWeight: 600}) // lint-ok inline-font-js: SVG axis-title 4430 c.svg.appendMany('text.xlab', d.conds).at({textAnchor: 'middle'}) 4431 .translate(co => [x(co)+x.bandwidth()/2, c.height+12]) 4432 .each(function(co){ 4433 var ls = d.condLabel[co].split('\n') 4434 d3.select(this).appendMany('tspan', ls).text(s => s) 4435 .at({x: 0, dy: (_, i) => i ? '1.15em' : 0}) 4436 }) 4437 var g = c.svg.appendMany('g', b.vals.filter(v => v.mean != null)) 4438 .translate(v => [x(v.cond), 0]) 4439 g.append('rect').at({ 4440 width: x.bandwidth(), y: v => c.y(v.mean), height: v => c.height - c.y(v.mean), 4441 fill: v => condFill(v.cond), 4442 }) 4443 g.each(function(v){ 4444 util.barWhisker(d3.select(this), {x: x.bandwidth()/2, lo: c.y(v.mean - v.sem), hi: c.y(v.mean + v.sem)}) 4445 }) 4446 }) 4447} 4448 4449window.init?.() 4450 4451// âââ public/selectivity-language/init-selectivity-language.js âââ 4452window.initSelectivityLanguage = async function(opts){ 4453 var base = (opts?.datapath || './').replace(/\/?$/, '/') 4454 var swap = await util.getFile(base + 'swap.json').catch(() => null) 4455 var rd = null, ex = null 4456 4457 var orange = t => d3.interpolateOranges(0.1 + 0.55*t) 4458 var WS_LABELS = {'Explicit report': 1, 'Flexible computation': 1} 4459 var catFill = lab => WS_LABELS[lab] ? util.tol.blue : util.gray[400] 4460 4461 function barChart(sel, rows, meta, ylab, ymax){ 4462 sel.html('') 4463 var groups = [] 4464 meta.tasks.forEach(t => meta.models.forEach(m => groups.push([t, m]))) 4465 var nC = meta.cats.length, nG = groups.length 4466 var avail = Math.max(120, sel.node().clientWidth 4467 || sel.node().parentNode?.clientWidth || 380) 4468 var nM = meta.models.length, nT = meta.tasks.length 4469 var bottomM = 42 + (nM > 1 ? 16 : 0) 4470 + (nT > 1 ? 16 : 0) 4471 var c = d3.conventions({ 4472 sel, totalWidth: Math.min(avail, 420), height: 200, 4473 margin: {left: 58, top: 22, right: 4, 4474 bottom: bottomM}, 4475 }) 4476 var gx = d3.scaleBand().domain(d3.range(nG)) 4477 .range([0, c.width]).paddingInner(0.18) 4478 var cx = d3.scaleBand().domain(meta.cats) 4479 .range([0, gx.bandwidth()]).paddingInner(0.22) 4480 c.y.domain([0, ymax]) 4481 c.yAxis.tickFormat(d3.format('.0%')).ticks(5).tickSize(4) 4482 c.drawAxis() 4483 c.svg.select('.x').remove() 4484 util.ggPlot(c) 4485 var ylines = String(ylab).split('\n') 4486 var yfs = ylines.length > 1 ? 12 : 14 4487 var yx = -44 - (ylines.length-1) * (yfs+1) 4488 var ytxt = c.svg.append('text') 4489 .at({x: yx, y: c.height/2}) 4490 .attr('transform', `rotate(-90 ${yx} ${c.height/2})`) 4491 .st({fontSize: yfs, fill: util.gray[700], 4492 textAnchor: 'middle'}) 4493 ylines.forEach((l, i) => ytxt.append('tspan') 4494 .at({x: yx, dy: i ? yfs+1 : 0}).text(l)) 4495 4496 var idx = {} 4497 rows.forEach(r => { idx[[r.task, r.model, r.cat]] = r }) 4498 4499 groups.forEach(([t, m], gi) => { 4500 var g = c.svg.append('g').translate([gx(gi), 0]) 4501 meta.cats.forEach(cat => { 4502 var r = idx[[t, m, cat]] 4503 if (!r || r.mean == null) return 4504 var bx = cx(cat), bw = cx.bandwidth() 4505 var n = r.pts.length 4506 var sd = n > 1 ? Math.sqrt( 4507 r.pts.reduce((a,p)=>a+(p-r.mean)**2,0)/(n-1)) : 0 4508 var se = n > 1 ? sd/Math.sqrt(n) : 0
4509 var lo = Math.max(0, r.mean - 1.96*se) 4510 var hi = Math.min(1, r.mean + 1.96*se) 4511 g.append('rect') 4512 .at({x: bx, y: c.y(r.mean), width: bw, 4513 height: c.height - c.y(r.mean)}) 4514 .st({fill: catFill(meta.labels[cat])}) 4515 .append('title') 4516 .text(`${t} / ${meta.model_labels[m]}\n` 4517 + `${meta.labels[cat]}: ` 4518 + `${(r.mean*100).toFixed(0)}% ± ` 4519 + `${(1.96*se*100).toFixed(0)}% (n=${n})`) 4520 if (n > 1) util.barWhisker(g, {x: bx + bw/2, lo: c.y(lo), hi: c.y(hi)}) 4521 }) 4522 meta.cats.forEach((cat, ci) => { 4523 var bx = cx(cat), bw = cx.bandwidth() 4524 var words = meta.labels[cat].split(' ') 4525 var txt = g.append('text') 4526 .at({x: bx + bw/2, y: c.height + 14}) 4527 .st({fontSize: 'var(--fs-label)', textAnchor: 'middle', 4528 fill: util.gray[700]}) 4529 words.forEach((w, i) => txt.append('tspan') 4530 .at({x: bx + bw/2, dy: i ? '1.1em' : 0}).text(w)) 4531 }) 4532 // Model label under bars; task header above models 4533 // when there are multiple tasks. 4534 if (nG > 1){ 4535 var mY = c.height + 46 4536 if (meta.models.length > 1){ 4537 g.append('text') 4538 .at({x: gx.bandwidth()/2, y: mY}) 4539 .st({fontSize: 'var(--fs-label)', textAnchor: 'middle', 4540 fill: util.gray[500]}) 4541 .text(meta.model_labels[m].split(' ')[0]) 4542 } 4543 if (m === meta.models[0] && meta.tasks.length > 1){ 4544 var tX = gx.bandwidth() * meta.models.length / 2 4545 var tY = meta.models.length > 1 ? mY + 16 : mY 4546 g.append('text') 4547 .at({x: tX, y: tY}) 4548 .st({fontSize: 'var(--fs-small)', textAnchor: 'middle', 4549 fontWeight: 600, fill: util.gray[700]}) // lint-ok inline-font-js: structural sub-axis label 4550 .text(t) 4551 } 4552 } 4553 }) 4554 } 4555 4556 if (swap){ 4557 d3.selectAll('.flex-auto-swap').each(function(){ 4558 var sel = d3.select(this) 4559 var tag = sel.attr('data-tag') || 'v2' 4560 var tasksAttr = sel.attr('data-tasks') 4561 var modelsAttr = sel.attr('data-models') 4562 var tasks = tasksAttr 4563 ? tasksAttr.split(',') : swap.tasks 4564 var models = modelsAttr 4565 ? modelsAttr.split(',') : swap.models 4566 var rows = (swap[tag] || []) 4567 .filter(r => tasks.includes(r.task) 4568 && models.includes(r.model)) 4569 var meta = {cats: swap.cats, tasks: tasks, 4570 models: models, labels: swap.labels, 4571 colors: swap.colors, 4572 model_labels: swap.model_labels} 4573 var ylab = tag === 'ablation' 4574 ? 'P(own answer preserved)' 4575 : 'P(answer follows swap)' 4576 barChart(sel, rows, meta, ylab, 1.0) 4577 }) 4578 4579 // Polished example layout: one cell per column, each 4580 // with title, prompt box, lens-count, swap rows. 4581 d3.selectAll('.flex-auto-polished').each(function(){ 4582 var sel = d3.select(this).html('') 4583 var pe = swap.polished_ex 4584 if (!pe) return 4585 var grid = sel.append('div').at({class: 'row3'}) 4586 function renderPrompt(c){ 4587 var smax = d3.max(c.lens_strength || []) || 1 4588 // Fallback when no lens_hits collected for this 4589 // column: render plain prompt text. 4590 if (!c.tokens.length && c.prompt_text){ 4591 var t = esc(c.prompt_text) 4592 if (c.anom_str) 4593 t = t.replace(esc(c.anom_str), 4594 `<span class='anom'>${esc(c.anom_str)}</span>`) 4595 var ws = c.out0.split(/\s+/) 4596 var mg = ws.slice(0,5).join(' ') + (ws.length > 5 ? 'â¦' : '') 4597 return `<span class='prompt-role'>Human:</span> ${t}` 4598 + `\n\n<span class='prompt-role'>Assistant:</span>` 4599 + ` <span class='prompt-gen'>${esc(mg)}</span>` 4600 } 4601 var pp = c.tokens.map(prettyToken) 4602 for (var i = 1; i < pp.length; i++) 4603 if (pp[i].startsWith(' ') 4604 && pp[i-1].endsWith('\n')) 4605 pp[i] = pp[i].slice(1) 4606 var html = `<span class='prompt-role'>Human:</span>` 4607 var atok = c.assistant_tok
4608 pp.forEach((s, i) => { 4609 if (atok && i >= atok[0] && i < atok[1]){ 4610 if (i === atok[0]) 4611 html += `\n\n<span class='prompt-role'>Assistant:` 4612 + `</span>` 4613 return 4614 } 4615 var isAnom = c.anom_tok && i >= c.anom_tok[0] 4616 && i < c.anom_tok[1] 4617 s = esc(s) 4618 if (c.lens_hit?.[i]){ 4619 var t = (c.lens_strength?.[i] || 0) / smax 4620 html += `<span class='ramp${isAnom?' anom':''}' ` 4621 + `style='background:${orange(t)}'` 4622 + `>${s}</span>` 4623 } else if (isAnom){ 4624 html += `<span class='anom'>${s}</span>` 4625 } else html += s 4626 }) 4627 if (!atok) 4628 html += `\n\n<span class='prompt-role'>Assistant:</span>` 4629 var ws = c.out0.split(/\s+/) 4630 var mgen = ws.slice(0,5).join(' ') + (ws.length > 5 ? 'â¦' : '') 4631 html += ` <span class='prompt-gen'>${esc(mgen)}</span>` 4632 return html 4633 } 4634 pe.cols.forEach((c, idx) => { 4635 var cell = grid.append('div') 4636 cell.append('div').at({class: 'fig-title'}) 4637 .st({borderBottom: 'none', paddingBottom: 0}) 4638 .text(c.title.replace(' â ', ': ')) 4639 cell.append('div').at({class: 'prompt prompt-block'}) 4640 .html(renderPrompt(c)) 4641 var isCont = c.cat === 'auto_cont' 4642 // Legend (bold/orange-swatch key) sits between the prompt block and 4643 // the swap block in the auto_cont column â shared key for the whole 4644 // example row, placed once. Replaces the old per-column "N tokens 4645 // in lens" annotations. 4646 if (isCont) cell.append('div').at({class: 'inlay'}) 4647 .html(`<b>bold</b> = model output · ` 4648 + `<span class='legend-sq' style='background:${orange(.8)}'></span>` 4649 + `${esc(pe.sig_label)} in lens top-${pe.k}`) 4650 // Word-boundary truncation so the swap-result line never ends 4651 // mid-word (the auto_cont output is a full sentence; a hard char 4652 // cut at 36 sliced "cómo" â "cóm"). 34 keeps "â¦observando" and 4653 // fits the column at the paper's <figure class='wide'> width. 4654 var short = s => { 4655 var first = s.split(/[\n.]/)[0] 4656 if (first.length <= 34) return first 4657 return first.slice(0, 34).replace(/\s+\S*$/, '') + 'â¦' 4658 } 4659 var swp = cell.append('div').at({class: 'swp'}) 4660 swp.append('div').at({class: 'row-lr'}) 4661 .html(`Swap J-lens ${esc(pe.own)}â${esc(pe.donor)} ` 4662 + `at every position:`) 4663 swp.append('div').at({class: 'row-mid'}) 4664 .html(`<span class='lab'>output:</span> ` 4665 + `<b class='out'>${esc(short(c.out0))}</b>`) 4666 swp.append('div').at({class: 'row-mid'}) 4667 .html(`<span class='lab'>after swap:</span> ` 4668 + `<b class='out'>${esc(short(c.out1))}</b>` 4669 + (isCont ? '' 4670 : `<span class='vd'>${esc(c.verdict)}</span>`)) 4671 if (isCont) swp.append('div').at({class: 'vd-line'}) 4672 .text(c.verdict) 4673 }) 4674 sel.append('div').at({class: 'sep'}) 4675 .text(`Across all n=${pe.n_stim} snippets`) 4676 }) 4677 4678 // 3-panel chart (a/b/c) â sonnet lang prefill. 4679 d3.selectAll('.flex-auto-3panel').each(function(){ 4680 var sel = d3.select(this) 4681 var tp = swap.three_panel 4682 if (!tp) return 4683 sel.html('') 4684 tp.panels.forEach(p => { 4685 var cell = sel.append('div') 4686 var chart = cell.append('div') 4687 var meta = {cats: tp.cats, tasks: ['lang'], 4688 models: ['sonnet'], labels: swap.labels, 4689 colors: swap.colors, 4690 model_labels: swap.model_labels} 4691 var ymax = p.key === 'b' 4692 ? Math.max(.05, 4693 ...p.rows.map(r => r.mean || 0)) * 1.15 4694 : 1.0 4695 barChart(chart, p.rows, meta, p.ylab, ymax) 4696 chart.select('svg g').append('text.panel-title') 4697 .text(`${p.key} · ${p.title}`) 4698 .at({x: 0, y: -8, fontSize: 'var(--fs-small)', fontWeight: 600}) // lint-ok inline-font-js: SVG axis-title 4699 }) 4700 }) 4701 4702 util.colorCaption(d3.select('.selectivity-language'), {
4703 'continuation': {bg: util.gray[400], color: '#fff'}, 4704 'anomaly detection': {bg: util.gray[400], color: '#fff'}, 4705 'explicit report': {bg: util.tol.blue, color: '#fff'}, 4706 'flexible computation': {bg: util.tol.blue, color: '#fff'}, 4707 }) 4708 } 4709 4710 function esc(s){ 4711 return String(s ?? '').replace(/[&<>"']/g, c => 4712 ({'&':'&','<':'<','>':'>', 4713 '"':'"',"'":'''}[c])) 4714 } 4715 4716 function prettyToken(t){ 4717 // tokenization markers: case marker precedes space 4718 // marker (ââ½human = " Human"). ppToken consumes the 4719 // space; we need to preserve it. 4720 if (t.startsWith('<') && t.endsWith('>')) return '' 4721 return String(t) 4722 .replace(/([â²â])([·â½â]?)(\p{L})/gu, 4723 (_, _c, sp, l) => (sp ? ' ' : '') + l.toUpperCase()) 4724 .replace(/âª([·â½â]?)(\p{L}+)/gu, 4725 (_, sp, w) => (sp ? ' ' : '') + w.toUpperCase()) 4726 .replace(/[â²ââª]/gu, '') 4727 .replace(/[â½â·]/gu, ' ') 4728 .replace(/â/gu, '\n') 4729 } 4730 4731 function renderTokenPrompt(c){ 4732 // Token-level rendering when lens-hit data is present. 4733 // c.tokens: per-token marked strings; 4734 // c.lens_hit: bool[]; c.anom_tok: [start,end) or null. 4735 var smax = d3.max(c.lens_strength || []) || 1 4736 var ws = c.ws_prefix || '' 4737 var pp = c.tokens.map(prettyToken) 4738 // Drop a leading space on tokens that follow a 4739 // newline so paragraphs aren't indented by one char. 4740 for (var i = 1; i < pp.length; i++) 4741 if (pp[i].startsWith(' ') && pp[i-1].endsWith('\n')) 4742 pp[i] = pp[i].slice(1) 4743 while (pp.length && !pp[pp.length-1].trim()) pp.pop() 4744 var body = pp.map((s, i) => { 4745 // Prefill mode: body tokens include the Assistant 4746 // marker inline; replace those tokens with the 4747 // styled turn label. 4748 if (c.assistant_tok && i >= c.assistant_tok[0] 4749 && i < c.assistant_tok[1]){ 4750 return i === c.assistant_tok[0] 4751 ? `\n\n<span class='fa-turn'>Assistant:</span>` 4752 : '' 4753 } 4754 var isAnom = c.anom_tok && i >= c.anom_tok[0] 4755 && i < c.anom_tok[1] 4756 s = esc(s) 4757 if (c.lens_hit?.[i]){ 4758 var aCls = isAnom ? ' fa-anom' : '' 4759 var t = (c.lens_strength?.[i] || 0) / smax 4760 return ( 4761 `<span class='fa-lens-hit${aCls}' ` 4762 + `style='background:${orange(t)}'>` 4763 + s + `</span>`) 4764 } 4765 if (isAnom) return `<span class='fa-anom'>${s}</span>` 4766 return s 4767 }).join('') 4768 var lead = pp[0]?.startsWith(' ') ? '' : ' ' 4769 var wsD = ws ? (ws.startsWith(' ') ? ws : ' ' + ws) : '' 4770 var tail = c.assistant_tok 4771 ? '' 4772 : (`\n\n<span class='fa-turn'>Assistant:</span>` 4773 + (wsD ? `<span class='fa-ws'>${esc(wsD)}</span>` : '')) 4774 return ( 4775 `<span class='fa-turn'>Human:</span>${lead}${body}` 4776 + tail 4777 ) 4778 } 4779 4780 function highlightPrompt(c, mode){ 4781 if (c.tokens) return renderTokenPrompt(c) 4782 // mode: 'all' | 'stim' | 'post' | 'surround' 4783 // Returns HTML with swap region wrapped in .fa-swp and 4784 // anomaly (if present) in .fa-anom. 4785 var q = esc(c.question) 4786 var et = esc(c.text) 4787 var stim_start = Math.max(0, q.indexOf(et)) 4788 var stim_end = stim_start + et.length 4789 var pre, swp, post 4790 if (mode === 'all' || mode === 'surround'){ 4791 pre = ''; swp = q; post = '' 4792 } else if (mode === 'stim'){ 4793 pre = q.slice(0, stim_start) 4794 swp = q.slice(stim_start) 4795 post = '' 4796 } else { 4797 var q_start = stim_end 4798 while (q[q_start] === '\n') q_start++ 4799 pre = q.slice(0, q_start) 4800 swp = q.slice(q_start) 4801 post = '' 4802 } 4803 if (c.anomaly){ 4804 var ea = esc(c.anomaly) 4805 // For 'surround' the anomaly is EXCLUDED from the 4806 // swap â render it as a non-swp island inside the 4807 // highlighted region. 4808 if (mode === 'surround'){ 4809 swp = swp.replace( 4810 ea, 4811 `</span><span class='fa-anom'>${ea}</span>` 4812 + `<span class='fa-swp'>`) 4813 } else { 4814 pre = pre.replace( 4815 ea, `<span class='fa-anom'>${ea}</span>`) 4816 swp = swp.replace(
4817 ea, `<span class='fa-anom'>${ea}</span>`) 4818 } 4819 } 4820 // Write-only continuation embeds its own Assistant 4821 // prefix in the question string; detect to avoid 4822 // double-rendering the turn marker. 4823 var hasAssist = /\n\nAssistant:/.test(c.question) 4824 // For full-scope (all/surround), the swap covers the 4825 // Human:/Assistant: markers too â highlight them. 4826 var full = (mode === 'all' || mode === 'surround') 4827 var headCls = full ? 'fa-swp fa-pre' : 'fa-pre' 4828 var tail = hasAssist ? '' 4829 : `<span class='${headCls}'>\n\nAssistant:</span>` 4830 // Warm-start prefix (if provided): show separately 4831 // after Assistant:, styled to distinguish unswapped- 4832 // sampled tokens from swapped generation. 4833 if (c.ws_prefix){ 4834 tail += `<span class='fa-ws'>${esc(c.ws_prefix)}</span>` 4835 } 4836 return (`<span class='${headCls}'>Human: </span>${pre}` 4837 + `<span class='fa-swp'>${swp}</span>` 4838 + `${post}${tail}`) 4839 } 4840 4841 var TASK_NOUN = { 4842 lang: 'language', code: 'programming-language', 4843 cuisine: 'cuisine', sport: 'sport', 4844 code2: 'programming-language', 4845 } 4846 4847 function renderExample(sel, d, tag){ 4848 sel.html('') 4849 var head = sel.append('div').at({class: 'fa-ex-head'}) 4850 var noun = TASK_NOUN[d.task] || d.task 4851 head.append('b').text(d.donor 4852 ? `Swapping ${noun} J-lens vector: ${d.own} â ${d.donor}` 4853 : `Ablating ${noun} J-lens vector: ${d.own} â â `) 4854 if (d.cols.some(c => c.lens_hit)){ 4855 var lm = d.lens_meta || {} 4856 var lr = lm.display_layers?.length 4857 ? `L${lm.display_layers[0]}â` 4858 + `${lm.display_layers[lm.display_layers.length-1]}` 4859 : 'Workspace layers' 4860 head.append('span').at({class: 'fa-ex-legend'}) 4861 .html( 4862 `<span class='fa-sw'></span>` 4863 + `${d.own}-token in lens top-${lm.k||10}, ${lr}` 4864 + (d.cols.some(c => c.anom_tok) 4865 ? ` · <span class='fa-anom'>underlined</span>` 4866 + ` = inserted anomaly` : '')) 4867 } 4868 var nc = d.cols.length 4869 // Group: report+flexible (workspace) come first, then 4870 // continuation+anomaly (automatic). Add a wider gap 4871 // and a dashed divider before the automatic group. 4872 var nAuto = d.cols.filter(c => 4873 /^Automatic/.test(c.title)).length 4874 var nWork = nc - nAuto 4875 var grid = nc >= 6 4876 ? {gridTemplateColumns: 4877 `repeat(${Math.ceil(nWork/2)}, 1fr) ` 4878 + `repeat(${Math.ceil(nAuto/2)}, 1fr)`, 4879 gridTemplateRows: 'auto auto', 4880 gridAutoFlow: 'column', 4881 columnGap: '18px', rowGap: '28px'} 4882 : {gridTemplateColumns: `repeat(${nc}, 1fr)`} 4883 var cols = sel.append('div').at({class: 'fa-ex-cols'}) 4884 .st(grid) 4885 // Default highlight mode (for legacy tags with no 4886 // per-column `highlight` field). 4887 var dflt = tag==='v2all' || tag==='sf_final' ? 'all' 4888 : d.question_first ? 'stim' : 'post' 4889 d.cols.forEach((c, i) => { 4890 var isAuto = /^Automatic/.test(c.title) 4891 var firstAuto = isAuto && i === nWork 4892 var col = cols.append('div').at({class: 'fa-ex-col'}) 4893 .classed('fa-ex-auto', isAuto) 4894 .classed('fa-ex-divider', firstAuto) 4895 col.append('h4.mlabel').text(c.title).at({title: c.title}) 4896 var mode = c.highlight || dflt 4897 col.append('div').at({class: 'fa-ex-prompt prompt-block'}) 4898 .html(highlightPrompt(c, mode)) 4899 var outs = col.append('div').at({class: 'fa-ex-outs'}) 4900 ;['0.0','1.0'].forEach(k => { 4901 outs.append('div').at({class: 'fa-ex-out'}) 4902 .html(`<span class='fa-alpha'>` 4903 + `α=${k==='0.0'?'0':'1'}</span>` 4904 + `<span>${esc(c.out[k] || 'â')}</span>`) 4905 }) 4906 col.append('div').at({class: 'fa-ex-verdict'}) 4907 .text(c.verdict) 4908 }) 4909 if (d.stim_gloss){ 4910 sel.append('div').at({class: 'fa-ex-gloss'}) 4911 .text(`[${d.own}] ${d.stim_gloss}` 4912 + (d.anom_gloss 4913 ? ` · anomaly: ${d.anom_gloss}` : '')) 4914 } 4915 } 4916 4917 if (ex){ 4918 d3.selectAll('.flex-auto-example').each(function(){ 4919 var sel = d3.select(this) 4920 var task = sel.attr('data-task') 4921 var tag = sel.attr('data-tag') || 'v2' 4922 var d = ex[tag]?.[task] 4923 if (d) renderExample(sel, d, tag) 4924 }) 4925 } 4926 4927 if (rd){ 4928 d3.selectAll('.flex-auto-readout').each(function(){ 4929 var sel = d3.select(this) 4930 var meta = {cats: rd.cats, tasks: rd.tasks, 4931 models: rd.models, labels: rd.labels, 4932 colors: rd.colors, 4933 model_labels: rd.model_labels} 4934 var ymax = d3.max(rd.data, r => r.mean) * 1.1 || .4 4935 barChart(sel, rd.data, meta, 4936 'Lens readout hit rate (cell, k=10)', ymax) 4937 }) 4938 } 4939} 4940 4941window.init?.() 4942 4943// âââ public/post-training/init-post-training.js âââ 4944window.initPostTraining = async function(opts){ 4945 var spec = opts?.spec || 'tylenol' 4946 var sel = d3.select(opts?.sel || '.post-training').html('') 4947 var base = (opts?.datapath || './').replace(/\/?$/, '/') 4948 var d = await util.getFile(base + spec + '.json') 4949 4950 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 4951 var PROD = util.tol.blue, BASE = util.gray[600] 4952 var sideLabel = s => s.replace(/^base model$/i, 'Base model').replace(/^base$/, 'Base model').replace('post-trained', 'Sonnet 4.5') 4953 4954 if (spec === 'tylenol') return renderTylenol() 4955 return renderCombined() 4956 4957 // ââ tylenol: 2 columns à (static prompt + 2 token bar-tables at the 4958 // single readout position). No per-token selection â readers see exactly 4959 // the keyPos readout the prose discusses; bake ships only that. 4960 function renderTylenol(){ 4961 var sideColor = t => /base/i.test(t.label) ? BASE : PROD 4962 var pair = sel.append('div.pt-pair') 4963 var cols = pair.appendMany('div.pt-col', d.cols) 4964 var pr = cols.append('div.pt-prompt.prompt-block') 4965 if (d.sysText){ 4966 pr.append('span.sys').text(d.sysText) 4967 pr.append('br'); pr.append('br') 4968 } 4969 pr.append('span.prompt-role').text('Human: ') 4970 pr.append('span').html(c => esc(c.promptBody) 4971 .replace(/(\d+mg)/, '<span class="hl">$1</span>') 4972 .replace('pain is', 'pain <span class="tok-hit">is</span>')) 4973 4974 var rt = cols.append('div.rtables') 4975 .appendMany('div.rtable', c => d3.sort(c.tables, t => !/base/i.test(t.label))) 4976 var hd = rt.append('div.pt-side') 4977 hd.append('span.swatch').st({background: sideColor}) 4978 hd.append('span').text(t => /base/i.test(t.label) ? 'Base model' : 'Post-trained model') 4979 var ro = hd.filter(t => t.rollout).append('span.pt-ro').text('â') 4980 var tip = ro.append('div.pt-ro-tip') 4981 tip.append('div.pt-ro-title').text('Completion:') 4982 tip.append('div.prompt-block').text(t => t.rollout) 4983 4984 rt.append('div.bar-table').each(function(t){ 4985 var maxLp = d3.max(t.rows, r => r.lp) 4986 var bg = util.tint(sideColor(t), 0.75) 4987 var rs = d3.select(this).appendMany('div.bar-row', t.rows) 4988 rs.append('div.bar').st({width: r => (Math.exp(r.lp - maxLp) * 100) + '%', background: bg}) 4989 rs.append('span.t').text(r => util.ppCell(r.t).trim()) 4990 rs.append('span.val').text(r => r.lp.toFixed(2)) 4991 }) 4992 } 4993 4994 // ââ combined: title/sub, 2 stacked transcripts (rank-colored), bar chart ââ 4995 function renderCombined(){ 4996 sel.append('div.fig-title').text(d.title) 4997 sel.append('div.psub').text(d.sub) 4998 var main = sel.append('div.pt-main') 4999 var left = main.append('div.pt-left') 5000 var right = main.append('div.pt-right') 5001 5002 // rank â color (whiteâorange, log scale). Same ramp as feature activations. 5003 // capped at 0.85 so dark-end text stays legible â legend uses the same ramp(). 5004 var yFloor = d.bars.yFloor 5005 var ramp = t => d3.interpolateOranges(t * 0.85) 5006 var rankT = r => Math.max(0, Math.min(1, 1 - Math.log(Math.min(r+1, yFloor))/Math.log(yFloor))) 5007 var rankColor = r => r >= yFloor ? '' : ramp(rankT(r)) 5008 // flip text darkâlight when the background is too dark to read 5009 var rankTxt = r => rankT(r) > 0.6 ? '#fff' : '' 5010 5011 var sideColor = t => t.label === 'base' ? BASE : PROD 5012 var tx = left.appendMany('div.pt-tx-wrap', d3.sort(d.transcripts, t => t.label !== 'base')) 5013 var side = tx.append('div.pt-side') 5014 side.append('span.swatch').st({background: sideColor}) 5015 side.append('span').text(t => sideLabel(t.label)) 5016 side.filter(t => t.answer).append('span.pt-side-ans') 5017 .text(t => 'Answer: ' + t.answer) 5018 var box = tx.append('div.pt-tx.prompt-block') 5019 box.append('span.sys').text(d.sysText) 5020 box.append('br'); box.append('br') 5021 box.append('span.prompt-role').text('Human: ') 5022 box.appendMany('span.tok', t => t.humanTokens) 5023 .text(e => e.s).st({background: e => rankColor(e.r), color: e => rankTxt(e.r)}) 5024 box.append('br'); box.append('br') 5025 box.append('span.prompt-role').text('Assistant: ') 5026 box.appendMany('span.tok', t => t.asstTokens) 5027 .text(e => e.s).st({background: e => rankColor(e.r), color: e => rankTxt(e.r)}) 5028 box.filter(t => t.ellipsis).append('span.ell').text(' â¦') 5029 5030 // legend: endpoint labels are HTML spans (not svg text) so they never clip 5031 var leg = left.append('div.pt-legend') 5032 leg.append('span.leg-label').text(`Token shading = J-lens rank of ${d.conceptLabel.replace(/ word$/, ' words')}`) 5033 leg.append('span.leg-end').text('1') 5034 var lw = 140, lh = 9, ln = 50 5035 leg.append('svg.colorbar').at({width: lw, height: lh}) 5036 .appendMany('rect', d3.range(ln)).at({ 5037 x: i => i*lw/ln, width: lw/ln + 1, height: lh, fill: i =>
5037 ramp(1 - i/(ln-1)), 5038 }) 5039 leg.append('span.leg-end').text(yFloor + '+') 5040 5041 // bar panel: inverted log y, geomean bars + per-item dots 5042 // sized so the chart's bottom edge lines up with the bottom of the transcripts 5043 var margin = {left: 50, right: 8, top: 8, bottom: 44} 5044 var txBottom = (tx.nodes().at(-1)?.getBoundingClientRect().bottom - left.node().getBoundingClientRect().top) || 0 5045 var chartH = Math.max(320, txBottom - margin.top - margin.bottom) 5046 var c = d3.conventions({ 5047 sel: right.append('div'), width: 220, height: chartH, margin, 5048 }) 5049 var spans = d.bars.spans, items = d.bars.items 5050 var xpos = [], xc = 0 5051 spans.forEach((sp, si) => { 5052 if (si) xc += 0.6 5053 ;['base','prod'].forEach(sd => { xpos.push({sp, sd, x: xc}); xc += 1 }) 5054 }) 5055 var x = d3.scaleLinear().domain([-0.7, xc - 0.3]).range([0, c.width]) 5056 c.y = d3.scaleLog().domain([yFloor, 0.85]).range([c.height, 0]) 5057 c.yAxis.scale(c.y).tickValues([1,2,5,10,20,50,100]).tickFormat(d3.format('d')) 5058 c.drawAxis() 5059 c.svg.select('.x').remove() 5060 util.ggPlot(c) 5061 util.addAxisLabel(c, '', 'Rank of concept words â', '', 0, -8) 5062 5063 var ttSel = d3.select('.tooltip') 5064 var bw = x(0.85) - x(0) 5065 var rk = (it, sd, sp) => Math.min(it[sd][sp] + 1, yFloor) 5066 var fmtR = v => v >= yFloor ? 'â¥' + yFloor : v 5067 var circ = {}, slope = {} 5068 xpos.forEach(p => { 5069 var gm = d.bars.geomean[p.sp][p.sd] 5070 var base = p.sd === 'prod' ? PROD : BASE 5071 c.svg.append('rect').at({ 5072 x: x(p.x) - bw/2, width: bw, 5073 y: c.y(gm), height: c.y(yFloor) - c.y(gm), 5074 fill: p.sp === 'user' ? base : util.tint(base, 0.5), 5075 }) 5076 circ[p.sp + '_' + p.sd] = c.svg.appendMany('circle.jpt', 5077 items.map((it, i) => ({it, i, sp: p.sp, sd: p.sd, v: rk(it, p.sd, p.sp)}))) 5078 .at({cx: x(p.x), cy: o => c.y(o.v), r: 1.8, 5079 fillOpacity: 0, stroke: util.gray[700], strokeWidth: 1}).nodes() 5080 }) 5081 spans.forEach((sp, si) => { 5082 var x0 = x(xpos[si*2].x), x1 = x(xpos[si*2+1].x) 5083 slope[sp] = c.svg.appendMany('path.slope', items).at({ 5084 d: it => `M ${x0} ${c.y(rk(it,'base',sp))} L ${x1} ${c.y(rk(it,'prod',sp))}`, 5085 stroke: util.gray[600], strokeWidth: 0.5, opacity: 0.25, pointerEvents: 'none', 5086 }).nodes() 5087 }) 5088 5089 var last, hovEls = [] 5090 var clearHov = () => { hovEls.forEach(n => n.classList.remove('hov')); hovEls = [] } 5091 util.nearestHover(c.svg, c.svg.selectAll('circle.jpt'), { 5092 radius: 40, 5093 onHover: (o, el, e) => { 5094 if (el !== last){ 5095 last = el 5096 clearHov() 5097 hovEls = [slope[o.sp][o.i], circ[o.sp+'_base'][o.i], circ[o.sp+'_prod'][o.i]] 5098 hovEls.forEach(n => n.classList.add('hov')) 5099 var it = o.it 5100 ttSel.classed('tooltip-hidden', false).html('').st({maxWidth: 420}) 5101 ttSel.append('div').html(`${o.sp} turn · ${it.id || ''}`) 5102 ttSel.append('table.tt-pair').html( 5103 `<tr><td class=k>Base model</td><td>rank <b>${fmtR(rk(it,'base',o.sp))}</b></td></tr>` + 5104 `<tr><td class=k>Sonnet 4.5</td><td>rank <b>${fmtR(rk(it,'prod',o.sp))}</b></td></tr>`) 5105 if (it.prompt) ttSel.append('div.prompt-block') 5106 .st({margin: '6px 0', maxHeight: '14em', overflowY: 'auto'}).text(it.prompt) 5107 var lt = it[o.sd]?.lens_topk 5108 if (lt?.tokens?.length){ 5109 ttSel.append('div').st({fontSize: 'var(--fs-label)', color: 'var(--text-light)', margin: '6px 0 3px'}) 5110 .text(`J-lens top-10 (${o.sd === 'prod' ? 'Sonnet 4.5' : 'base'}):`) 5111 var toks = ttSel.append('div.lens-topk').st({flexWrap: 'wrap'}) 5112 lt.tokens.forEach((t, ti) => toks.append('span.lenstk').text(util.ppCell(t)) 5113 .st({background: lt.hit_idx?.includes(ti) ? 'var(--hl-yellow)' : 'var(--tok-bg)'})) 5114 } 5115 } 5116 var bb = ttSel.node().getBoundingClientRect() 5117 ttSel.st({left: d3.clamp(20, e.clientX - bb.width/2, innerWidth - bb.width - 20) + 'px', 5118 top: (innerHeight > e.clientY + 20 + bb.height ? e.clientY + 20 : e.clientY - bb.height - 20) + 'px'}) 5119 }, 5120 onLeave: () => { last = null; clearHov(); ttSel.classed('tooltip-hidden', true) }, 5121 }) 5122 c.svg.appendMany('text.xtick', xpos) 5123 .each(function(p){ 5124 var [a,b] = (p.sd === 'prod' ? ['Sonnet','4.5'] : ['Base','model']) 5125 d3.select(this).text('').at({textAnchor:'middle'}).translate([x(p.x), c.height+12]) 5126 .append('tspan').text(a).at({x:0}) 5127 d3.select(this).append('tspan').text(b).at({x:0, dy:'1.1em'}) 5128 }) 5129 if (spans.length > 1){ 5130 c.svg.append('text.xgroup').text('User turn') 5131 .translate([x(0.5), c.height+38]).at({textAnchor:'middle'}) 5132 c.svg.append('text.xgroup').text('Assistant turn') 5133 .translate([x(3.1), c.height+38]).at({textAnchor:'middle'}) 5134 } 5135 } 5136} 5137 5138window.init?.() 5139 5140// âââ public/lens-inline/init-lens-inline.js âââ 5141window.initLensInline = async function(opts){ 5142 var sel = d3.select(opts?.sel || '.lens-inline').html('') 5143 var base = (opts?.datapath || './').replace(/\/?$/, '/') 5144 var manifest = await util.getFile(base + 'manifest.json?' + Date.now()) 5145 var slices = await Promise.all((manifest.slugs || []).map(s => util.loadLensSlice(base, s.slug))) 5146 5147 var FINAL = util.series[5], WORK = util.series[1], WORK2 = util.series[0] 5148 var colorOf = seg => seg.c ?? (seg.L >= 24 ? FINAL : WORK) 5149 5150 var figIntro = opts?.figIntro || manifest.figIntro 5151 var perSlug = manifest.perSlug || { 5152 avgfp_raw: { 5153 title: 'Protein Recognition', 5154 layers: [24, 20, 14], width: 340, callouts: [8, 10], 5155 cap: ['Five characters into the GFP amino acid sequence, the lens reads ', 5156 {ci:0,L:14,c:WORK2}, ', then ', {ci:1,L:14,c:WORK2}, 'escent and ', {ci:1,L:20}, '.'], 5157 }, 5158 mars: { 5159 title: 'Multihop Recall', 5160 layers: [24, 17, 12], width: 350, callouts: [-1], 5161 cap: ['The lens shows ', {ci:0,L:12,c:WORK2}, ' and ', {ci:0,L:17}, 5162 ' as intermediate steps before the answer ', {ci:0,L:24}, '.'], 5163 }, 5164 calc_ooo: { 5165 title: 'Mental Arithmetic', 5166 layers: [24, 20, 18, 14], width: 210, callouts: [-1], 5167 cap: ['The lens reveals intermediate values ', {ci:0,L:18,c:WORK2}, ' and ', {ci:0,L:20}, 5168 ', and the answer of ', {ci:0,L:24}, '.'], 5169 }, 5170 bug_code: { 5171 title: 'Bug detection', 5172 layers: [17, 13, 10], width: 260, callouts: [-1], wrap: true,
5173 cap: ['At the empty-list call the lens reads ', {ci:0,L:10,c:WORK2}, 5174 ', ', {ci:0,L:13}, ', ', {ci:0,L:17,c:FINAL}, '.'], 5175 }, 5176 face: { 5177 title: 'ASCII Face', 5178 layers: [20, 17, 13], width: 230, callouts: [14, 19], wrap: true, sideStack: true, 5179 cap: ['The lens reads ', {ci:0,L:17,c:FINAL}, ' at the "o" and ', 5180 {ci:1,L:17}, ' / ', {ci:1,L:20,c:WORK2}, ' at the "^".'], 5181 }, 5182 injection: { 5183 title: 'Prompt injection (Opus 4.5)', 5184 gloss: '[fabricated news search results inside a tool call]', 5185 layers: [21, 14, 10], width: 410, callouts: [17], wrap: true, 5186 cap: ['The lens shows the model has recognised the ', 5187 {ci:0,L:14}, ' ', {ci:0,L:10,c:WORK2}, ' ', {ci:0,L:21,c:FINAL}, '.'], 5188 }, 5189 } 5190 5191 // Layout fallback chain: explicit rows â single-row order â preferred Figure 3 5192 // order when all six slugs are present â chunk manifest.slugs into rows of 3 5193 // (lens-inline-reasoning takes this path). 5194 var bySlug = Object.fromEntries(slices.map(d => [d.slug, d])) 5195 var manSlugs = (manifest.slugs || []).map(s => s.slug).filter(s => bySlug[s]) 5196 var chunked = manSlugs.length 5197 ? [...Array(Math.ceil(manSlugs.length / 3))].map((_, i) => manSlugs.slice(i * 3, i * 3 + 3)) 5198 : null 5199 var INLINE_ROWS = [['mars', 'calc_ooo', 'avgfp_raw'], ['bug_code', 'face', 'injection']] 5200 var rows = manifest.rows 5201 || (manifest.order && [manifest.order]) 5202 || (INLINE_ROWS.flat().every(s => bySlug[s]) && INLINE_ROWS) 5203 || chunked 5204 5205 var figTitle = manifest.title ?? "The J-lens reveals the model's internal thoughts across layers" 5206 if (figTitle) sel.append('div.fig-title').text(figTitle) 5207 // "Figure N:" stays on the whole-figure <figcaption>, not panel 1's 5208 // subcaption â prime panelCaption's counter so the first-call prefix 5209 // injection (util.panelCaption n==1) never fires here. 5210 opts._pc = opts._pc || 1 5211 rows.forEach(row => { 5212 var grid = sel.append('div.figgrid') 5213 row.forEach(slug => bySlug[slug] && renderPanel(grid.append('div.cell.panel'), bySlug[slug])) 5214 }) 5215 5216 var figcap = sel.node().closest('figure')?.querySelector('figcaption') 5217 5218 // The slice-viewer link is part of the caption text (a linked phrase), not a 5219 // separate floating element. It goes on the figure-level figcaption when one 5220 // exists (article render); standalone pages append it to the first panel caption. 5221 if (opts?.sliceLink !== false){ 5222 var stackHref = (opts?.slicePath || (base.startsWith('../../') ? '../slice-stack/' : 'public/slice-stack/')) + 'index.html' 5223 var capHome = figcap ? d3.select(figcap) : sel.select('.panel .cap') 5224 capHome.select('span.slice-note').remove() 5225 capHome.append('span.slice-note') 5226 .html(` Explore these prompts in the <a class=explore href="${stackHref}">slice viewer</a>.`) 5227 } 5228 5229 function renderPanel(panel, d){ 5230 var cfg = perSlug[d.slug] || {} 5231 var title = cfg.title ?? d.title 5232 if (title) panel.append('div.mlabel').text(title) 5233 var lspec = (cfg.layers || [24, 17, 12, 6]) 5234 .map(l => typeof l === 'number' ? {L: l} : l) 5235 .filter(r => r.L < d.n_layers) 5236 var presets = (cfg.callouts || [d.default_ctx ?? d.nCtx - 1]).map(c => c < 0 ? d.nCtx - 1 : c) 5237 var state = {override: null} 5238 5239 var body = cfg.sideStack ? panel.append('div.body-row') : panel 5240 var paraSel = body.append('div.prompt-para').classed('wrap', cfg.wrap) 5241 .at({title: cfg.wrap ? null : d.ctxTokens.map(t => util.ppToken(t, true)).join('')}) 5242 if (cfg.gloss) paraSel.append('span.prompt-gloss').text(cfg.gloss + ' ') 5243 var isTrunc = i => d.ctxTokens[i] === 'â¦' 5244 var ptoks = util.appendPromptPara(paraSel, d.ctxTokens) 5245 .classed('trunc', isTrunc) 5246 if (!cfg.noExplore) ptoks.on('click', (e, i) => { 5247 if (isTrunc(i)) return 5248 var pi = d3.minIndex(presets, p => Math.abs(p - i)) 5249 state.override = (presets[pi] === i) ? null : {pi, ctx: i} 5250 render() 5251 }) 5252 else ptoks.st({cursor: 'default'}) 5253 5254 // Fixed widths only apply â¥768px (style.css); stacked mobile panels fill the card. 5255 // A prompt line that nearly fits the configured width must never wrap â font 5256 // metrics drift a few px across platforms/zoom and the break lands mid-verse 5257 // (",âµ" stranded on its own line). Widen to the prompt's natural line width when 5258 // it's close; genuinely-wrapping prompts (long lines, > +80px) keep the configured width. 5259 function panelWidth(){ 5260 if (!cfg.wrap) return cfg.width 5261 var n = paraSel.node() 5262 n.style.cssText = 'position:absolute;width:max-content;white-space:pre' 5263 var need = n.offsetWidth + 3 5264 n.style.cssText = '' 5265 return need > cfg.width && need < cfg.width + 80 ? need : cfg.width 5266 } 5267 if (cfg.width){ 5268 panel.style('--panel-w', panelWidth() + 'px') 5269 // re-measure once webfonts land â fallback-font metrics differ slightly 5270 document.fonts?.ready.then(() => panel.style('--panel-w', panelWidth() + 'px')) 5271 } 5272 5273 var swoop = panel.append('svg.swoop') 5274 var stacks = body.append('div.stacks').appendMany('div.stack', d3.range(preset
5274s.length)) 5275 var capColors = new Map((cfg.cap || []).filter(s => s.L != null) 5276 .map(s => [s.t ?? d.argmax(presets[s.ci], s.L), colorOf(s)])) 5277 // L100 reads as a layer count; cap the display at L99 here (paper-wide 5278 // axis ticks still end at 100 â this is a per-figure label override). 5279 var llabel = i => { var d = util.layerLabel(i); return 'L' + (d == 100 ? 99 : d) } 5280 stacks.each(function(pi){ 5281 var rows = d3.select(this).appendMany('div.layer-row', lspec) 5282 rows.append('span.llabel').text(r => llabel(r.L)) 5283 rows.append('span.top1') 5284 rows.append('span.rank').text(r => r.rank > 1 ? '(' + util.ppRank(r.rank) + ')' : '') 5285 }) 5286 5287 var capSel = util.panelCaption(panel, opts, '').classed('cap', true) 5288 if (figIntro && !opts._fi) { capSel.append('span').text(figIntro + ' '); opts._fi = 1 } 5289 ;(cfg.cap || []).forEach(seg => { 5290 if (typeof seg === 'string') return capSel.append('span').text(seg) 5291 var disp = seg.t ? util.ppCell(seg.t).trim() : tokAt(presets[seg.ci], seg.L).trim() 5292 capSel.append('span').html(`"<span class=ckey style="color:${colorOf(seg)}">${disp}</span>"`) 5293 }) 5294 function ctxAt(pi){ return state.override?.pi === pi ? state.override.ctx : presets[pi] } 5295 function tokAt(ctx, L){ 5296 var raw = d.argmax(ctx, L) 5297 var s = util.ppCell(raw) 5298 return (d.upwordAt?.[ctx] && raw && !/^[â½Â·]/.test(raw)) ? s.toUpperCase() : s 5299 } 5300 5301 function render(){ 5302 var shown = d3.range(presets.length).map(ctxAt) 5303 ptoks.classed('sel', i => shown.includes(i)) 5304 stacks.each(function(pi){ 5305 d3.select(this).selectAll('.top1').each(function(r){ 5306 var ctx = r.ctx ?? ctxAt(pi) 5307 var raw = r.t ?? d.argmax(ctx, r.L) 5308 var c = r.c ?? capColors.get(raw) 5309 var disp = util.ppCell(raw) 5310 if (d.upwordAt?.[ctx] && raw && !/^[â½Â·]/.test(raw)) disp = disp.toUpperCase() 5311 if (r.cap1) disp = disp.replace(/\S/, c => c.toUpperCase()) 5312 d3.select(this).text(disp).at({title: disp}).st({color: c || 'var(--text-light)'}) 5313 }) 5314 }) 5315 requestAnimationFrame(() => layoutStacks(shown)) 5316 } 5317 5318 function layoutStacks(shown){ 5319 var pn = panel.node(), pr = pn.getBoundingClientRect() 5320 // fitMount may have scaled the whole mount; getBoundingClientRect is 5321 // post-transform but SVG user units are pre-transform, so unscale. 5322 var k = pn.offsetWidth / pr.width || 1 5323 var paraR = paraSel.node().getBoundingClientRect() 5324 var maxTx = Math.round((paraR.right - pr.left) * k) - 8 5325 swoop.at({width: pn.offsetWidth}).html('') 5326 // connector runs UNDER the prompt text (.ptok sits above with a bg-colored 5327 // text stroke, style.css), so no white halo needed on the line itself 5328 var connector = path => 5329 swoop.append('path').at({d: path, fill: 'none', stroke: util.gray[500], strokeWidth: 1}) 5330 stacks.each(function(pi){ 5331 var tok = ptoks.filter(i => i === shown[pi]).node() 5332 if (!tok) return 5333 // a wrapped inline token has one rect per line fragment â anchor the 5334 // swoop to the last fragment so it points at the visible word, not the 5335 // empty trailing box on the previous line. 5336 var rects = tok.getClientRects() 5337 var t = rects[rects.length - 1] || tok.getBoundingClientRect() 5338 var tx = Math.min(Math.round((t.left + t.width/2 - pr.left) * k), maxTx) 5339 var s = this.getBoundingClientRect() 5340 if (cfg.sideStack) { 5341 var ty = Math.round((t.top + t.height/2 - pr.top) * k) 5342 var x1 = Math.round((s.left - pr.left) * k) - 1 5343 var y1 = Math.round((s.top + s.height/2 - pr.top) * k) 5344 var mx = Math.round((tx + t.width*k/2 + x1) / 2) 5345 connector(`M${tx + t.width*k/2 + 1},${ty} C${mx},${ty} ${mx},${y1} ${x1},${y1}`) 5346 } else { 5347 var x1 = Math.round((s.left - pr.left) * k) + 18 5348 var y0 = Math.round((t.bottom - pr.top) * k) + 1, y1 = Math.round((s.top - pr.top) * k) 5349 var my = Math.round((y0 + y1) / 2) 5350 connector(`M${tx},${y0} C${tx},${my} ${x1},${my} ${x1},${y1}`) 5351 } 5352 }) 5353 } 5354 5355 panel.node().__relayout = () =>
5355 layoutStacks(d3.range(presets.length).map(ctxAt)) 5356 render() 5357 } 5358 5359 var mount = sel.node() 5360 mount.__liRO?.disconnect() 5361 mount.__liRO = new ResizeObserver(util.throttleDebounce(() => 5362 sel.selectAll('.panel').each(function(){ this.__relayout?.() }), 80)) 5363 mount.__liRO.observe(mount.closest('figure') || mount) 5364} 5365 5366window.init?.() 5367 5368// âââ public/methods-qualitative/init-methods-qualitative.js âââ 5369window.initMethodsQualitative = async ({datapath, state}) => { 5370 const sel = d3.select('.methods-qualitative').html('') 5371 const data = await util.getFile(datapath + 'qualitative.json') 5372 const LABEL = {jacobian: 'J-lens', logit: 'Logit', tuned: 'Tuned'} 5373 const TITLE = {protein: 'Green Fluorescent Protein'} 5374 const ttSel = d3.select('.tooltip') 5375 5376 const FINAL = util.series[0], WORK = util.series[1] 5377 // ppCell leaves \t as a raw tab (renders as blank); show a glyph instead 5378 const ppTok = t => util.ppCell(t).replace(/\t/g, 'â¥') 5379 // Per-panel token â category. Matched against ppCell(t).trim().toLowerCase(). 5380 // 'final' = the token the model actually outputs at this position; 5381 // 'work' = intermediates related to the computation; everything else is noise. 5382 const CAT = { 5383 multihop: { 5384 final: ['red', 'rust'], 5385 work: ['mars', 'color', 'colour'], 5386 }, 5387 poetry: { 5388 final: ['he'], 5389 work: ['rabbit', 'rabb'], 5390 }, 5391 multilingual: { 5392 final: ['大'], 5393 work: ['big', 'bigger', 'large', 'larger', 'болÑÑ', 'opposite', 'opposites', 'oppos', 'chinese'], 5394 }, 5395 protein: { 5396 final: ['g'], 5397 work: ['green', 'fluor', 'fluorescent', 'lumin', 'color', 'colors', 'light', 'nobel'], 5398 }, 5399 face: { 5400 final: ['', '^', '|'], 5401 work: ['face', 'faces', 'facial', 'nose', 'mouth', 'emoji'], 5402 }, 5403 compute: { 5404 final: ['edly'], 5405 work: ['seven', '7', 'nine', '9', 'math', 'arithmetic', 'square', 'squared', 'calcul'], 5406 }, 5407 } 5408 function catColor(panel, t){ 5409 // ppCell turns all-whitespace into ⣠glyphs; normalise back to '' so a 5410 // panel can list '' in final to mean "any whitespace token". 5411 const s = util.ppCell(t).replaceAll('â£', ' ').trim().toLowerCase() 5412 const c = CAT[panel] || {} 5413 if (c.final?.includes(s)) return FINAL 5414 if (c.work?.includes(s)) return WORK 5415 return null 5416 } 5417 5418 const pickRows = rows => rows.filter((r, i) => i % 2 == 0) 5419 5420 const panels = sel.append('div.mq-grid') 5421 .appendMany('div.mq-panel', data.panels) 5422 5423 panels.append('div.fig-title').text(d => TITLE[d.name] || d.title) 5424 5425 // one table per panel: rows = layers (deepâshallow), cols = methods. 5426 // Clicking a prompt token swaps the table to that position's readouts. 5427 panels.each(function (panel) { 5428 const psel = d3.select(this) 5429 const positions = panel.positions || {[panel.keyPos]: panel.rows} 5430 let pos = panel.keyPos 5431 5432 const prompt = psel.append('div.prompt-block.mq-prompt') 5433 if (panel.tokens) { 5434 const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 5435 const ptoks = prompt.appendMany('span.ptok', panel.tokens) 5436 .html(d => esc(d.s || '·').replace(/\n/g, 'âµ<br>')) 5437 .classed('mq-nopos', d => !positions[d.pos]) 5438 .on('click', (e, d) => { if (positions[d.pos]) { pos = d.pos; draw() } }) 5439 var syncToks = () => ptoks.classed('tok-hit', d => d.pos == pos) 5440 } else { 5441 prompt.html(panel.promptHtml 5442 .replace(/^<div class="promptbox"><b>Prompt:<\/b>\s?/, '') 5443 .replace(/<\/div>$/, '')) 5444 .selectAll('.ktok').classed('tok-hit', true) 5445 var syncToks = () => {} 5446 } 5447 5448 const t = psel.append('table.mq-table') 5449 const hr = t.append('thead').append('tr') 5450 hr.append('th') 5451 hr.appendMany('th.mq-mhdr', data.methods).text(m => LABEL[m] || m) 5452 const tb = t.append('tbody') 5453 5454 function draw(){ 5455 syncToks() 5456 tb.html('') 5457 pickRows(positions[pos]).forEach(r => { 5458 const tr = tb.append('tr') 5459 tr.append('td.mq-layer').text('L' + util.layerLabel(r.layer)) 5460 data.methods.forEach(m => { 5461 if (!r[m].k?.length) return tr.append('td.mq-cell.mq-noise').text('â') 5462 const c = catColor(panel.name, r[m].t) 5463 const d = {...r[m], layer: r.layer, method: m} 5464 tr.append('td.mq-cell') 5465 .text(ppTok(d.t)) 5466 .classed('mq-noise', !c) 5467 .st({color: c}) 5468 .datum(d) 5469 .call(d3.attachTooltip, ttSel, []) 5470 .on('mouseover', (e, d) => { 5471 ttSel.html('') 5472 const tt = ttSel.append('div.mq-tt') 5473 tt.append('div.mq-tt-hdr').text(`L${util.layerLabel(d.layer)} · ${LABEL[d.method]} lens`) 5474 const tbl = tt.append('table')
5475 d.k.forEach((x, i) => { 5476 const tr = tbl.append('tr') 5477 tr.append('td.r').text(i + 1) 5478 tr.append('td.t').text(ppTok(x.t)).st({color: catColor(panel.name, x.t)}) 5479 tr.append('td.p').text(x.p.toFixed(1) + '%') 5480 }) 5481 }) 5482 }) 5483 }) 5484 } 5485 draw() 5486 }) 5487 5488 util.colorCaption(sel, {'predicted next token': FINAL, intermediates: WORK}) 5489} 5490 5491window.init?.() 5492 5493// âââ public/jlens-audit/init-jlens-audit.js âââ 5494!function(){ 5495 5496// J-lens keeps the paper's lens blue; NLA/SAE are competitor readouts. 5497var METHODS = [ 5498 {key: 'nla', label: 'NLA (used for ground truth)', color: util.tol.magenta}, 5499 {key: 'jlens', label: 'J-lens', color: util.tol.blue}, 5500 {key: 'sae', label: 'SAE features', color: util.gray[500]}, 5501] 5502 5503var CASE_LINES = { 5504 'Evaluation awareness': ['Evaluation', 'awareness'], 5505 'Planning in poetry': ['Planning', 'in poetry'], 5506 'Misreported tool calls': ['Misreported', 'tool calls'], 5507 'Answer thrashing': ['Answer', 'thrashing'], 5508 'Language switching': ['Language', 'switching'], 5509 'Reward hacking': ['Reward', 'hacking'], 5510} 5511 5512function xlab(c, tx, lines){ 5513 var t = c.svg.append('text.xlab').at({x: tx, y: c.height + 14, textAnchor: 'middle'}) 5514 lines.forEach((ln, li) => t.append('tspan').text(ln).at({x: tx, dy: li ? 13 : 0})) 5515} 5516 5517// Solid bar (mean) + open per-item dots spread across the bar + optional SEM whisker. 5518function barDots(c, bx, bw, mt, mean, pts, sem){ 5519 c.svg.append('rect').at({ 5520 x: bx, y: c.y(mean), width: bw, height: c.height - c.y(mean), fill: mt.color, 5521 }) 5522 var cx = bx + bw/2 5523 if (sem) util.barWhisker(c.svg, {x: cx, lo: c.y(mean - sem), hi: c.y(mean + sem)}) 5524 // open per-item dots, same convention as misalign-lens jitter dots 5525 var jw = bw*0.55 5526 c.svg.appendMany('circle.jpt', pts || []) 5527 .at({cx: (_, i) => cx + ((i / Math.max((pts.length - 1), 1)) - 0.5) * jw, 5528 cy: p => c.y(p), r: 2.2, fillOpacity: 0, stroke: util.gray[700], strokeWidth: 1}) 5529} 5530 5531window.initJlensAudit = async function(opts){ 5532 var sel = d3.select('.jlens-audit').html('') 5533 var base = (opts?.datapath || './').replace(/\/?$/, '/') 5534 var d = await util.getFile(base + 'data.json') 5535 5536 var row = sel.append('div.panel-row') 5537 5538 // Shrink toward the container on narrow screens so fitMount's scale stays 5539 // readable; desktop keeps the full 620px chart. 5540 var availW = sel.node().closest('figure')?.clientWidth || 620 5541 var chartW = d3.clamp(380, availW - 54, 620) 5542 5543 // Grouped bars per case; dots = per-claim means (n=10 seeds each). 5544 var cb = d3.conventions({ 5545 sel: row.append('div.panel').append('div'), 5546 width: chartW, height: 210, 5547 margin: {left: 46, top: 8, right: 8, bottom: 36}, 5548 }) 5549 cb.y.domain([0, 10]) 5550 cb.yAxis.ticks(5); cb.drawAxis(); cb.svg.select('.x').remove() 5551 util.ggPlot(cb) 5552 util.addAxisLabel(cb, '', 'Investigator score') 5553 var x0 = d3.scaleBand().domain(d.cases.map(cs => cs.id)).range([0, cb.width]).padding(0.22) 5554 var x1 = d3.scaleBand().domain(METHODS.map(mt => mt.key)).range([0, x0.bandwidth()]).padding(0.12) 5555 d.cases.forEach(cs => { 5556 var gx = x0(cs.id) 5557 METHODS.forEach(mt => { 5558 var s = cs.scores[mt.key] 5559 barDots(cb, gx + x1(mt.key), x1.bandwidth(), mt, s.mean, s.claims, 0) 5560 }) 5561 xlab(cb, gx + x0.bandwidth()/2, CASE_LINES[cs.label] || [cs.label]) 5562 }) 5563 5564 var leg = sel.append('div.legend-row') 5565 METHODS.forEach(mt => { 5566 var item = leg.append('div.legend-item') 5567 item.append('span.legend-sq').st({background: mt.color}) 5568 item.append('span').text(mt.label) 5569 }) 5570} 5571 5572}() 5573window.init?.() 5574 5575// âââ public/jlens-rm-bias/init-jlens-rm-bias.js âââ 5576window.initJlensRmBias = async function(opts){ 5577 var sel = d3.select('.jlens-rm-bias').html('') 5578 var base = (opts?.datapath || './').replace(/\/?$/, '/') 5579 var d = await util.getFile(base + 'data.json') 5580 5581 var wordColor = {reward: util.tol.orange, bias: util.tol.red} 5582 var setLabel = {neutral: 'Neutral', quirk_eliciting: 'Quirk-eliciting', goal_probing: 'Goal-probing'} 5583 var ctlLabel = { 5584 'RM bias model\n(Jacobian lens)': 'RM bias model,\nJ-lens', 5585 'RM bias model\n(Logit lens)': 'RM bias model,\nlogit lens', 5586 'Baseline\n(Jacobian lens)': 'Baseline model,\nJ-lens', 5587 'Baseline\n(Logit lens)': 'Baseline model,\nlogit lens', 5588 } 5589 var ctlLabelM = { 5590 'RM bias model\n(Jacobian lens)': 'RM bias\nmodel,\nJ-lens', 5591 'RM bias model\n(Logit lens)': 'RM bias\nmodel,\nlogit lens', 5592 'Baseline\n(Jacobian lens)': 'Baseline\nmodel,\nJ-lens', 5593 'Baseline\n(Logit lens)': 'Baseline\nmodel,\nlogit lens', 5594 } 5595 var cw = Math.min(sel.node().parentNode.getBoundingClientRect().width || 800, 5596 (document.documentElement.clientWidth || 800) - 36) 5597 var mobile = cw < 700 5598 var pw = Math.min(420, Math.max(240, cw - 60)) 5599 var colorTitle = t => t.replace(/"(reward|bias)"/g, (_, w) => `"<span style="color:${wordColor[w]}">${w}</span>"`) 5600 5601 var row = sel.append('div.row') 5602 5603 function drawDotWhisker(parent, groups, series, raw, prompts, title, ylabel, w, h){ 5604 var panel = parent.append('div.panel') 5605 panel.append('div.fig-title').html(colorTitle(title)) 5606 var c = d3.conventions({ 5607 sel: panel.append('div'), 5608 width: w, height: h, margin: {left: 48, top: 8, right: 8, bottom: mobile ? 58 : 44}, 5609 }) 5610 var allPts = raw ? Object.values(raw).flat(2) : []
5611 var ymax = d3.max(Object.values(series).flat().concat(allPts)) || 1 5612 var x0 = d3.scaleBand().domain(d3.range(groups.length)).range([0, c.width]).padding(0.18) 5613 var x1 = d3.scaleBand().domain(Object.keys(series)).range([0, x0.bandwidth()]).paddingInner(0.18) 5614 c.y.domain([0, ymax]).nice() 5615 c.yAxis.ticks(5); c.drawAxis(); c.svg.select('.x').remove() 5616 util.ggPlot?.(c) 5617 util.addAxisLabel?.(c, '', ylabel) 5618 5619 groups.forEach((g, gi) => { 5620 var gx = x0(gi) 5621 Object.entries(series).forEach(([sn, vals]) => { 5622 var bx = gx + x1(sn), bw = x1.bandwidth(), cx = bx + bw/2 5623 var v = vals[gi], col = wordColor[sn] 5624 c.svg.append('rect').at({x: bx, width: bw, y: c.y(v), height: c.height - c.y(v), fill: col}) 5625 if (raw && raw[sn]){ 5626 var pts = raw[sn][gi], sem = d3.deviation(pts)/Math.sqrt(pts.length) || 0 5627 util.barWhisker(c.svg, {x: cx, lo: c.y(v - sem), hi: c.y(v + sem), stroke: util.gray[900]}) 5628 var jw = bw*0.55, pr = prompts?.[gi] 5629 c.svg.appendMany('circle.jpt', pts.map((p, i) => ({p, sn, col, pr: pr?.[i]}))) 5630 .at({cx: (_, i) => cx + (i/(pts.length-1||1) - 0.5)*jw, cy: o => c.y(o.p), r: 2.2, 5631 fillOpacity: 0, stroke: '#000', strokeWidth: 1}) 5632 } 5633 }) 5634 var lines = g.split('\n'), tx = gx + x0.bandwidth()/2 5635 var t = c.svg.append('text.xlab').at({x: tx, y: c.height + 14, textAnchor: 'middle'}) 5636 lines.forEach((ln, li) => t.append('tspan').text(ln).at({x: tx, dy: li ? 13 : 0})) 5637 }) 5638 } 5639 5640 var ctlRaw = { 5641 reward: d.control_2x2.map(c => c.reward_pts), 5642 bias: d.control_2x2.map(c => c.bias_pts), 5643 } 5644 var hasPts = ctlRaw.reward.every(Boolean) 5645 drawDotWhisker( 5646 row, d.control_2x2.map(c => (mobile ? ctlLabelM : ctlLabel)[c.label] || c.label), 5647 hasPts 5648 ? {reward: ctlRaw.reward.map(p => d3.mean(p)), bias: ctlRaw.bias.map(p => d3.mean(p))} 5649 : {reward: d.control_2x2.map(c => c.reward), bias: d.control_2x2.map(c => c.bias)}, 5650 hasPts ? ctlRaw : undefined, 5651 d.control_2x2.map(c => c.prompts), 5652 `"reward" and "bias" in top-25 J-lens readouts`, 5653 'Appearances â', mobile ? pw : 380, 220, 5654 ) 5655 5656 var sets = Object.keys(d.per_set).sort((a, b) => d.per_set[b].reward.mean - d.per_set[a].reward.mean) 5657 drawDotWhisker( 5658 row, sets.map(s => setLabel[s] || s), 5659 {reward: sets.map(s => d.per_set[s].reward.mean), bias: sets.map(s => d.per_set[s].bias.mean)}, 5660 {reward: sets.map(s => d.per_set[s].reward.points), bias: sets.map(s => d.per_set[s].bias.points)}, 5661 sets.map(s => d.per_set[s].prompts), 5662 'Breakdown per prompt type', 5663 'Appearances â', mobile ? pw : 280, 220, 5664 ) 5665 5666 util.colorCaption(sel, wordColor) 5667} 5668window.init?.() 5669 5670// âââ public/jlens-rm-bias-examples/init-jlens-rm-bias-examples.js âââ 5671window.initJlensRmBiasExamples = async function(opts){ 5672 var sel = d3.select('.jlens-rm-bias-examples').html('') 5673 var base = (opts?.datapath || './').replace(/\/?$/, '/') 5674 var d = await util.getFile(base + 'data.json') 5675 5676 var setLabel = {neutral: 'Neutral', quirk_eliciting: 'Quirk-eliciting', goal_probing: 'Goal-probing'} 5677 var sets = Object.keys(d.examples) 5678 5679 var er = sel.append('div.ex-row.figgrid') 5680 sets.forEach(s => { 5681 var ex = d.examples[s] 5682 var card = er.append('div.card.cell') 5683 card.append('div.set.mlabel').text(setLabel[s] || s) 5684 // Lens is read at the ":" after Assistant â outline that position. 5685 var pb = card.append('div.prompt-block') 5686 pb.append('span.prompt-role').text('Human: ') 5687 pb.append('span').text(ex.prompt) 5688 pb.append('span.prompt-role').text('\n\nAssistant') 5689 pb.append('span.tok-hit').text(':') 5690 card.append('div.toks-label.mlabel').text('Top J-lens readout') 5691 var toks = card.append('div.toks') 5692 // Drop tokens that contain U+FFFD (partial multibyte byte sequences) or 5693 // decode to empty after marker stripping â they're tokenizer artifacts. 5694 var kept = ex.tokens.map((t, i) => ({t, hit: ex.hit_idx.includes(i)})) 5695 .filter(o => !/�/.test(o.t) && (util.ppToken?.(o.t) ?? o.t).trim() !== '') 5696 kept.forEach(o => {
5697 toks.append('span.tok').text(util.ppToken?.(o.t) ?? o.t).classed('hl', o.hit) 5698 }) 5699 }) 5700} 5701window.init?.() 5702 5703// âââ public/pref-violation-lens/init-pref-violation-lens.js âââ 5704window.initPrefViolationLens = async function(opts){ 5705 var sel = d3.select('.pref-violation-lens').html('') 5706 var base = (opts?.datapath || './').replace(/\/?$/, '/') 5707 var d = await util.getFile(base + 'data.json') 5708 5709 var BASE = util.gray[600], POST = util.tol.blue 5710 5711 // ââ top row: two example columns (baked HTML fragments) ââ 5712 var top = sel.append('div.pvl-examples') 5713 d.examples.forEach(h => top.append('div.pvl-ex').html(h)) 5714 // append the example layer to the Base/Post-trained mini-table headers 5715 top.selectAll('.thd span:last-child').each(function(){ 5716 d3.select(this).append('span.exly').text(` (${d.ex_layer})`) 5717 }) 5718 5719 // ââ bottom row: d3 grouped-bar panels ââ 5720 sel.append('div.fig-title.pvl-title') 5721 .text('Conflict signal in J-lens, base vs post-trained') 5722 var bot = sel.append('div.pvl-bars') 5723 var rng = d3.randomLcg(7) 5724 d.panels.forEach((p, pi) => { 5725 var W = 420, H = 240 5726 var c = d3.conventions({sel: bot.append('div'), width: W, height: H, 5727 margin: {left: 44, right: 8, top: 24, bottom: 44}}) 5728 var x = d3.scaleBand().domain(d3.range(p.groups.length)).range([0, c.width]) 5729 .paddingInner(0.35).paddingOuter(0.12) 5730 var bw = x.bandwidth(), barW = bw*0.46, dx = bw*0.25 5731 c.y.domain([0, p.ymax]) 5732 c.yAxis.ticks(5) 5733 c.drawAxis() 5734 c.svg.select('.x').remove() 5735 util.ggPlot(c) 5736 util.addAxisLabel(c, '', `${p.label} in J-lens (% lens prob) â`) 5737 5738 if (pi === 0){ 5739 var lg = c.svg.append('g.pvl-lg').translate([6, 6]) 5740 ;[['Base model', BASE], ['Post-trained model', POST]].forEach(([t, col], i) => { 5741 lg.append('rect').at({x: 0, y: i*16, width: 10, height: 10, rx: 2, fill: col}) 5742 lg.append('text').text(t).at({x: 15, y: i*16 + 9}) 5743 }) 5744 } 5745 5746 p.groups.forEach((g, gi) => { 5747 var cx = x(gi) + bw/2 5748 ;[['base', -dx, BASE], ['post', dx, POST]].forEach(([m, off, fill]) => { 5749 var v = g[m] 5750 c.svg.append('rect').at({x: cx + off - barW/2, width: barW, 5751 y: c.y(v.mean), height: c.height - c.y(v.mean), fill}) 5752 c.svg.appendMany('circle', v.pts) 5753 .at({cx: pt => cx + off + (rng() - 0.5)*barW*0.5, cy: pt => c.y(pt), 5754 r: 2.2, fill: util.gray[900], opacity: 0.3}) 5755 }) 5756 // 2-line group label 5757 var lbl = c.svg.append('text.glab').at({x: cx, y: c.height + 14, textAnchor: 'middle'}) 5758 g.label.split(' ').forEach((w, i) => 5759 lbl.append('tspan').text(w).at({x: cx, dy: i ? '1.15em' : 0})) 5760 }) 5761 }) 5762 5763 var cap = sel.node().closest('figure')?.querySelector('figcaption') 5764 if (cap) cap.innerHTML = cap.innerHTML.replace(/ \((?:blue|orange)\)/g, '') 5765 util.colorCaption(sel, { 5766 'base': {bg: BASE, color: '#fff', firstOnly: true}, 5767 'post-trained': {bg: POST, color: '#fff', firstOnly: true}, 5768 }) 5769} 5770 5771window.init?.() 5772 5773// âââ public/layer-diagram/init-layer-diagram.js âââ 5774window.initLayerDiagram = async function({datapath}){ 5775 var sel = d3.select('.layer-diagram').html('') 5776 var d = await util.getFile(datapath + 'cka.json') 5777 5778 // Visual vocab matched to intro-structural so the two schematics read as one. 5779 var gray = util.gray, manilla = util.brand.manilla 5780 var jEdge = d3.color(util.brand.kraft).darker(0.5).formatHex() 5781 var C = { 5782 soft: gray[100], softEdge: gray[300], 5783 jBand: manilla, jEdge: jEdge, 5784 blockBg: util.tint(manilla, 0.55), blockEdge: util.tint(manilla, 0.15), 5785 arrow: gray[400], text: gray[700], textFade: gray[400], annot: gray[500], 5786 } 5787 5788 function wrap(s, w){ 5789 var out = [], line = '' 5790 s.split(/\s+/).forEach(t => { 5791 if (line && (line + ' ' + t).length > w){ out.push(line); line = t } 5792 else line = line ? line + ' ' + t : t 5793 }) 5794 if (line) out.push(line) 5795 return out 5796 } 5797 5798 // narrow containers (mobile / single gallery column): both panels are sized 5799 // to the figure's content width so neither overflows and gets clipped by the 5800 // centered .ld-row â the schematic scales via viewBox, the heatmap redraws 5801 // smaller with a horizontal colorbar below it. The figure's width can settle 5802 // after first render (gallery card layout / scrollbars), so re-render when it 5803 // changes instead of trusting a single early measurement.
5804 var fig = sel.node().closest('figure') 5805 var availW, narrow, lastW = -1 5806 5807 function render(){ 5808 var cs = fig && getComputedStyle(fig) 5809 availW = fig ? fig.clientWidth - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0) : 1e6 5810 if (!availW || availW < 0) availW = 1e6 5811 if (Math.abs(availW - lastW) < 2) return 5812 lastW = availW 5813 narrow = availW < 560 5814 sel.html('') 5815 var row = sel.append('div.ld-row') 5816 drawDiagram(row.append('div.ld-diagram')) 5817 drawHeatmap(row.append('div.ld-heatmap')) 5818 } 5819 render() 5820 if (fig){ 5821 fig.__ldRO?.disconnect() 5822 fig.__ldRO = new ResizeObserver(util.throttleDebounce(render, 200)) 5823 fig.__ldRO.observe(fig) 5824 } 5825 5826 // âââââ left: layer schematic â sliver/band/J-space shared with intro-structural âââââ 5827 function drawDiagram(host){ 5828 var NL = 6, WS = [3, 5] 5829 var W = 400, H = 420 5830 var svg = host.append('svg').at({width: W, height: H, viewBox: `0 0 ${W} ${H}`}) 5831 if (narrow && availW < W) svg.at({width: availW, height: Math.round(H*availW/W)}) 5832 5833 function rrect(x, y, w, h, r, attrs){ 5834 return svg.append('rect').at({x, y, width: w, height: h, rx: r, ry: r, ...attrs}) 5835 } 5836 function brace(x, y0, y1, bw){ 5837 var ym = (y0 + y1) / 2 5838 svg.append('path.brace').at({fill: 'none', stroke: C.arrow, d: 5839 `M ${x+bw},${y0} Q ${x},${y0} ${x},${y0+bw} V ${ym-bw}` + 5840 ` Q ${x},${ym} ${x-bw},${ym} Q ${x},${ym} ${x},${ym+bw}` + 5841 ` V ${y1-bw} Q ${x},${y1} ${x+bw},${y1}`}) 5842 } 5843 5844 var g = util.drawJSliver(svg, {bx0: 132, bx1: 252, H, capH: 24, pad: 14, 5845 jFrac: [(NL - WS[1])/NL, (NL - WS[0] + 1)/NL], arrowId: 'ld-arrow'}) 5846 var {cx, bx0, bx1, rTop, rBot, jTop, jBot, sliverL} = g 5847 var seg = (rBot - rTop)/NL 5848 5849 // left annotations: motor/sensory braces outside, workspace inside w/ dashed leader 5850 var bwBrace = 8, tip = 3 5851 var bxBrace = bx0 - bwBrace - tip, txtX = bxBrace - bwBrace - 8 5852 function annoText(ym, label, desc){ 5853 var lines = [label, ...wrap(desc, 16)] 5854 var ty0 = ym - (lines.length - 1) * 7 5855 var txt = svg.append('text').at({textAnchor: 'end', x: txtX}) 5856 lines.forEach((t, i) => txt.append(i === 0 ? 'tspan.anno-key' : 'tspan.anno-sub') 5857 .text(t).at({x: txtX, y: ty0 + i*14, dy: '.35em'})) 5858 } 5859 ;[{y0: rTop, y1: jTop, ph: d.phases[2]}, {y0: jBot, y1: rBot, ph: d.phases[0]}] 5860 .forEach(a => { 5861 brace(bxBrace, a.y0 + tip, a.y1 - tip, bwBrace) 5862 annoText((a.y0 + a.y1)/2, a.ph.label, a.ph.desc) 5863 }) 5864 var wym = (jTop + jBot) / 2, ibx = sliverL - bwBrace - tip 5865 brace(ibx, jTop + tip, jBot - tip, bwBrace) 5866 svg.append('line').at({x1: ibx - bwBrace, y1: wym, x2: txtX + 4, y2: wym, 5867 stroke: C.arrow, strokeWidth: 1, strokeDasharray: '3 3'}) 5868 annoText(wym, d.phases[1].label, d.phases[1].desc) 5869 5870 // âââ¦ââ layer chips on the right (layer-diagram extra) 5871 var lx = bx1 + 16, lw = 48, lh = 22, lxc = lx + lw/2
5872 d3.range(NL).forEach(i => { 5873 var ly = rBot - (i + 0.5)*seg, ws = i+1 >= WS[0] && i+1 <= WS[1] 5874 svg.append('path').at({d: `M${bx1},${ly+seg*0.28} C${bx1+12},${ly+seg*0.28} ${lxc},${ly+lh/2+4} ${lxc},${ly+lh/2}`, 5875 fill: 'none', stroke: C.arrow, strokeWidth: 0.9}) 5876 svg.append('path').at({d: `M${lxc},${ly-lh/2} C${lxc},${ly-lh/2-4} ${bx1+12},${ly-seg*0.28} ${bx1+1},${ly-seg*0.28}`, 5877 fill: 'none', stroke: C.arrow, strokeWidth: 0.9, markerEnd: 'url(#ld-arrow)'}) 5878 rrect(lx, ly - lh/2, lw, lh, 4, {fill: C.blockBg, stroke: C.blockEdge, strokeWidth: 1}) 5879 var tb = svg.append('text.block').translate([lxc, ly]) 5880 .at({textAnchor: 'middle', dy: '.35em', fill: ws ? C.text : C.textFade}) 5881 tb.append('tspan').text('â') 5882 tb.append('tspan').text(i+1).at({dy: '0.25em', fontSize: '75%'}) // lint-ok inline-font-js: subscript 5883 }) 5884 } 5885 5886 // âââââ right: CKA heatmap with workspace band + phase brackets âââââ 5887 function drawHeatmap(host){ 5888 var n = d.n_layers 5889 var margin = narrow ? {left: 40, right: 90, top: 50, bottom: 100} 5890 : {left: 44, right: 184, top: 28, bottom: 40} 5891 var size = narrow ? Math.min(400, availW) - margin.left - margin.right : 320 5892 var cell = size/n 5893 var c = d3.conventions({sel: host, width: size, height: size, margin, layers: 'cs'}) 5894 var ctx = c.layers[0], svg = c.layers[1] 5895 5896 var color = d3.scaleSequential(d3.interpolateViridis).domain([0, 1]) 5897 for (var i = 0; i < n; i++) for (var j = 0; j < n; j++){ 5898 ctx.fillStyle = color(d.sim[i][j]) 5899 ctx.fillRect(j*cell, size - (i+1)*cell, cell+0.5, cell+0.5) 5900 } 5901 5902 c.x.domain([-0.5, n-0.5]); c.y.domain([-0.5, n-0.5]) 5903 c.xAxis.tickValues(util.layerTicks(n)).tickFormat(util.layerLabel).tickSizeOuter(0).tickSizeInner(3) 5904 c.yAxis.tickValues(util.layerTicks(n)).tickFormat(util.layerLabel).tickSizeOuter(0).tickSizeInner(3) 5905 c.drawAxis() 5906 util.addAxisLabel(c, 'Layer (reindexed) â', 'Layer (reindexed) â') 5907 5908 var title = `Centered kernel alignment of ${d.public_name || 'Sonnet 4.5'} J-lens` 5909 if (narrow){ 5910 svg.append('text.fig-title').translate([-margin.left + 4, -32]) 5911 .appendMany('tspan', wrap(title, 30)).text(s => s) 5912 .at({x: 0, dy: (s, j) => j ? '1.2em' : 0}) 5913 } else { 5914 svg.append('text.fig-title').text(title).translate([size/2, -12]).at({textAnchor: 'middle'}) 5915 } 5916 5917 var bx = size + 12, gap = 2 5918 d.phases.forEach(ph => { 5919 var by0 = c.y(ph.lo-0.5) - gap, by1 = c.y(ph.hi+0.5) + gap 5920 svg.append('path.bracket').at({d: `M${bx-5},${by0} H${bx} V${by1} H${bx-5}`, 5921 stroke: C.annot, strokeWidth: 0.9, fill: 'none'}) 5922 var words = ph.label.split(' ') 5923 var t = svg.append('text.phase').translate([bx+7, (by0+by1)/2]).at({textAnchor: 'start'}) 5924 words.forEach((w, k) => 5925 t.append('tspan').text(w).at({x: 0, dy: k ? '1.1em' : words.length>1 ? '-.15em' : '.35em'})) 5926 }) 5927 5928 var m = 60 5929 if (narrow){ 5930 var cb = svg.append('g.cbar').translate([0, size + 56]) 5931 var cw = size, ch = 10 5932 cb.appendMany('rect', d3.range(m)).at({x: i => i*cw/m, width: cw/m+0.5, height: ch, fill: i => color(i/(m-1))}) 5933 cb.appendMany('text', [0, 0.5, 1.0]).text(v => v.toFixed(1)) 5934 .at({x: v => cw*v, y: ch + 14, textAnchor: v => v ? v == 1 ? 'end' : 'middle' : 'start'}) 5935 cb.append('text.axis-label').text('CKA similarity') 5936 .at({x: cw/2, y: ch + 32, textAnchor: 'middle'}) 5937 } else { 5938 var cb = svg.append('g.cbar').translate([size + 106, size*0.125]) 5939 var ch = size*0.75, cw = 12 5940 cb.appendMany('rect', d3.range(m)).at({y: i => ch - (i+1)*ch/m, width: cw, height: ch/m+0.5, fill: i => color(i/(m-1))}) 5941 cb.appendMany('text', [0, 0.2, 0.4, 0.6, 0.8, 1.0]).text(v => v.toFixed(1)) 5942 .at({x: cw+5, y: v => ch*(1-v), dy: '.35em'}) 5943 cb.append('text.axis-label').text('CKA similarity') 5944 .at({transform: `translate(${cw+36},${ch/2}) rotate(90)`, textAnchor: 'middle', dy: '-.2em'}) 5945 } 5946 } 5947} 5948 5949window.init?.() 5950 5951// âââ public/line-panels/init-line-panels.js âââ 5952// Generic per-layer line-panel row. Reads {n_layers, band, band_label, 5953// panels:[{title, ylabel, legend_title, series:[{label,x,y}], annotations}]}. 5954// One svg cell per panel; viridis-ordinal series; grey workspace band with 5955// label; optional arrow annotations. 5956 5957window.initLinePanels = async function({datapath, spec = 'main', state}){ 5958 var sel = d3.select(`.line-panels[data-spec="${spec}"]`).html('') 5959 var d = await util.getFile(datapath + spec + '.json') 5960 var n = d.n_layers, [blo, bhi] = d.band 5961 var q = new URLSearchParams(location.search) 5962 var edit = state?.edit ?? (q.get('tune') === '1' || q.has('edit')) 5963 5964 var isBelow = d.legend_pos === 'below' 5965 // 2-col grid (main 2Ã2): wider per-panel, slightly shorter so total height 5966 // stays close to the old 1Ã3 row. 5967 var w = d.cols ? 320 : 300, h = d.cols ? 180 : 200 5968 // 'below' (appendix) puts series labels inside the plot area, so no right 5969 // margin is needed; non-below (main) needs room for end-labels at c.width+lx. 5970 var margin = {top: 22, right: isBelow ? 10 : 52, bottom: 32, left: 50} 5971 // Narrow screens: collapse the 2Ã2 grid to one full-width column so each 5972 // panel stays readable, instead of letting fitMount shrink the whole grid.
5973 var availW = sel.node().getBoundingClientRect().width 5974 var mobile = d.cols && availW > 0 && availW < 700 5975 if (mobile) w = Math.min(420, availW) - margin.left - margin.right 5976 var wsLabelPanel = 1 5977 var annItems = [] 5978 var row = sel.append('div.panel-row') 5979 if (d.cols) row.st({display: 'grid', gridTemplateColumns: `repeat(${mobile ? 1 : d.cols}, max-content)`}) 5980 5981 d.panels.forEach((p, pi) => { 5982 var cell = row.append('div.lp-cell') 5983 var c = d3.conventions({sel: cell, width: w, height: h, margin}) 5984 var ys = p.series.flatMap(s => s.y) 5985 var ylo = d3.min(ys), yhi = d3.max(ys) 5986 var isPct = p.pct ?? (yhi <= 1.01 && ylo >= 0 && /accuracy|fraction|rate/i.test(p.ylabel)) 5987 c.x.domain([0, n - 1]) 5988 // proportion panels can't run past [0,1] â clamp and skip nice() so the 5989 // pad doesn't push the axis to 110% 5990 if (isPct) c.y.domain([Math.max(0, ylo), Math.min(1, yhi + (yhi - ylo) * 0.10)]) 5991 else c.y.domain([ylo, yhi + (yhi - ylo) * 0.10]).nice() 5992 c.xAxis.tickValues(util.layerTicks(n)).tickFormat(util.layerLabel) 5993 c.yAxis.ticks(5) 5994 if (isPct) c.yAxis.tickFormat(d3.format('.0%')) 5995 c.drawAxis() 5996 util.ggPlot(c) 5997 util.addAxisLabel(c, 'Layer (reindexed) â', p.ylabel, p.title, 0, -8) 5998 5999 util.addWsBand(c, blo, bhi, {label: pi === wsLabelPanel && d.band_label}) 6000 6001 // series colors. Categorical = lens types â util.lensColors (sibling-figure 6002 // convention; orange is never a series color). Viridis ordinal for top-k / 6003 // percentile ramps. 6004 var nser = p.series.length 6005 var catPal = [util.tol.blue, util.tol.teal, util.tol.magenta, util.tol.cyan, util.gray[500]] 6006 var color = p.colormap === 'categorical' 6007 ? i => util.lensColors[p.series[i].label] || catPal[i % catPal.length] 6008 : i => d3.interpolateViridis(nser > 1 ? i / (nser - 1) : 0.5) 6009 var labelColor = p.colormap === 'categorical' ? color 6010 : i => i === nser - 1 ? d3.color(color(i)).darker(0.6) : color(i) 6011 var line = d3.line().x((v, i) => c.x(p.series[0].x[i])).y(v => c.y(v)) 6012 c.svg.appendMany('path.lp-line.line-series', p.series) 6013 .at({d: s => line(s.y), stroke: (s, i) => color(i), 'data-series': s => pi + '-' + s.label}) 6014 c.svg.appendMany('g.lp-dots', p.series) 6015 .at({fill: (s, i) => color(i)}) 6016 .each(function(s){ 6017 d3.select(this).appendMany('circle', s.x) 6018 .at({r: 1.8, cx: (x, i) => c.x(x), cy: (x, i) => c.y(s.y[i])}) 6019 }) 6020 6021 // direct labels for both legend modes; for 'below' (lens types), label only 6022 // one panel since the series are the same across panels â pick the panel 6023 // and per-series anchor where the lines are best separated. 6024 if (d.legend_pos === 'below') { 6025 var lensName = {jacobian: 'J-lens', logit: 'Logit lens', tuned: 'Tuned lens'} 6026 var lensLabel = { 6027 panel: 0, 6028 jacobian: {at: 4, dy: 2, anchor: 'start', dx: 178}, 6029 logit: {at: 14, dy: 18, anchor: 'end', dx: 41}, 6030 tuned: {at: 14, dy: 13, anchor: 'start', dx: -69}, 6031 } 6032 if (pi === lensLabel.panel) { 6033 p.series.forEach((s, i) => { 6034 var L = lensLabel[s.label]; if (!L) return 6035 var ai = s.x.indexOf(L.at) 6036 util.directLabel(c.svg, { 6037 x: c.x(s.x[ai]), y: c.y(s.y[ai]), dx: L.dx, dy: L.dy, 6038 anchor: L.anchor, text: lensName[s.label] || s.label, color: labelColor(i), key: s.label, 6039 }) 6040 }) 6041 } 6042 // legacy strip retained for now, hidden via CSS 6043 var lg = cell.append('div.lp-legend').st({display: 'none'}) 6044 lg.append('span.lp-legend-title').text(p.legend_title) 6045 lg.appendMany('span.lp-legend-item', p.series) 6046 .at({'data-series': s => pi + '-' + s.label}) 6047 .html((s, i) => 6048 `<span class='legend-swatch' style='background:${color(i)}'></span>${s.label}`) 6049 } else { 6050 var nx = p.series[0].x.length, iMid = Math.floor(nx * 0.6) 6051 // Top-anchored stack: title flush with top of plot area, labels packed 6052 // immediately below in their iMid y-order (so visual order matches lines). 6053 var words = p.legend_title.split(' '), lh = 10 6054 var titleH = words.length * lh 6055 var ttx = c.width + 34 + (p.legend_title_dx || 0) 6056 var tt = c.svg.append('text') 6057 .at({x: ttx, y: lh - 2, textAnchor: 'end'}) 6058 .st({fill: 'var(--text-light)', fontSize: 'var(--fs-label)'}) 6059 if (words.length > 1) words.forEach((wd, i) => 6060 tt.append('tspan').text(wd).at({x: ttx, dy: i ? lh : 0})) 6061 else tt.text(p.legend_title) 6062 var lx = c.width + 34 6063 var specs = p.series.map((s, i) => ({ 6064 text: s.label, color: labelColor(i), halo: '#fff', key: pi + '-' + s.label, 6065 x: lx, y: c.y(s.y[iMid]), dy: 4, anchor: 'end', 6066 })) 6067 specs.sort((a, b) => a.y - b.y) 6068 .forEach((sp, i) => sp.y = titleH + 10 + i * 14) 6069 specs.forEach(sp => util.directLabel(c.svg, sp)) 6070 } 6071 6072 // arrowhead marker (per-svg so url() resolves) 6073 c.svg.append('defs').append('marker') 6074 .at({id: `lp-arrow-${spec}`, viewBox: '0 0 6 6', refX: 5, refY: 3, 6075 markerWidth: 5, markerHeight: 5, orient: 'auto'}) 6076 .append('path').at({d: 'M0,0 L6,3 L0,6', fill: util.gray[500]}) 6077 6078 // annotations: tip at (x,y), text at (tx,ty); both in (layer, y-frac). 6079 // If tx/ty absent, derive from `dir` once. 6080 var lh = 9, L = 22 6081 var toPx = a => ({ 6082 ax: c.x(a.x), ay: (1 - a.y) * c.height, 6083 tx: c.x(a.tx), ty: (1 - a.ty) * c.height, 6084 }) 6085 var fromPx = (a, k, px, py) => { 6086 a[k === 'tip' ? 'x' : 'tx'] = +c.x.invert(px).toFixed(2) 6087 a[k === 'tip' ? 'y' : 'ty'] = +(1 - py / c.height).toFixed(3) 6088 } 6089 var dump = () => console.log(`[line-panels:${spec}] ${p.title} annotations:`, 6090 JSON.stringify(p.annotations, null, 2)) 6091 var dumpD = util.debounce(dump, 300) 6092 6093 ;
6093(p.annotations || []).forEach((a, ai) => { 6094 Object.assign(a, state?.anno?.[`${pi}.${ai}`]) 6095 a.y ??= 0.7 6096 if (a.tx == null) { 6097 var dir = a.dir || (a.x > n / 2 ? 'right' : 'left') 6098 if (dir === 'right') { a.tx = a.x - L / w * (n - 1); a.ty = a.y } 6099 else if (dir === 'left') { a.tx = a.x + L / w * (n - 1); a.ty = a.y } 6100 else if (dir === 'down') { a.tx = a.x + 0.5; a.ty = a.y } 6101 else { a.tx = a.x; a.ty = a.y - 0.08 } 6102 } 6103 var g = c.svg.append('g.lp-annotation') 6104 var arrow = g.append('path.lp-ann-arrow') 6105 .at({'marker-end': `url(#lp-arrow-${spec})`}) 6106 var txt = g.append('text.lp-ann') 6107 a.lines.forEach((ln, i) => 6108 txt.append('tspan').at({dy: i ? lh : 0}).text(ln)) 6109 var tip = g.append('circle.lp-ann-handle') 6110 var tail = g.append('circle.lp-ann-handle') 6111 6112 function render() { 6113 var {ax, ay, tx, ty} = toPx(a) 6114 var anchor = a.anchor 6115 ?? (Math.abs(tx - ax) < 8 ? 'middle' : (tx > ax ? 'start' : 'end')) 6116 var nlines = a.lines.length 6117 var ly = ty < ay 6118 ? ty - (nlines - 1) * lh / 2 6119 : ty + lh - (anchor === 'middle' ? 0 : (nlines - 1) * lh / 2) 6120 txt.at({'text-anchor': anchor, y: ly}) 6121 txt.selectAll('tspan').at({x: tx + (anchor === 'start' ? 3 : anchor === 'end' ? -3 : 0)}) 6122 // Arrow tail leaves the text-block edge nearest the tip, not (tx,ty) 6123 // itself â so it reads as coming from under/over the text rather than 6124 // through its middle. 6125 var bb = txt.node().getBBox() 6126 var sy = ay > ty ? bb.y + bb.height + 1 : bb.y - 1 6127 var sx = anchor === 'middle' 6128 ? Math.max(bb.x, Math.min(ax, bb.x + bb.width)) 6129 : tx 6130 var gap = 3 6131 var dxn = ax - sx, dyn = ay - sy, dn = Math.hypot(dxn, dyn) || 1 6132 arrow.at({d: `M${sx},${sy} L${ax - dxn / dn * gap},${ay - dyn / dn * gap}`}) 6133 tip.at({cx: ax, cy: ay, r: 4}) 6134 tail.at({cx: tx, cy: ty, r: 4}) 6135 } 6136 render() 6137 g.datum({spec, pi, ai, a, render}) 6138 6139 ;[['tip', tip], ['tail', tail]].forEach(([k, hnd]) => { 6140 hnd.classed('editable', edit) 6141 annItems.push({sel: hnd, key: `ann_${pi}_${ai}_${k}`, 6142 get: () => { var px = toPx(a) 6143 return k == 'tip' ? {x: px.ax, y: px.ay} : {x: px.tx, y: px.ty} }, 6144 set: pp => { fromPx(a, k, pp.x, pp.y); render(); dumpD() 6145 state?.onAnnoChange?.(spec, pi, ai, a) }}) 6146 }) 6147 }) 6148 }) 6149 util.dragTune(annItems, 'line-panels-' + spec) 6150 util.bindSeriesHover(sel) 6151 util.dragTuneLabels(sel, 'line-panels-' + spec) 6152} 6153 6154window.init?.() 6155 6156// âââ public/capacity-fve-occupancy/init-capacity-fve-occupancy.js âââ 6157// [[FIGURE:capacity-fve-occupancy]] â 1Ã2: J-lens FVE vs k (log-log); 6158// occupancy vs layer (percentile). 6159 6160window.initCapacityFveOccupancy = async function({datapath}){ 6161 var sel = d3.select('.capacity-fve-occupancy').html('') 6162 var d = await util.getFile(datapath + 'data.json') 6163 var n = d.n_layers, [blo, bhi] = d.band 6164 6165 var w = 360, h = 240 6166 var row = sel.append('div.panel-row') 6167 6168 // ---- (a) occupancy vs layer ---- 6169 ;(function(){ 6170 var xs = d.occ.x, ss = d.occ.series 6171 var cell = row.append('div.cf-cell') 6172 var c = d3.conventions({sel: cell, width: w, height: h, 6173 margin: {top: 26, right: 36, bottom: 36, left: 50}}) 6174 c.x.domain([0, n - 1]) 6175 var ys = ss.flatMap(s => s.y) 6176 c.y.domain([0, d3.max(ys) * 1.10]).nice() 6177 c.xAxis.tickValues(util.layerTicks(n)).tickFormat(util.layerLabel) 6178 c.yAxis.ticks(5) 6179 c.drawAxis() 6180 util.ggPlot(c) 6181 util.addAxisLabel(c, 'Layer (reindexed) â', 'Occupancy â', '(a) J-space occupancy') 6182 6183 util.addWsBand(c, blo, bhi) 6184 6185 var nser = ss.length 6186 var color = i => d3.interpolateViridis(nser > 1 ? i / (nser - 1) : 0.5) 6187 var line = d3.line().x((v, i) => c.x(xs[i])).y(v => c.y(v)) 6188 c.svg.appendMany('path.cf-line.line-series', ss) 6189 .at({d: s => line(s.y), stroke: (s, i) => color(i), 'data-series': s => s.label}) 6190 6191 // direct labels just inside the right edge, spread so adjacent percentiles don't overlap 6192 var iEnd = xs.length - 1 6193 var specs = ss.map((s, i) => ({
6194 text: 'p' + s.label, color: color(i), key: s.label, 6195 x: c.width - 4, y: c.y(s.y[iEnd]), anchor: 'end', dy: -4, 6196 })) 6197 util.spreadLabels(specs, 12).forEach(sp => util.directLabel(c.svg, sp)) 6198 util.bindSeriesHover(cell) 6199 })() 6200 6201 // ---- (b) FVE excess over random at crossover, per layer ---- 6202 ;(function(){ 6203 var ss = d.fve.series 6204 var cell = row.append('div.cf-cell') 6205 var c = d3.conventions({sel: cell, width: w, height: h, 6206 margin: {top: 26, right: 12, bottom: 36, left: 50}}) 6207 var x = d3.scaleBand().domain(ss.map(s => s.label)).range([0, c.width]) 6208 .paddingInner(0.35).paddingOuter(0.2) 6209 c.y.domain([0, d3.max(ss, s => s.y) * 1.15]).nice() 6210 c.yAxis.scale(c.y).ticks(5, '~g') 6211 c.drawAxis() 6212 c.svg.select('.x').remove() 6213 util.ggPlot(c) 6214 util.addAxisLabel(c, 'Layer (reindexed) â', 'FVE â FVE[random] â', 6215 '(b) J-lens FVE excess over random', 0, -8) 6216 6217 c.svg.appendMany('rect', ss) 6218 .at({x: s => x(s.label), width: x.bandwidth(), 6219 y: s => c.y(s.y), height: s => c.height - c.y(s.y), 6220 fill: util.gray[400]}) 6221 c.svg.appendMany('text.cf-barlab', ss) 6222 .text(s => s.label) 6223 .at({x: s => x(s.label) + x.bandwidth()/2, y: c.height + 14, 6224 textAnchor: 'middle'}) 6225 })() 6226} 6227 6228window.init?.() 6229 6230// âââ public/broadcast-ablation/init-broadcast-ablation.js âââ 6231// [[FIGURE:broadcast-ablation]] â 1Ã3: ablating top-1% broadcast heads vs 6232// matched random across (a) recall@25 (b) introspection P@1 (c) felt_exp. 6233 6234window.initBroadcastAblation = async function({datapath}){ 6235 var sel = d3.select('.broadcast-ablation').html('') 6236 var d = await util.getFile(datapath + 'data.json') 6237 var [blo, bhi] = d.band 6238 6239 var C_TOP = util.tol.orange 6240 var C_RAND = util.gray[500], C_CLEAN = util.gray[700] 6241 var w = 300, h = 230 6242 sel.append('div.fig-title').text('Ablating the J-lens broadcast heads') 6243 var row = sel.append('div.panel-row') 6244 // halo over the workspace band = manilla at .45 over the ggPlot bg 6245 var bandHalo = d3.interpolateRgb('#EAECED', util.brand.manilla)(0.45) 6246 6247 function dlabel(c, text, col, x, y, anchor, halo){ 6248 c.svg.append('text.ba-dlabel').text(text) 6249 .at({x: x, y: y, dy: '.32em', textAnchor: anchor || 'start'}) 6250 .st({fill: col, paintOrder: 'stroke', stroke: halo || '#EAECED', 6251 strokeWidth: 2.5, strokeLinejoin: 'round'}) 6252 } 6253 6254 // ---- (a) recall@25 vs layer ---- 6255 ;(function(){ 6256 var cell = row.append('div.ba-cell') 6257 var c = d3.conventions({sel: cell, width: w, height: h, 6258 margin: {top: 26, right: 10, bottom: 36, left: 50}}) 6259 var xs = d.recall.x 6260 c.x.domain(d3.extent(xs)) 6261 c.y.domain([0.62, 1.0]) 6262 c.xAxis.tickValues(util.layerTicks(d.n_layers).filter(t => t >= xs[0])).tickFormat(util.layerLabel) 6263 c.yAxis.ticks(4) 6264 c.drawAxis() 6265 util.ggPlot(c) 6266 util.addAxisLabel(c, 'Layer (reindexed) â', 'recall@25 â', '(a) J-lens overlap') 6267 util.addWsBand(c, blo, bhi) 6268 6269 var line = d3.line().x((v,i) => c.x(xs[i])).y(v => c.y(v)) 6270 d.recall.rand_seeds.forEach(s => 6271 c.svg.append('path.ba-seed').at({d: line(s), stroke: C_RAND})) 6272 c.svg.append('path.ba-line').at({d: line(d.recall.rand_mean), stroke: C_RAND}) 6273 c.svg.append('path.ba-line').at({d: line(d.recall.top), stroke: C_TOP}) 6274 6275 // labels centered over the chart (~layer 18), haloed to blend with the band 6276 var i18 = xs.reduce((best, v, i) => Math.abs(v - 18) < Math.abs(xs[best] - 18) ? i : best, 0) 6277 dlabel(c, 'Random', C_RAND, c.x(18), c.y(d.recall.rand_mean[i18]) - 14, 'middle', bandHalo) 6278 dlabel(c, 'Broadcast', C_TOP, c.x(18), c.y(d.recall.top[i18]) + 18, 'middle', bandHalo) 6279 })() 6280 6281 // ---- (b) introspection P@1 vs strength ---- 6282 ;(function(){ 6283 var cell = row.append('div.ba-cell') 6284 var c = d3.conventions({sel: cell, width: w, height: h, 6285 margin: {top: 26, right: 10, bottom: 36, left: 50}}) 6286 var xs = d.vi.x.filter(s => s <= 0.02) 6287 var n = xs.length 6288 c.x.domain([0, 0.02]) 6289 c.y.domain([0, 0.88]) 6290 c.xAxis.tickValues([0, 0.01, 0.02]).tickFormat(v => v.toFixed(2)) 6291 c.yAxis.ticks(4) 6292 c.drawAxis() 6293 util.ggPlot(c) 6294 util.addAxisLabel(c, 'Steering strength â', 'P(reported) â', 6295 '(b) Introspective sensitivity') 6296 6297 var line = d3.line().x((v,i) => c.x(xs[i])).y(v => c.y(v)) 6298 var trim = a => a.slice(0, n) 6299 d.vi.rand_seeds.forEach(s => 6300 c.svg.append('path.ba-seed').at({d: line(trim(s)), stroke: C_RAND})) 6301 c.svg.append('path.ba-line').at({d: line(trim(d.vi.rand_mean)), stroke: C_RAND}) 6302 c.svg.append('path.ba-line').at({d: line(trim(d.vi.clean)), stroke: C_CLEAN}) 6303 c.svg.append('path.ba-line').at({d: line(trim(d.vi.top)), stroke: C_TOP}) 6304 6305 var i = n - 1 6306 dlabel(c, 'Random', C_RAND, 6307 c.x(xs[i]) - 4, c.y(d.vi.rand_mean[i]) - 8, 'end') 6308 dlabel(c, 'Clean', C_CLEAN, 6309 c.x(xs[i]) - 4, c.y(d.vi.clean[i]) + 32, 'end') 6310 dlabel(c, 'Broadcast', C_TOP, 6311 c.x(xs[i]) - 4, c.y(d.vi.top[i]) - 8, 'end') 6312 })() 6313 6314 // ---- (c) felt_exp Î ---- 6315 ;(function(){ 6316 var cell = row.append('div.ba-cell') 6317 var bars = [['Broadcast', d.fe.top, d.fe.top_se, C_TOP], 6318 ['Random', d.fe.rand, d.fe.rand_se, C_RAND]] 6319 var c = d3.conventions({sel: cell, width: w * 0.6, height: h, 6320 margin: {top: 26, right: 10, bottom: 36, left: 50}}) 6321 var xb = d3.scaleBand().domain(bars.map(b => b[0])).range([0, c.width]) 6322 .paddingInner(0.35).paddingOuter(0.2) 6323 c.y.domain([-18, 4]) 6324 c.xAxis.tickValues([]) 6325 c.yAxis.ticks(5) 6326 c.drawAxis() 6327 util.ggPlot(c) 6328 util.addAxisLabel(c, '', 'Î experiential-language score (pp) â', 6329 '(c) Experiential self-report') 6330 c.svg.append('line').at({x1: 0, x2: c.width, y1: c.y(0), y2: c.y(0), 6331 stroke: util.gray[600], 'stroke-width': 1}) 6332 6333 c.svg.appendMany('rect.ba-bar', bars) 6334 .at({x: b => xb(b[0]), width: xb.bandwidth(), 6335 y: b => c.y(Math.max(0, b[1])), 6336 height: b => Math.abs(c.y(b[1]) - c.y(0)), 6337 fill: b => b[3]}) 6338 var cx = b => xb(b[0]) + xb.bandwidth()/2 6339 c.svg.appendMany('line.ba-err', bars) 6340 .at({x1: cx, x2: cx, y1: b => c.y(b[1]-b[2]), y2: b => c.y(b[1]+b[2]), 6341 stroke: util.gray[700], 'stroke-width': 1.2}) 6342 c.svg.appendMany('line.ba-err', bars) 6343 .at({x1: b => cx(b)-4, x2: b => cx(b)+4, 6344 y1: b => c.y(b[1]-b[2]), y2: b => c.y(b[1]-b[2]), 6345 stroke: util.gray[700], 'stroke-width': 1.2}) 6346 c.svg.appendMany('line.ba-err', bars) 6347 .at({x1: b => cx(b)-4, x2: b => cx(b)+4, 6348 y1: b => c.y(b[1]+b[2]), y2: b => c.y(b[1]+b[2]), 6349 stroke: util.gray[700], 'stroke-width': 1.2}) 6350 c.svg.appendMany('text.ba-bar-label', bars) 6351 .at({x: cx, y: b => c.y(b[1] + (b[1] > 0 ? b[2] : -b[2])) + (b[1] > 0 ? -6 : 14)}) 6352 .text(b => d3.format('+.1f')(b[1])) 6353 c.svg.appendMany('text.ba-tick-label', bars) 6354 .at({x: cx, y: c.height + 16}) 6355 .text(b => b[0]) 6356 })() 6357} 6358 6359window.init?.() 6360 6361// âââ public/lens-callout/init-lens-callout-main.js âââ 6362// Main-text lens-callout figures (blackmail, evil_anthropic). 6363// Transcript is a baked HTML string; only the callout chip rows come from 6364// data, so layer labels track util.layerLabel and chip text is the real 6365// lens output. No auto-layout â every box sits at (x, anchorTop + dy). 6366// 6367// fig_{id}.json: 6368// { slug, txtW, txt, // txt: HTML string; <b data-co=i> marks anchors
6369// callouts: [{ctx, lo, hi, k, x, dy}], 6370// pinned: [str, ...] } 6371// {slug}.parquet: long {layer, ctx, rank, str, prob} for callout cells only. 6372 6373window.initLensCalloutMain = async function(opts){ 6374 var sel = d3.select(opts.sel).html('').classed('lens-callout lens-callout-main', true) 6375 var base = opts.datapath.replace(/\/?$/, '/') 6376 var fc = await util.getFile(base + 'fig_' + opts.fig + '.json') 6377 var rows = await util.getFile(base + fc.slug + '.parquet') 6378 var by = d3.group(rows, r => r.ctx, r => r.layer) 6379 var cell = (ctx, L) => by.get(ctx)?.get(L) || [] 6380 6381 // util.series order is blue/green/brown/purple/orange/red â swap red into 6382 // slot 2 so the third group (evidence; transparency) reads red, not brown. 6383 var PALETTE = ['#1f77b4', '#2ca02c', '#d62728', '#9467bd', '#ee7733', '#8c564b'] 6384 var pinned = new Map(fc.pinned.flatMap((grp, i) => 6385 grp.map(s => [s, PALETTE[i % PALETTE.length]]))) 6386 6387 var tune = util.params.get('tune') === '1' 6388 var stage = sel.append('div.stage').st({width: fc.figW}) 6389 var svg = stage.append('svg.lines') 6390 stage.append('div.txt').st({width: fc.txtW}).html(fc.txt) 6391 6392 var anchor = i => stage.select(`b[data-co='${i}']`).node() 6393 6394 function draw(){ 6395 var sb = stage.node().getBoundingClientRect() 6396 stage.selectAll('.callout').remove() 6397 svg.html('') 6398 fc.callouts.forEach((co, i) => { 6399 var a = anchor(i) 6400 var top = (a ? a.getBoundingClientRect().top - sb.top : 0) + co.dy 6401 var box = stage.append('div.callout').st({left: co.x, top, maxWidth: fc.figW - co.x}) 6402 box.appendMany('div.co-row', d3.range(co.hi, co.lo - 1, -1)).each(function(L){ 6403 var row = d3.select(this) 6404 row.append('span.row-label').text(util.layerLabel(L)) 6405 util.appendLensTopk(row.append('span.lens-topk'), cell(co.ctx, L), co.k) 6406 }) 6407 if (tune) box.call(d3.drag() 6408 .on('drag', ev => { co.x += ev.dx; co.dy += ev.dy; draw() }) 6409 .on('end', () => util.params.set('co', 6410 JSON.stringify(fc.callouts.map(c => [c.x|0, c.dy|0]))))) 6411 if (a){ 6412 var ab = a.getBoundingClientRect(), bb = box.node().getBoundingClientRect() 6413 var toRight = bb.left >= ab.right 6414 svg.append('line').at({ 6415 x1: (toRight ? ab.right : ab.left) - sb.left, y1: ab.top + ab.height/2 - sb.top, 6416 x2: (toRight ? bb.left : bb.right) - sb.left, y2: bb.top + bb.height/2 - sb.top, 6417 }) 6418 } 6419 }) 6420 sel.selectAll('.lenstk').each(function(){ 6421 var it = d3.select(this).datum() 6422 d3.select(this).at({style: util.pinnedStyle(it && pinned.get(it.str))}) 6423 }) 6424 var h = stage.select('.txt').node().offsetHeight 6425 stage.selectAll('.callout').each(function(){ h = Math.max(h, this.offsetTop + this.offsetHeight + 10) }) 6426 stage.st({minHeight: h}); svg.at({width: stage.node().offsetWidth, height: h}) 6427 } 6428 6429 draw() 6430 if (tune) sel.classed('editing', true) 6431} 6432 6433window.init?.() 6434 6435// âââ public/misalign-lens/init-misalign-lens.js âââ 6436!function(){ 6437 6438async function loadData(opts){ 6439 var base = (opts?.datapath || './').replace(/\/?$/, '/') 6440 return await util.getFile(base + 'data.json') 6441} 6442 6443function drawBars(sel, d){ 6444 var color = Object.fromEntries(d.models.map((m,i) => [m.key, util.series[i % util.series.length]])) 6445 var modelKeys = d.models.map(m => m.key) 6446 var panelRow = sel.append('div.panel-row') 6447 var ttSel = d3.select('.tooltip') 6448 var trunc = s => s && s.length > 200 ? s.slice(0, 200) + 'â¦' : (s || '') 6449 6450 var allVals = d.panels.flatMap(p => 6451 d.categories.flatMap(cat => modelKeys.flatMap(mk => p.cats[cat][mk].points))) 6452 var ymax = d3.max(allVals) || 1 6453 6454 d.panels.forEach((panel, pi) => { 6455 var c = d3.conventions({ 6456 sel: panelRow.append('div.panel'), 6457 width: 250, height: 260, 6458 margin: {left: pi ? 14 : 66, top: 30, right: 8, bottom: 44}, 6459 }) 6460 var x0 = d3.scaleBand().domain(d.categories).range([0, c.width]).padding(0.16) 6461 var x1 = d3.scaleBand().domain(modelKeys).range([0, x0.bandwidth()]).padding(0.12) 6462 c.y.domain([0, ymax]).nice() 6463 c.yAxis.ticks(5) 6464 c.drawAxis() 6465 c.svg.select('.x').remove() 6466 if (pi) c.svg.select('.y').selectAll('text').remove() 6467 util.ggPlot?.(c) 6468 6469 c.svg.append('text.fig-title').text(panel.title.replace(/^[a-z]/, c => c.toUpperCase())).at({x: c.width/2, y: -14, textAnchor: 'middle'}) 6470 if (!pi) util.addAxisLabel?.(c, '', d.ylabel) 6471
6472 d.categories.forEach(cat => { 6473 var gx = x0(cat) 6474 modelKeys.forEach(mk => { 6475 var cell = panel.cats[cat][mk] 6476 var bx = gx + x1(mk), bw = x1.bandwidth() 6477 c.svg.append('rect').at({ 6478 x: bx, y: c.y(cell.mean), width: bw, height: c.height - c.y(cell.mean), 6479 fill: color[mk], 6480 }) 6481 var jw = bw * 0.5 6482 var prompts = cell.prompts || [] 6483 var lensTopk = cell.lens_topk || [] 6484 cell.points.forEach((v, i) => { 6485 var jx = bx + bw/2 + ((i / Math.max(cell.points.length-1, 1)) - 0.5) * jw 6486 c.svg.append('circle.jpt').datum({v, mk, cat, pr: prompts[i], lt: lensTopk[i]}) 6487 .at({cx: jx, cy: c.y(v), r: 1.8, fillOpacity: 0, stroke: util.gray[700], strokeWidth: 1}) 6488 }) 6489 if (cell.sem > 0) { 6490 util.barWhisker(c.svg, {x: bx + bw/2, 6491 lo: c.y(cell.mean - cell.sem), hi: c.y(cell.mean + cell.sem)}) 6492 } 6493 }) 6494 c.svg.append('text').text(cat) 6495 .at({x: gx + x0.bandwidth()/2, y: c.height + 16, textAnchor: 'middle'}) 6496 .st({fontSize: 'var(--fs-small)'}) 6497 }) 6498 6499 var last 6500 util.nearestHover(c.svg, c.svg.selectAll('circle.jpt'), { 6501 radius: 40, 6502 onHover: (o, el, e) => { 6503 if (el !== last){ 6504 last = el 6505 ttSel.classed('tooltip-hidden', false).html('') 6506 ttSel.append('div').html(`<b style="color:${color[o.mk]}">${o.mk}</b> · ${o.cat} · ${o.v}`) 6507 if (o.pr) ttSel.append('div.prompt-block').text(trunc(o.pr)) 6508 if (o.lt?.tokens?.length){ 6509 var toks = ttSel.append('div.toks') 6510 o.lt.tokens.forEach((t, ti) => 6511 toks.append('span.tok').text(util.ppToken?.(t) ?? t).classed('hl', o.lt.hit_idx?.includes(ti))) 6512 } 6513 } 6514 var bb = ttSel.node().getBoundingClientRect() 6515 ttSel.st({left: d3.clamp(20, e.clientX - bb.width/2, innerWidth - bb.width - 20) + 'px', 6516 top: (innerHeight > e.clientY + 20 + bb.height ? e.clientY + 20 : e.clientY - bb.height - 20) + 'px'}) 6517 }, 6518 onLeave: () => { last = null; ttSel.classed('tooltip-hidden', true) }, 6519 }) 6520 }) 6521 6522 var leg = sel.append('div.legend') 6523 d.models.forEach(m => { 6524 var e = leg.append('span.legend-item') 6525 e.append('span.swatch').st({background: color[m.key]}) 6526 e.append('span').text(m.label) 6527 }) 6528} 6529 6530function drawExample(sel, d){ 6531 var color = Object.fromEntries(d.models.map((m,i) => [m.key, util.series[i % util.series.length]])) 6532 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 6533 var ex = sel.append('div.card.ex-card') 6534 // mark the lens-read position (the trailing prefill "I") with the shared .tok-hit outline 6535 ex.append('div.prompt.prompt-block').html(esc(d.example.prompt) 6536 .replace(/I$/, '<span class="tok-hit">I</span>')) 6537 ex.append('div.mlabel').text(`Top-10 lens tokens at "I", ${d.example.layer_label}`) 6538 var tbl = ex.append('div.ex-table') 6539 d.example.rows.forEach(r => { 6540 var row = tbl.append('div.ex-row') 6541 row.append('div.model').text(r.model).st({color: color[r.model]}) 6542 var toks = row.append('div.toks') 6543 r.tokens.forEach((t, i) => { 6544 toks.append('span.tok').text(t).classed('hl', r.hit_idx.includes(i)) 6545 }) 6546 }) 6547} 6548 6549window.initMisalignLens = async function(opts){ 6550 var sel = d3.select('.misalign-lens').html('') 6551 var d = await loadData(opts) 6552 drawBars(sel, d) 6553} 6554 6555window.initMisalignLensExample = async function(opts){ 6556 var sel = d3.select('.misalign-lens-example').html('') 6557 var d = await loadData(opts) 6558 drawExample(sel, d) 6559} 6560 6561}() 6562window.init?.() 6563 6564// âââ public/roleplay-lens/init-roleplay-lens.js âââ 6565!function(){ 6566 6567// base â dark grey, post-trained â blue (matches post-training figures). 6568var PALETTE = [util.gray[600], util.tol.blue] 6569 6570async function loadData(opts){ 6571 var base = (opts?.datapath || './').replace(/\/?$/, '/') 6572 return await util.getFile(base + 'data.json') 6573} 6574 6575var BAR_MARGIN = {left: 48, top: 8, right: 6, bottom: 56} 6576 6577// One narrow hit-rate panel: 3 groups à {base, post-trained}, per-unit dots, 6578// thin per-unit baseâpost lines, bold mean line. Mirrors figures_polished._panel. 6579function drawPanel(sel, d, word, color, modelKeys, {width, height, ylabel}){ 6580 var groupKeys = d.groups.map(g => g.key) 6581 var groupLabel = Object.fromEntries(d.groups.map(g => [g.key, g.label])) 6582 var c = d3.conventions({sel, width, height, margin: BAR_MARGIN}) 6583 var x0 = d3.scaleBand().domain(groupKeys).range([0, c.width]).padding(0.16) 6584 var x1 = d3.scaleBand().domain(modelKeys).range([0, x0.bandwidth()]).padding(0.12) 6585 c.y.domain([0, 1]) 6586 c.yAxis.ticks(5).tickFormat(d3.format('.1f')) 6587 c.drawAxis() 6588 c.svg.select('.x').remove() 6589 util.ggPlot?.(c) 6590 // y-label: only the word is bold, " hit rate" regular â matches the 6591 // original $\mathbf{word}$ hit rate 6592 var yl = c.svg.append('text.axis-label') 6593 .at({transform: `translate(-32,${c.height/2}) rotate(-90)`, textAnchor: 'middle'}) 6594 yl.append('tspan.bold').text(`"${ylabel}"`) 6595 yl.append('tspan').text(' hit rate â') 6596 6597 groupKeys.forEach(g => { 6598 var gp = d.panels[word][g] 6599 var gx = x0(g), bw = x1.bandwidth(), jw = bw * 0.55 6600 var cx = {}, jpos = {} 6601 modelKeys.forEach(mk => { 6602 var cell = gp[mk] 6603 var bx = gx + x1(mk) 6604 cx[mk] = bx + bw/2 6605 c.svg.append('rect').at({ 6606 x: bx, y: c.y(cell.mean), width: bw, height: c.height - c.y(cell.mean), 6607 fill: color[mk], 6608 }) 6609 jpos[mk] = cell.points.map((v, i) => {
6610 var jx = cx[mk] + ((i / Math.max(cell.points.length - 1, 1)) - 0.5) * jw 6611 c.svg.append('circle').at({cx: jx, cy: c.y(v), r: 1.7, fill: util.gray[700], fillOpacity: 0.32}) 6612 return [jx, c.y(v)] 6613 }) 6614 var tick = c.svg.append('text.mtick').at({x: cx[mk], y: c.height + 10, textAnchor: 'middle'}) 6615 mk.split('-').forEach((w, li) => { 6616 tick.append('tspan').text(w).at({x: cx[mk], dy: li ? '1.05em' : 0}) 6617 }) 6618 }) 6619 if (modelKeys.length === 2){ 6620 var [a, b] = [jpos[modelKeys[0]], jpos[modelKeys[1]]] 6621 d3.range(Math.min(a.length, b.length)).forEach(i => { 6622 c.svg.append('line').at({ 6623 x1: a[i][0], y1: a[i][1], x2: b[i][0], y2: b[i][1], 6624 stroke: util.gray[500], strokeWidth: 0.5, strokeOpacity: 0.25, 6625 }) 6626 }) 6627 } 6628 var gl = c.svg.append('text.glabel').at({x: gx + x0.bandwidth()/2, y: c.height + 36, textAnchor: 'middle'}) 6629 groupLabel[g].split(' ').forEach((w, li) => { 6630 gl.append('tspan').text(w).at({x: gx + x0.bandwidth()/2, dy: li ? '1.1em' : 0}) 6631 }) 6632 }) 6633} 6634 6635function drawTable(sel, label, color, rows){ 6636 var maxLp = d3.max(rows, r => r.logprob) 6637 var wrap = sel.append('div.rtable') 6638 var head = wrap.append('div.thd') 6639 head.append('span.swatch').st({background: color}) 6640 head.append('span').text(label) 6641 var tbl = wrap.append('div.bar-table') 6642 rows.forEach(r => { 6643 var row = tbl.append('div.bar-row') 6644 row.append('div.bar').st({width: (Math.exp(r.logprob - maxLp) * 100) + '%', background: util.tint(color, 0.75)}) 6645 row.append('span.t').text(util.ppToken(r.token)).classed('hl', r.hilite) 6646 row.append('span.val').text(r.logprob.toFixed(2)) 6647 }) 6648} 6649 6650function drawPromptBox(sel, block){ 6651 var box = sel.append('div.prompt.prompt-block') 6652 if (block.system_prompt) box.append('div.turn.sys').text(block.system_prompt) 6653 block.turns.forEach(t => { 6654 var h = box.append('div.turn') 6655 h.append('span.prompt-role').text('Human:') 6656 h.append('span').text(' ' + t.human) 6657 var a = box.append('div.turn') 6658 if (t.is_marker) a.append('span.tok-hit').text('Assistant') 6659 else a.append('span.prompt-role').text('Assistant:') 6660 a.append('span.reply').text((t.is_marker ? ': ' : ' ') + t.assistant + (t.truncated ? 'â¦' : '')) 6661 }) 6662} 6663 6664function drawCard(sel, d, which, color){ 6665 var ex = d.example 6666 var card = sel.append('div.rp-col') 6667 drawPromptBox(card, ex[which]) 6668 var tables = card.append('div.rtables') 6669 d.models.forEach(m => drawTable(tables, m.label.replace(/^./, c => c.toUpperCase()), 6670 color[m.key], ex.readouts[which][m.key])) 6671} 6672 6673// [[FIGURE:roleplay-lens]] â 2Ã2: top row = control / persona prompt-box + 6674// tables; bottom row = 2 bar panels (disclaimer | fictional). Bars get a full 6675// card-width each so x-tick labels fit at the normal --fs-label size. 6676window.initRoleplayLens = async function(opts){ 6677 var sel = d3.select('.roleplay-lens').html('') 6678 var d = await loadData(opts) 6679 var color = Object.fromEntries(d.models.map((m, i) => [m.key, PALETTE[i % PALETTE.length]])) 6680 var modelKeys = d.models.map(m => m.key) 6681 var cards = sel.append('div.panel-row') 6682 drawCard(cards, d, 'control', color) 6683 drawCard(cards, d, 'persona', color) 6684 var bars = sel.append('div.rp-bars') 6685 ;
6685(d.example_words || d.words.slice(0, 2)).forEach(word => { 6686 drawPanel(bars.append('div.panel'), d, word, color, modelKeys, 6687 {width: 290, height: 170, ylabel: word}) 6688 }) 6689} 6690 6691// [[FIGURE:roleplay-lens-example]] â kept for back-compat: cards-only view. 6692window.initRoleplayLensExample = async function(opts){ 6693 var sel = d3.select('.roleplay-lens-example').html('') 6694 var d = await loadData(opts) 6695 var color = Object.fromEntries(d.models.map((m, i) => [m.key, PALETTE[i % PALETTE.length]])) 6696 var row = sel.append('div.panel-row') 6697 drawCard(row, d, 'control', color) 6698 drawCard(row, d, 'persona', color) 6699} 6700 6701}() 6702window.init?.() 6703 6704// âââ public/reward-hack-readout/init-reward-hack-readout.js âââ 6705window.initRewardHackReadout = async function(opts){ 6706 var sel = d3.select('.reward-hack-readout').html('') 6707 var base = (opts?.datapath || './').replace(/\/?$/, '/') 6708 var d = await util.getFile(base + 'data.json') 6709 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 6710 6711 // Ordinal: baseline grey, then three darkening blues. Shared with reward-hack-quant. 6712 var blue = util.tol.blue 6713 var CHK = {'Baseline': util.gray[300], 'SDF': util.tint(blue, .55), 6714 'Phase 1': util.tint(blue, .25), 'Phase 2': blue} 6715 var color = Object.fromEntries(d.models.map((m,i) => 6716 [m.key, CHK[m.key] ?? util.tint(blue, 1 - i/Math.max(d.models.length-1,1))])) 6717 var labelFix = {'Phase 1': 'Phase 1: Code RL', 'Phase 2': 'Phase 2: HHH RL'} 6718 var label = Object.fromEntries(d.models.map(m => [m.key, labelFix[m.key] || m.label])) 6719 6720 sel.append('div.prompt-block').html(esc(d.example.prompt) 6721 .replace(/(Human|Assistant): /g, '<span class="prompt-role">$1: </span>') 6722 .replace(/I$/, '<span class="tok-hit">I</span>')) 6723 sel.append('div.mlabel').text('Top J-lens tokens') 6724 6725 var rows = sel.append('div.tok-rows') 6726 d.example.rows.forEach(r => { 6727 var row = rows.append('div.tok-row') 6728 var lab = row.append('div.chk-label')
6729 lab.append('span.swatch').st({background: color[r.model]}) 6730 lab.append('span').text(label[r.model] || r.model) 6731 var toks = row.append('div.toks') 6732 r.tokens.forEach((t, i) => { 6733 toks.append('span.tok') 6734 .text(util.ppToken?.(t) ?? t) 6735 .classed('hl', r.hit_idx.includes(i)) 6736 }) 6737 }) 6738} 6739window.init?.() 6740 6741// âââ public/reward-hack-quant/init-reward-hack-quant.js âââ 6742window.initRewardHackQuant = async function(opts){ 6743 var sel = d3.select('.reward-hack-quant').html('') 6744 var base = (opts?.datapath || './').replace(/\/?$/, '/') 6745 var d = await util.getFile(base + 'data.json') 6746 6747 // Ordinal: baseline grey, then three darkening blues. Shared with reward-hack-readout. 6748 var blue = util.tol.blue 6749 var CHK = {'Baseline': util.gray[300], 'SDF': util.tint(blue, .55), 6750 'Phase 1': util.tint(blue, .25), 'Phase 2': blue} 6751 var color = Object.fromEntries(d.models.map((m,i) => 6752 [m.key, CHK[m.key] ?? util.tint(blue, 1 - i/Math.max(d.models.length-1,1))])) 6753 var modelKeys = d.models.map(m => m.key) 6754 6755 var ttSel = d3.select('.tooltip') 6756 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 6757 6758 // n=40: combine both coding categories per checkpoint. 6759 function combined(panel, mk){ 6760 var pts = d.categories.flatMap(cat => panel.cats[cat][mk].points) 6761 var prompts = d.categories.flatMap(cat => panel.cats[cat][mk].prompts || []) 6762 var lensTopk = d.categories.flatMap(cat => panel.cats[cat][mk].lens_topk || []) 6763 var cats = d.categories.flatMap(cat => (panel.cats[cat][mk].points || []).map(_ => cat)) 6764 var mean = pts.length ? d3.mean(pts) : 0 6765 var sem = pts.length > 1 ? d3.deviation(pts) / Math.sqrt(pts.length) : 0 6766 return {points: pts, prompts, lensTopk, cats, mean, sem} 6767 } 6768 6769 // Baked prompts all end "Assistant: I"; box the panel's read-position token. 6770 function renderPrompt(pr, pi){ 6771 var h = esc(pr).replace(/(Human|Assistant): /g, '<span class="prompt-role">$1: </span>') 6772 if (pi == 0) return h.replace(/: <\/span>I$/, '</span><span class="tok-hit">:</span>') 6773 if (pi == 1) return h.replace(/I$/, '<span class="tok-hit">I</span>') 6774 return h.replace(/I$/, '') + '<span class="tok-hit">â¦</span>' 6775 } 6776 6777 var allPts = d.panels.flatMap(p => modelKeys.flatMap(mk => combined(p, mk).points)) 6778 var ymax = d3.max(allPts) || 1 6779 6780 var row = sel.append('div.panel-row') 6781 d.panels.forEach((panel, pi) => { 6782 var title = panel.title.replace(/^./, s => s.toUpperCase()).replace('" I"', '"I"') 6783 var c = d3.conventions({ 6784 sel: row.append('div.panel'), 6785 width: 260, height: 220, 6786 margin: {left: 60, top: 26, right: 8, bottom: 38}, 6787 }) 6788 var x = d3.scaleBand().domain(modelKeys).range([0, c.width]).padding(0.22) 6789 c.y.domain([0, ymax]).nice() 6790 c.yAxis.ticks(5) 6791 c.drawAxis() 6792 c.svg.select('.x').remove() 6793 util.ggPlot?.(c) 6794 util.addAxisLabel?.(c, '', '', title) 6795 if (!pi){ 6796 var yLab = c.svg.select('.y').append('g').translate([-34, c.height/2]) 6797 .append('text.axis-label').at({textAnchor: 'middle', transform: 'rotate(-90)'}) 6798 yLab.append('tspan').text('Deception-vocab in top-10').at({x: 0}) 6799 yLab.append('tspan').text('(sum over WS layers) â').at({x: 0, dy: '1.1em'}) 6800 } 6801 6802 modelKeys.forEach((mk, mi) => { 6803 var cell = combined(panel, mk) 6804 var bx = x(mk), bw = x.bandwidth() 6805 c.svg.append('rect').at({ 6806 x: bx, y: c.y(cell.mean), width: bw, height: c.height - c.y(cell.mean), 6807 fill: color[mk], 6808 }) 6809 var jw = bw * 0.5 6810 cell.points.forEach((v, i) => { 6811 var jx = bx + bw/2 + ((i / Math.max(cell.points.length-1, 1)) - 0.5) * jw 6812 c.svg.append('circle.jpt') 6813 .datum({v, mk, cat: cell.cats[i], pr: cell.prompts[i], lt: cell.lensTopk[i]}) 6814 .at({cx: jx, cy: c.y(v), r: 1.8, fillOpacity: 0, stroke: util.gray[700], strokeWidth: 1}) 6815 }) 6816 if (cell.sem > 0){ 6817 util.barWhisker(c.svg, {x: bx + bw/2, 6818 lo: c.y(cell.mean - cell.sem), hi: c.y(cell.mean + cell.sem)}) 6819 } 6820 c.svg.append('text.mklabel').text(mk) 6821 .at({x: bx + bw/2, y: c.height + 14, textAnchor: 'middle'}) 6822 }) 6823 6824 var last 6825 util.nearestHover(c.svg, c.svg.selectAll('circle.jpt'), { 6826 radius: 40, 6827 onHover: (o, el, e) => { 6828 if (el !== last){ 6829 last = el 6830 ttSel.classed('tooltip-hidden', false).html('') 6831 ttSel.append('div').html(`<b style="color:${color[o.mk]}">${o.mk}</b> · ${o.cat} · count ${o.v}`) 6832 ttSel.append('div.tt-note').text('(count = sum over workspace layers)') 6833 if (o.pr) ttSel.append('div.prompt-block').html(renderPrompt(o.pr, pi)) 6834 if (o.lt?.tokens?.length){ 6835 var toks = ttSel.append('div.toks') 6836 o.lt.tokens.forEach((t, ti) => 6837 toks.append('span.tok').text(util.ppToken?.(t) ?? t).classed('hl', o.lt.hit_idx?.includes(ti))) 6838 } 6839 }
6840 var bb = ttSel.node().getBoundingClientRect() 6841 ttSel.st({left: d3.clamp(20, e.clientX - bb.width/2, innerWidth - bb.width - 20) + 'px', 6842 top: (innerHeight > e.clientY + 20 + bb.height ? e.clientY + 20 : e.clientY - bb.height - 20) + 'px'}) 6843 }, 6844 onLeave: () => { last = null; ttSel.classed('tooltip-hidden', true) }, 6845 }) 6846 }) 6847 6848} 6849window.init?.() 6850 6851// âââ public/modulation-probe/init-modulation-probe.js âââ 6852!function(){ 6853 6854var ORANGE = util.tol.orange, BLUE = util.tol.blue, GREY = util.gray[500], INK = util.gray[700], PURPLE = util.tol.magenta 6855var COND_COLORS = {none: GREY, imagine: 'var(--hl-yellow)', real: BLUE} 6856var MEASURES = [ 6857 {key: 'jlens', label: 'J-lens activation of tokens naming the property', color: ORANGE, off: -1}, 6858 {key: 'probe', label: 'property probe (non-J-space component)', color: PURPLE, off: 1}, 6859] 6860 6861async function loadData(opts){ 6862 var base = (opts?.datapath || './').replace(/\/?$/, '/') 6863 return await util.getFile(base + 'data.json') 6864} 6865 6866function drawExample(sel, d){ 6867 var ex = d.example 6868 sel.append('div.fig-title').text('Example: imagining JavaScript code is Python') 6869 6870 var prompt = sel.append('div.prompt') 6871 prompt.append('span.role').text('Human: ') 6872 prompt.append('span.instr').text(ex.instruction) 6873 if (ex.carrier_box){ 6874 prompt.append('span').text('\n\n' + ex.carrier_box.pre) 6875 prompt.append('span.tok-hit').text(ex.carrier_box.tok) 6876 prompt.append('span').text(ex.carrier_box.post) 6877 } else { 6878 prompt.append('span').text('\n\n' + ex.carrier_text) 6879 } 6880 6881 sel.append('div.mlabel').text( 6882 'Top-5 J-lens readout at the boxed token, ' + ex.layer_label + ' (log probs)') 6883 var cols = sel.append('div.lenscols') 6884 var hits = new Set((ex.hit_tokens || []).map(t => t.toLowerCase())) 6885 var half = ex.probe_half_range 6886 ex.rows.forEach(row => { 6887 var col = cols.append('div.lenscol').classed('imagine', row.cond === 'imagine') 6888 var head = col.append('div.chead') 6889 head.append('span.legend-sq').st({background: COND_COLORS[row.cond]}) 6890 head.append('span').text(row.label) 6891 // The values are log-probs; the bar widths are each token's probability 6892 // relative to the column's top token. 6893 var vmax = d3.max(row.tokens, t => t[1]) ?? 0 6894 row.tokens.forEach(t => { 6895 var r = col.append('div.lensrow') 6896 r.append('div.fill').st({width: (Math.exp(Math.min(t[1] - vmax, 0))*100) + '%', background: COND_COLORS[row.cond]}) 6897 r.append('span.t').classed('hit', hits.has(t[0].trim().toLowerCase())).text(t[0]) 6898 r.append('span.v').text(d3.format('.2f')(t[1])) 6899 }) 6900 col.append('div.ranknote').st({marginTop: 16}).text(ex.probe_label || 'property probe') 6901 var g = col.append('div.gauge') 6902 var tr = g.append('div.gtrack') 6903 var pct = d3.clamp(0, 50 + (row.probe_z / half) * 50, 100) 6904 var lbl = d3.format('+.1f')(row.probe_z) + 'Ï' 6905 tr.append('div.gzero') 6906 tr.append('div.gstem').st({left: Math.min(50, pct) + '%', width: Math.abs(pct - 50) + '%'}) 6907 tr.append('div.gdot').st({left: pct + '%'}) 6908 tr.append('div.gval').st({left: d3.clamp(10, pct, 90) + '%'}).text(lbl) 6909 }) 6910} 6911 6912function drawPanel(sel, cond, d, ymax, ymin){ 6913 var cats = d.quant.categories 6914 var margin = {left: 22, top: 8, right: 4, bottom: 30} 6915 var c = d3.conventions({ 6916 sel: sel.append('div.panel'), 6917 width: 360, height: 75, 6918 margin, 6919 }) 6920 // viewBox + width:100% lets the panel shrink with a narrow column (mobile) 6921 // while max-width keeps the desktop render at its natural size. 6922 var totW = 360 + margin.left + margin.right, totH = 75 + margin.top + margin.bottom 6923 d3.select(c.svg.node().ownerSVGElement) 6924 .at({viewBox: '0 0 ' + totW + ' ' + totH}) 6925 .st({width: '100%', maxWidth: totW + 'px', height: 'auto'}) 6926 var x = d3.scaleBand().domain(cats).range([0, c.width]).padding(0.28) 6927 c.y.domain([ymin, ymax]) 6928 c.x = x 6929 c.xAxis = d3.axisBottom(x).tickFormat(k => (d.category_labels[k] || k).split(' (')[0]) 6930 c.yAxis.ticks(5) 6931 c.drawAxis() 6932 util.ggPlot(c) 6933 c.svg.selectAll('.x text').st({fontSize: 9}) 6934 .each(function(k){ 6935 var lbl = (d.category_labels[k] || k) 6936 var m = lbl.match(/^(.*?) \((.*)\)$/) 6937 if (!m) return 6938 d3.select(this).text('') 6939 d3.select(this).append('tspan').at({x: 0, dy: '0.71em'}).text(m[1]) 6940 d3.select(this).append('tspan').at({x: 0, dy: '1.1em'}).text('(' + m[2] + ')') 6941 }) 6942 c.svg.append('line').at({x1: 0, x2: c.width, y1: c.y(0), y2: c.y(0), stroke: INK, strokeWidth: 0.6}) 6943 6944 var bw = x.bandwidth() / 2 - 2 6945 MEASURES.forEach(m => { 6946 var cell = cond.cells[m.key] 6947 if (!cell) return
6948 cats.forEach((cat, ci) => { 6949 var bx = x(cat) + x.bandwidth()/2 + (m.off < 0 ? -bw - 1 : 1) 6950 var mean = cell.means[ci] 6951 c.svg.append('rect').at({ 6952 x: bx, y: Math.min(c.y(0), c.y(mean)), 6953 width: bw, height: Math.abs(c.y(mean) - c.y(0)), 6954 fill: m.color, 6955 }) 6956 var pts = cell.points[ci] || [] 6957 pts.forEach((v, j) => { 6958 var jit = (pts.length > 1 ? (j/(pts.length-1) - 0.5) : 0) * bw * 0.6 6959 c.svg.append('circle').at({ 6960 cx: bx + bw/2 + jit, cy: c.y(v), r: 2.1, 6961 fillOpacity: 0, stroke: util.gray[700], strokeWidth: 1, 6962 }) 6963 }) 6964 // ±1 SEM error bar on the wrong-target-controlled differences. 6965 var sem = cell.sems?.[ci] 6966 if (sem != null){ 6967 var cx = bx + bw/2 6968 c.svg.append('line').at({x1: cx, x2: cx, y1: c.y(mean - sem), y2: c.y(mean + sem), stroke: util.gray[700], strokeWidth: 1.2}) 6969 ;[mean - sem, mean + sem].forEach(v => { 6970 c.svg.append('line').at({x1: cx - 2.5, x2: cx + 2.5, y1: c.y(v), y2: c.y(v), stroke: util.gray[700], strokeWidth: 1.2}) 6971 }) 6972 } 6973 }) 6974 }) 6975 return c 6976} 6977 6978window.initModulationProbe = async function(opts){ 6979 var sel = d3.select('.modulation-probe').html('') 6980 var d = await loadData(opts) 6981 if (!d?.quant?.conds || !d?.example?.hit_tokens || d.example.probe_half_range == null) return 6982 var row = sel.append('div.mp-row') 6983 drawExample(row.append('div'), d) 6984 6985 var right = row.append('div') 6986 right.append('div.fig-title').text('Across four properties') 6987 // Both bar panels share one y extent so the imagine vs. real effect sizes 6988 // read directly off the bar heights. 6989 var condVals = cond => { 6990 var vals = [] 6991 MEASURES.forEach(m => (cond.cells[m.key]?.points || []).forEach(p => vals.push(...p))) 6992 return vals 6993 } 6994 // cover both the baked extents and the raw points so jitter dots never leave the plot area 6995 var ymax = d3.max(d.quant.conds, c => Math.max(c.ymax ?? -Infinity, d3.max(condVals(c)) * 1.08)) 6996 var ymin = d3.min(d.quant.conds, c => Math.min(c.ymin ?? Infinity, Math.min(0, d3.min(condVals(c))) - 0.4)) 6997 d.quant.conds.forEach((cond, i) => { 6998 right.append('div.ptitle-sm').st({marginTop: i === 0 ? 0 : 14}) 6999 .text(cond.title) 7000 drawPanel(right, cond, d, ymax, ymin) 7001 }) 7002 var leg = right.append('div.legend') 7003 MEASURES.forEach(m => { 7004 var l = leg.append('div') 7005 l.append('span.legend-sq').st({background: m.color}) 7006 l.append('span').text(m.label) 7007 }) 7008} 7009 7010}() 7011window.init?.() 7012 7013// âââ public/probe-swap/init-probe-swap.js âââ 7014!function(){ 7015 7016async function loadData(opts){ 7017 var base = (opts?.datapath || './').replace(/\/?$/, '/') 7018 return await util.getFile(base + 'data.json') 7019} 7020 7021// Shared J-space condition palette (util.jspaceColors); fall back to the 7022// baked hex if a key isn't in the map. 7023var condColor = o => util.jspaceColors[o.key] ?? o.color 7024 7025function rankCol(sel, r, ex){ 7026 var t = sel.append('table.bars') 7027 var th = t.append('thead').append('tr').append('th') 7028 th.append('span.sw.swatch').st({background: condColor(r)}) 7029 th.append('span').text(r.label) 7030 var tb = t.append('tbody') 7031 // Tokens only â the swap target's rank/log-prob annotations are carried 7032 // in the data but not rendered (matching the polished figure). 7033 r.tokens.forEach(tok => { 7034 var cls = 'tok' 7035 if (tok.toLowerCase() == ex.expected.toLowerCase()) cls = 'tok hit' 7036 else if (tok.toLowerCase() == ex.original.toLowerCase()) cls = 'tok orig' 7037 tb.append('tr').append('td').append('div.entry') 7038 .append('span').at({class: cls}).text(tok) 7039 }) 7040} 7041 7042// One concept's probe decomposition as an equation broken at the "+": 7043// [probe for X] = [J-space component: atoms ...] 7044// + [non-J-space component] 7045// The "+" line stacks under the J-space chip on the right of the "=". 7046function eqBlock(sel, e){ 7047 var b = sel.append('div.eqblock') 7048 var lhs = b.append('div.eqline.eqlhs') 7049 var c0 = lhs.append('span.eqchip') 7050 c0.append('span').text('probe for ') 7051 c0.append('b').text(e.concept) 7052 lhs.append('span.eqop').text('=') 7053 var rhs = b.append('div.eqrhs') 7054 var l1 = rhs.append('div.eqline') 7055 var c1 = l1.append('span.eqchip') 7056 c1.append('span.sw.swatch').st({background: util.jspaceColors.aligned}) 7057 c1.append('span').text('J-space component')
7058 e.atoms.forEach(a => c1.append('span.atom').text(a)) 7059 c1.append('span.eqell').text('â¦') 7060 var l2 = rhs.append('div.eqline.cont') 7061 l2.append('span.eqop').text('+') 7062 var c2 = l2.append('span.eqchip') 7063 c2.append('span.sw.swatch').st({background: util.jspaceColors.residual}) 7064 c2.append('span').text('non-J-space component') 7065} 7066 7067function drawExample(sel, d){ 7068 var ex = d.example 7069 sel.append('div.ptitle') 7070 .text('Example: swapping the working representation of an inferred intermediate') 7071 // Plain prompt text; only the answer slot (the trailing "is ___") is 7072 // marked. 7073 var p = sel.append('div.prompt') 7074 var txt = ex.prompt.trim() 7075 var stem = txt.endsWith(' is') ? txt.slice(0, -3) : txt 7076 p.append('span').text(stem + ' ') 7077 p.append('span.tok-hit').text('is') 7078 7079 ;(ex.decomp || []).forEach(e => eqBlock(sel, e)) 7080 7081 sel.append('div.colslab').text( 7082 `model's next-token distribution after exchanging ${ex.src_concept} â ${ex.tgt_concept} along each pair of directions`) 7083 // One shared table (not 4 independent ones) so header height and row 7084 // baselines are identical across all readout columns. 7085 var t = sel.append('div.cols').append('table.bars') 7086 var hr = t.append('thead').append('tr') 7087 // Explicit two-line splits so all four headers sit at the same height; the 7088 // last label is widest and is allowed to overflow its column slightly right. 7089 var hsplit = s => { 7090 if (s == 'clean (no swap)') return ['clean', '(no swap)'] 7091 var m = s.match(/^(.*\S) (component|vectors)$/) 7092 return m ? [m[1], m[2]] : [s] 7093 } 7094 ex.readouts.forEach(r => { 7095 var th = hr.append('th') 7096 var hw = th.append('div.hwrap') 7097 hw.append('span.sw.swatch').st({background: condColor(r)}) 7098 hw.append('span.hlabel').appendMany('div', hsplit(r.label)).text(d => d) 7099 }) 7100 var tb = t.append('tbody') 7101 var nrows = d3.max(ex.readouts, r => r.tokens.length) 7102 d3.range(nrows).forEach(i => { 7103 var tr = tb.append('tr') 7104 ex.readouts.forEach(r => { 7105 var tok = r.tokens[i] 7106 var td = tr.append('td') 7107 if (tok == null) return 7108 var cls = 'tok' 7109 if (tok.toLowerCase() == ex.expected.toLowerCase()) cls = 'tok hit' 7110 else if (tok.toLowerCase() == ex.original.toLowerCase()) cls = 'tok orig' 7111 td.append('div.entry').append('span').at({class: cls}).text(tok) 7112 }) 7113 }) 7114} 7115 7116function drawBars(sel, d){ 7117 var w = d.bars 7118 sel.append('div.ptitle').text(`Across ${d.n_items} two-hop reasoning prompts`) 7119 var c = d3.conventions({ 7120 sel: sel.append('div.panel'), 7121 // Wide enough that adjacent bars' multi-line x labels ("probe, 7122 // non-J-space component" next to "+ J-space coords clamped") don't 7123 // collide at the figure's label font size. 7124 width: 400, height: 280, 7125 // The bottom margin holds the multi-line bar labels plus the 7126 // variance-share annotations arrowed to the two component labels; 7127 // the left margin holds the two-line y label. 7128 margin: {left: 64, top: 16, right: 8, bottom: 86}, 7129 }) 7130 // Each series carries its own x position so the complementary-clamp 7131 // variant of each component sits adjacent to its parent, with gaps 7132 // between the groups. 7133 var xmin = d3.min(w.series, s => s.x ?? 0), xmax = d3.max(w.series, s => s.x ?? 0) 7134 c.x.domain([xmin - 0.55, xmax + 0.55]) 7135 c.y.domain([0, 1]) 7136 c.xAxis.tickValues(w.series.map(s => s.x ?? 0)).tickFormat(() => '') 7137 c.yAxis.tickValues([0, .25, .5, .75, 1]).tickFormat(d3.format('.0%')) 7138 c.drawAxis() 7139 util.ggPlot?.(c) 7140 7141 var bw = 0.7 7142 w.series.forEach((s, i) => { 7143 var xc = s.x ?? i, col = condColor(s) 7144 c.svg.append('rect').at({ 7145 x: c.x(xc - bw / 2), width: c.x(xc + bw / 2) - c.x(xc - bw / 2), 7146 y: c.y(s.frac), height: c.y(0) - c.y(s.frac), 7147 fill: col, 7148 }) 7149 c.svg.append('line').at({ 7150 x1: c.x(xc), x2: c.x(xc), y1: c.y(s.ci_lo), y2: c.y(s.ci_hi), 7151 stroke: util.gray[700], strokeWidth: 1.2, 7152 }) 7153 ;[s.ci_lo, s.ci_hi].forEach(v => c.svg.append('line').at({ 7154 x1: c.x(xc) - 3, x2: c.x(xc) + 3, y1: c.y(v), y2: c.y(v), 7155 stroke: util.gray[700], strokeWidth: 1.2, 7156 })) 7157 c.svg.append('text').text(d3.format('.0%')(s.frac)) 7158 .at({x: c.x(xc), y: c.y(s.ci_hi) - 6, textAnchor: 'middle', fill: util.gray[700]}) 7159 .st({fontSize: 'var(--fs-small)'}) 7160 // One word per line, "+" glued to its first word: every label's widest 7161 // line is a single word, so adjacent multi-line labels can't collide. 7162 var words = s.label.split(' ') 7163 if (words[0] == '+' && words.length > 1) words = ['+ ' + words[1], ...words.slice(2)]
7164 words = words.map(w => w.replace(/,$/, '')) 7165 var t = c.svg.append('text.slabel') 7166 .at({x: c.x(xc), y: c.height + 14, textAnchor: 'middle', fill: util.gray[600]}) 7167 words.forEach((ln, j) => { 7168 t.append('tspan').text(ln).at({x: c.x(xc), dy: j ? 10 : 0}) 7169 }) 7170 }) 7171 // Average variance-share annotations: arrows pointing up from below at 7172 // the two component bars' x labels, quoting how much of the probe's 7173 // variance each component carries on average. 7174 if (d.share_mean != null){ 7175 var pct = d3.format('.0%') 7176 var shares = {probe_jpart2: d.share_mean, probe_ortho2: 1 - d.share_mean} 7177 w.series.filter(s => shares[s.key] != null).forEach(s => { 7178 var x = c.x(s.x ?? 0), yh = c.height + 41 7179 c.svg.append('path').at({ 7180 d: `M ${x} ${yh + 12} L ${x} ${yh} M ${x - 3} ${yh + 4} L ${x} ${yh} L ${x + 3} ${yh + 4}`, 7181 stroke: util.gray[600], fill: 'none', strokeWidth: 1, 7182 }) 7183 var t = c.svg.append('text.sharenote') 7184 .at({x, y: yh + 24, textAnchor: 'middle', fill: util.gray[600]}) 7185 t.append('tspan').text(`${pct(shares[s.key])} of probe variance`).at({x, dy: 0}) 7186 t.append('tspan').text('on average').at({x, dy: 10}) 7187 }) 7188 } 7189 7190 // Two lines so the label fits inside the plot height instead of 7191 // overflowing past the title and into the example column. 7192 var yl = c.svg.append('text') 7193 .at({transform: `translate(${-56},${c.height / 2}) rotate(-90)`, textAnchor: 'middle'}) 7194 .st({fontSize: 'var(--fs-small)'}) 7195 yl.append('tspan').text('% of answers that flip').at({x: 0, dy: 0}) 7196 yl.append('tspan').text("to the swap target's answer").at({x: 0, dy: 13}) 7197} 7198 7199window.initProbeSwap = async function(opts){ 7200 var sel = d3.select('.probe-swap').html('') 7201 var d = await loadData(opts) 7202 if (!d?.example?.decomp || !d?.bars) return 7203 var grid = sel.append('div.ps-row') 7204 drawExample(grid.append('div'), d) 7205 drawBars(grid.append('div'), d) 7206} 7207 7208}() 7209window.init?.() 7210 7211// âââ public/verbal-report-decomposition-merged/init-verbal-report-decomposition-merged.js âââ 7212!function(){ 7213 7214async function loadData(opts){ 7215 var base = (opts?.datapath || './').replace(/\/?$/, '/') 7216 return await util.getFile(base + 'data.json') 7217} 7218 7219// Conditions drawn in the two quantification panels (display order: controls 7220// first, the J-lens reference last). The random control is exactly 0 in both 7221// panels and is stated in the caption instead. 7222// Colors come from the shared util.jspaceColors so this figure matches probe-swap. 7223function remapConds(d){ d.conditions.forEach(c => c.color = util.jspaceColors[c.key] ?? c.color) } 7224// Display order: pure J-lens â J-space â non-J-space â clamped (controls last). 7225var ORDER = ['jlens', 'aligned', 'full', 'residual', 'residual_jclamp', 'random'] 7226function drawnConds(d){ 7227 return d.conditions.filter(c => c.in_swap_panel !== false) 7228 .sort((a, b) => ORDER.indexOf(a.key) - ORDER.indexOf(b.key)) 7229} 7230 7231function fmtPct(x){ return d3.format('.0%')(x) } 7232 7233function drawConstruction(sel, d){ 7234 var c = d.construction 7235 var conds = Object.fromEntries(d.conditions.map(x => [x.key, x])) 7236 7237 sel.append('div.ptitle').text('Constructing and decomposing a concept vector') 7238 sel.append('div.steplab').text( 7239 'Record the activation at the highlighted position; split it against the J-lens dictionary:') 7240 7241 var prompt = sel.append('div.prompt') 7242 prompt.append('span.role').text('Human:') 7243 prompt.append('span').text(` Tell me about ${c.word}\n\n`) 7244 prompt.append('span.role').text('Assistant') 7245 prompt.append('span.tok-hit').text(':') 7246 7247 // The cards show the worked example's own variance shares; the 7248 // cross-concept medians live in the caption. 7249 var avLab = `${Math.round(100*c.aligned_var)}% of variance` 7250 var rvLab = `${Math.round(100*c.residual_var)}% of variance` 7251 7252 // The decomposition rendered as an explicit sum: 7253 // concept vector = J-space component + non-J-space component 7254 var vchip = sel.append('div.vchip') 7255 var vline = vchip.append('div') 7256 vline.append('span').text('concept vector for â') 7257 var cvw = vline.append('span').text(c.word) 7258 vline.append('span').text('â') 7259 vchip.append('div.pct').text(`activation â mean over ${d.meta.baseline_n_words} baseline words`) 7260 7261 sel.append('div.eqop').text('=') 7262 7263 var aligned = sel.append('div.card') 7264 var alab = aligned.append('div.clab') 7265 alab.append('span.legend-sq').st({background: conds.aligned.color}) 7266 alab.append('span').text('J-space component ') 7267 alab.append('span.pct').text(`(${avLab})`) 7268 aligned.append('div.atoms').appendMany('span.atom', c.atoms) 7269 .html(a => `${util.ppToken(a.token)}<span class='w'>${d3.format('.2f')(a.weight)}</span>`) 7270 aligned.append('div.csub').text( 7271 `non-negative combination of ${d.meta.ito_k} J-lens vectors (matching pursuit); ⦠${d.meta.ito_k - c.atoms.length} more atoms`) 7272 7273 sel.append('div.eqop').text('+') 7274 7275 var resid = sel.append('div.card') 7276 var rlab = resid.append('div.clab') 7277 rlab.append('span.legend-sq').st({background: conds.residual.color}) 7278 rlab.append('span').text('non-J-space component ') 7279 rlab.append('span.pct').text(`(${rvLab})`) 7280 resid.append('div.csub').text('not aligned with any small set of J-lens vectors') 7281 7282 // Connector: exit rightward from the ":" token, curve down, land on the 7283 // chip's TOP BORDER directly above the concept word â path stays in the 7284 // whitespace right of "Assistant:" and outside the chip. 7285 sel.st({position: 'relative'})
7286 var sb = sel.node().getBoundingClientRect() 7287 var tb = prompt.select('.tok-hit').node().getBoundingClientRect() 7288 var vb = vchip.node().getBoundingClientRect() 7289 var wb = cvw.node().getBoundingClientRect() 7290 var x0 = tb.right - sb.left + 1, y0 = (tb.top + tb.bottom)/2 - sb.top 7291 var x1 = wb.left + wb.width/2 - sb.left, y1 = vb.top - sb.top 7292 sel.append('svg.connector') 7293 .st({position: 'absolute', left: 0, top: 0, pointerEvents: 'none', overflow: 'visible'}) 7294 .at({width: 1, height: 1}) 7295 .append('path').at({ 7296 d: `M ${x0} ${y0} C ${x0+20} ${y0+4}, ${x1} ${y0+4}, ${x1} ${y1}`, 7297 stroke: util.gray[500], strokeWidth: 1, fill: 'none', 7298 }) 7299} 7300 7301// Shared y domain for the two bar panels â proportion axis, clamped to [0, 1]. 7302// CI whiskers and per-category dots are clamped to the same range so the axis 7303// never extends below zero to fit them. 7304var Y_DOMAIN = [0, 1] 7305function clampY(v){ return Math.max(0, Math.min(1, v)) } 7306 7307function drawSwap(sel, d){ 7308 sel.append('div.ptitle').text('âThink of a {category}â experiment') 7309 sel.append('div.steplab').text( 7310 "Swap the chosen answer's directions for a target item's; does the target become the answer?") 7311 var chart = sel.append('div.vrdm-chart') 7312 var conds = drawnConds(d) 7313 var c = d3.conventions({ 7314 sel: chart, 7315 width: 240, height: 260, 7316 margin: {left: 48, top: 14, right: 8, bottom: 18}, 7317 }) 7318 c.x = d3.scaleBand().domain(conds.map(x => x.key)).range([0, c.width]).padding(0.35)
7319 c.y = d3.scaleLinear().domain(Y_DOMAIN).range([c.height, 0]) 7320 c.xAxis.scale(c.x).tickFormat(() => '') 7321 c.yAxis.scale(c.y).ticks(6).tickFormat(d3.format('.0%')) 7322 c.drawAxis() 7323 util.ggPlot(c) 7324 util.addAxisLabel(c, '', d.swap.ylabel, '', 0, -10) 7325 7326 conds.forEach(cond => { 7327 var s = d.swap.series[cond.key] 7328 var x0 = c.x(cond.key), bw = c.x.bandwidth() 7329 c.svg.append('rect').at({ 7330 x: x0, y: c.y(s.p5), width: bw, height: c.y(0) - c.y(s.p5), 7331 fill: cond.color, 7332 }) 7333 c.svg.append('path').at({ 7334 d: `M ${x0 + bw/2 - 3} ${c.y(clampY(s.ci_lo))} h 6 M ${x0 + bw/2} ${c.y(clampY(s.ci_lo))} V ${c.y(clampY(s.ci_hi))} M ${x0 + bw/2 - 3} ${c.y(clampY(s.ci_hi))} h 6`, 7335 stroke: util.gray[700], strokeWidth: 1.2, fill: 'none', 7336 }) 7337 // per-category dots, deterministic horizontal spread. No percent label 7338 // above the bar (matching the polished panel). 7339 c.svg.appendMany('circle', s.per_category).at({ 7340 cx: (q, i) => x0 + bw * (0.2 + 0.6 * (i / Math.max(s.per_category.length - 1, 1))), 7341 cy: q => c.y(clampY(q.p5)), r: 2.2, 7342 fillOpacity: 0, stroke: util.gray[700], strokeWidth: 1, 7343 }) 7344 }) 7345} 7346 7347function drawDose(sel, d){ 7348 sel.append('div.ptitle').text('âInjected thoughtâ experiment') 7349 sel.append('div.steplab').text( 7350 'Inject each component; does the model name the concept when asked what it is thinking about? Results taken from each conditionâs best injection strength.') 7351 var chart = sel.append('div.vrdm-chart') 7352 var conds = drawnConds(d) 7353 var c = d3.conventions({ 7354 sel: chart, 7355 width: 240, height: 260, 7356 margin: {left: 48, top: 14, right: 8, bottom: 18}, 7357 }) 7358 // Bar chart of the max P@5 over the strength sweep (the bake's peak_p5), 7359 // mirroring the swap panel; the full per-strength dose-response curves 7360 // live in the appendix figure (verbal-report-decomposition-dose). 7361 c.x = d3.scaleBand().domain(conds.map(x => x.key)).range([0, c.width]).padding(0.35)
7362 c.y = d3.scaleLinear().domain(Y_DOMAIN).range([c.height, 0]) 7363 c.xAxis.scale(c.x).tickFormat(() => '') 7364 c.yAxis.scale(c.y).ticks(6).tickFormat(d3.format('.0%')) 7365 c.drawAxis() 7366 util.ggPlot(c) 7367 util.addAxisLabel(c, '', d.dose.ylabel, '', 0, -10) 7368 7369 // No condition labels on the bars â the shared legend under panels 2-3 7370 // identifies them (matching the swap panel); each bar is annotated with 7371 // the injection strength its results are taken from. 7372 conds.forEach(cond => { 7373 var s = d.dose.series[cond.key] 7374 var x0 = c.x(cond.key), bw = c.x.bandwidth() 7375 c.svg.append('rect').at({ 7376 x: x0, y: c.y(s.peak_p5), width: bw, height: c.y(0) - c.y(s.peak_p5), 7377 fill: cond.color, 7378 }) 7379 c.svg.append('path').at({ 7380 d: `M ${x0 + bw/2 - 3} ${c.y(clampY(s.peak_ci_lo))} h 6 M ${x0 + bw/2} ${c.y(clampY(s.peak_ci_lo))} V ${c.y(clampY(s.peak_ci_hi))} M ${x0 + bw/2 - 3} ${c.y(clampY(s.peak_ci_hi))} h 6`, 7381 stroke: util.gray[700], strokeWidth: 1.2, fill: 'none', 7382 }) 7383 c.svg.append('text').at({ 7384 x: x0 + bw/2, y: c.y(s.peak_ci_hi) - 4, textAnchor: 'middle', 7385 fontSize: 8.5, fill: '#6b7280', 7386 }).text(`Strength: ${s.peak_strength}`) 7387 // No dots here: outcomes at a single strength are binary per concept, 7388 // so there is no per-group distribution to show (unlike the swap 7389 // panel's per-category dots); the full dose-response curves live in 7390 // the appendix figure. 7391 }) 7392} 7393 7394function drawLegend(sel, d){ 7395 var leg = sel.append('div.legendrow') 7396 // Legend reads in the same left-to-right order as the bars in the panels. 7397 drawnConds(d).forEach(c => { 7398 leg.append('div').html(`<span class='legend-sq' style='background:${c.color}'></span>${c.label}`) 7399 }) 7400} 7401 7402window.initVerbalReportDecompositionMerged = async function(opts){ 7403 var sel = d3.select('.verbal-report-decomposition-merged').html('') 7404 var d = await loadData(opts) 7405 if (!d?.construction || d?.swap?.series?.aligned?.p5 == null) return 7406 if (d?.dose?.series?.aligned?.peak_p5 == null) return 7407 remapConds(d) 7408 drawConstruction(sel.append('div.vrdm-col.construction'), d) 7409 var quant = sel.append('div.vrdm-quant') 7410 var charts = quant.append('div.charts') 7411 drawSwap(charts.append('div.vrdm-col'), d) 7412 drawDose(charts.append('div.vrdm-col'), d) 7413 drawLegend(quant, d) 7414} 7415 7416}() 7417window.init?.() 7418 7419// âââ public/selfreport/init-selfreport.js âââ 7420// Self-reports of experience. Two render paths share this module:
7421// - selfreport-soc / -other / -questions / -story / -controls / -dose draw 7422// d3 charts from the baked data.json so they follow the paper's figure 7423// conventions. 7424// - selfreport-examples (the response browser) injects the section's 7425// reviewed export verbatim (phenomenology/figures.py â figures_html/), 7426// via the loader below. 7427!function(){ 7428 7429// âââââ verbatim-export loader (jack's pipeline) âââââ 7430 7431// The exported figure HTML is build-generated (model text is escaped at 7432// build time) and fetched same-origin, but the datapath is a URL parameter: 7433// restrict it to relative paths and strip any active content before 7434// injection so a crafted ?datapath= cannot turn the loader into XSS. 7435function safeBase(base){ 7436 // relative paths only â init.js passes bare 'data/<dir>/' (no './' prefix), 7437 // so reject schemes, protocol-relative '//', and root-absolute '/' instead 7438 // of requiring a leading './'. 7439 return !/^[a-z]+:|^\/|\/\//i.test(base) 7440} 7441 7442function sanitizeFigureHtml(html){ 7443 var doc = new DOMParser().parseFromString(html, 'text/html') 7444 doc.querySelectorAll('script,iframe,object,embed,link,meta').forEach(n => n.remove()) 7445 doc.querySelectorAll('*').forEach(n => { 7446 for (var a of [...n.attributes]){ 7447 if (/^on/i.test(a.name)) n.removeAttribute(a.name) 7448 else if (/^(href|src|srcset|xlink:href)$/i.test(a.name) 7449 && /^\s*(javascript|vbscript|data:text)/i.test(a.value)) n.removeAttribute(a.name) 7450 } 7451 }) 7452 return doc.body.innerHTML 7453} 7454 7455var _cssDone = null 7456async function ensureCss(base){ 7457 if (_cssDone) return _cssDone 7458 _cssDone = (async () => { 7459 var css = await util.getFile(base + 'figures_html/figures.css') 7460 // figures.css is a standalone-page stylesheet with bare selectors (.row, 7461 // body, h2, â¦) â scope it under the loader mount so it can't leak onto 7462 // other figures' .row/.tok/etc. elements. 7463 d3.select('html').selectAppend('style.selfreport-figures-css') 7464 .text('.selfreport-examples {\n' + String(css).replaceAll('</', '<\\/') + '\n}') 7465 })() 7466 return _cssDone 7467} 7468 7469function makeInit(name){ 7470 return async function(opts){ 7471 var base = (opts?.datapath || './').replace(/\/?$/, '/') 7472 if (!safeBase(base)) return 7473 var sel = d3.select('.' + name) 7474 if (sel.empty()) return 7475 await ensureCss(base) 7476 var html = await util.getFile(base + 'figures_html/' + name + '.html') 7477 sel.html(sanitizeFigureHtml(String(html))) 7478 } 7479} 7480 7481window.initSelfreportExamples = makeInit('selfreport-examples') 7482 7483// âââââ d3 figures from data.json âââââ 7484 7485var _data = null 7486async function loadData(opts){ 7487 if (_data) return _data 7488 var base = (opts?.datapath || './').replace(/\/?$/, '/') 7489 _data = await util.getFile(base + 'data.json') 7490 return _data 7491} 7492 7493// Bar order leftâright; baseline gray, the J-space ablation red right next to 7494// it (red = ablated, never a series color), then the matched controls as blue 7495// tints. 7496var CONDS = [ 7497 {key: 'clean', color: util.gray[400]}, 7498 {key: 'ablate', color: util.tol.red}, 7499 {key: 'random', color: util.tint(util.tol.blue, 0.75)}, 7500 {key: 'ortho', color: util.tint(util.tol.blue, 0.5)}, 7501 {key: 'sae', color: util.tint(util.tol.blue, 0.25)}, 7502] 7503 7504// Match the reviewed figures' control wording until the bake re-runs with 7505// the renamed labels. 7506var COND_LABEL_OVERRIDE = { 7507 random: 'random control', 7508 ortho: 'non-J-space control', 7509 sae: 'SAE-direction control', 7510} 7511function condLabel(d, key){ return COND_LABEL_OVERRIDE[key] || d.cond_labels[key] || key } 7512 7513// "sonnet-4.5" â ["Sonnet", "4.5"] â two-line model labels under the bars. 7514function modelLines(m){ 7515 var [name, ...rest] = m.split('-') 7516 return [name[0].toUpperCase() + name.slice(1), rest.join('-')] 7517} 7518 7519// âââââ shared: example pair cards âââââ 7520 7521function drawPair(sel, pair, pairOpts={}){ 7522 var row = sel.append('div.exrow') 7523 if (!pairOpts.noHead){ 7524 var ex = row.append('div.exhead') 7525 ex.append('span.prompt-role').text('Human: ') 7526 ex.append('span').text(pair.prompt) 7527 } 7528 var grid = row.append('div.expair') 7529 ;[['baseline', util.gray[400], pair.baseline, pair.baseline_first], 7530 ['J-space ablated', util.tol.red, pair.ablated, pair.ablated_first]] 7531 .forEach(([label, color, txt, first]) => { 7532 var card = grid.append('div.card') 7533 var head = card.append('div.label') 7534 head.append('span.swatch').st({background: color}) 7535 head.append('span').text(label) 7536 // Questions excerpts are multi-turn â show each condition's full 7537 // conversation (shared framing/question, per-condition first 7538 // response, then the graded answer) in its own card. soc/other/story 7539 // pairs lack pair.framing and fall through to the single-excerpt path. 7540 if (pair.framing){ 7541 ;[['Human', pair.framing], ['Assistant', first], 7542 ['Human', pair.question], ['Assistant', txt]] 7543 .forEach(([who, t]) => { 7544 if (!t) return 7545 var turn = card.append('div.excerpt.prompt-block') 7546 turn.append('span.prompt-role').text(who + ': ') 7547 turn.append('span').text(t) 7548 }) 7549 } else { 7550 var body = card.append('div.excerpt.prompt-block') 7551 body.append('span.prompt-role').text('Assistant: ') 7552 body.append('span').text(txt) 7553 } 7554 }) 7555} 7556 7557// âââââ shared: experiential-language grouped bar panel âââââ 7558 7559function drawScoreBars(sel, d, barData, opts={}){ 7560 var models = d.models.filter(m => barData[m]) 7561 var c = d3.conventions({ 7562 sel, 7563 width: opts.width || 250, height: opts.height || 280, 7564 margin: {left: 46, top: 10, right: 8, bottom: 40}, 7565 }) 7566 var x = d3.scaleBand().domain(models).range([0, c.width]).padding(0.22) 7567 var conds = CONDS.filter(cond => models.some(m => barData[m][cond.key])) 7568 var xi = d3.scaleBand().domain(conds.map(d => d.key)).range([0, x.bandwidth()]).padding(0.12) 7569 var yMax = opts.yMax || 1 7570 c.y.domain([0, yMax]) 7571 c.yAxis.ticks(5) 7572 c.drawAxis() 7573 c.svg.select('.x').remove() 7574 util.ggPlot(c) 7575 7576 c.svg.append('text.axlabel').text(opts.ylabel || 'Experiential language score') 7577 .at({transform: `rotate(-90)`, x: -c.height/2, y: -34, textAnchor: 'middle'}) 7578
7579 models.forEach(m => { 7580 conds.forEach(cond => { 7581 var a = barData[m][cond.key] 7582 if (!a) return 7583 var bx = x(m) + xi(cond.key), bw = xi.bandwidth() 7584 c.svg.append('rect').at({ 7585 x: bx, y: c.y(a.mean), width: bw, height: c.height - c.y(a.mean), 7586 fill: cond.color, 7587 }) 7588 var cx = bx + bw/2 7589 var lo = Math.max(0, a.mean - a.ci), hi = Math.min(yMax, a.mean + a.ci) 7590 c.svg.append('line').at({x1: cx, x2: cx, y1: c.y(lo), y2: c.y(hi), stroke: util.gray[700], strokeWidth: 1.2}) 7591 if (opts.dots ?? true) (a.per_prompt || []).forEach((v, j, arr) => { 7592 var jitter = 0.4 * bw * ((j / Math.max(arr.length - 1, 1)) - 0.5) 7593 c.svg.append('circle').at({cx: cx + jitter, cy: c.y(v), r: 2, fill: 'none', stroke: util.gray[700], strokeWidth: 1}) 7594 }) 7595 }) 7596 // Two-line model labels ("Sonnet" / "4.5") so the narrow panels never 7597 // overlap adjacent labels. 7598 var t = c.svg.append('text') 7599 .at({x: x(m) + x.bandwidth()/2, y: c.height + 16, textAnchor: 'middle'}) 7600 .st({fontSize: 'var(--fs-body)', fill: 'var(--tick)'}) 7601 modelLines(m).filter(ln => ln).forEach((ln, j) => { 7602 t.append('tspan').text(ln).at({x: x(m) + x.bandwidth()/2, dy: j ? 13 : 0}) 7603 }) 7604 }) 7605 return conds 7606} 7607 7608function drawLegend(sel, d, conds){ 7609 var leg = sel.append('div.legend') 7610 conds.forEach(cond => { 7611 var item = leg.append('div.item') 7612 item.append('span.legend-sq').st({background: cond.color}) 7613 item.append('span').text(condLabel(d, cond.key)) 7614 }) 7615 return leg 7616} 7617 7618// âââââ selfreport-soc: examples beside [score bars over lens tokens] âââââ 7619 7620function drawLensTokens(sel, d, opts={}){ 7621 var toks = d.lens_tokens 7622 var rowH = opts.rowH || 17 7623 var c = d3.conventions({ 7624 sel, 7625 width: opts.width || 320, height: toks.length * rowH, 7626 margin: {left: 96, top: 8, right: 8, bottom: 40}, 7627 }) 7628 var y = d3.scaleBand().domain(toks.map(t => t.token)).range([0, c.height]).padding(0.18) 7629 var xmax = d3.max(toks, t => t.ws) * 1.05 7630 c.x.domain([0, xmax]) 7631 c.xAxis.ticks(4) 7632 c.drawAxis() 7633 c.svg.select('.y').remove() 7634 util.ggPlot(c) 7635 toks.forEach(t => { 7636 var bh = y.bandwidth()/2 7637 c.svg.append('rect').at({x: 0, y: y(t.token), width: c.x(t.ws), height: bh, fill: util.gray[600]}) 7638 c.svg.append('rect').at({x: 0, y: y(t.token) + bh, width: c.x(t.final), height: bh, fill: util.tint(util.tol.blue, 0.65)}) 7639 c.svg.append('text').text(t.token) 7640 .at({x: -5, y: y(t.token) + y.bandwidth()/2, textAnchor: 'end', dy: '.33em'}) 7641 .st({fontSize: 'var(--fs-label)', fontFamily: 'var(--font-mono)'}) 7642 }) 7643 c.svg.append('text.axlabel').text('Fraction of slots in top-10 readout') 7644 .at({x: c.width/2, y: c.height + 32, textAnchor: 'middle'}) 7645} 7646 7647// Small html legend under a chart â [label, color] pairs. 7648function drawSwatchLegend(sel, items){ 7649 var leg = sel.append('div.legend') 7650 items.forEach(([label, color]) => { 7651 var item = leg.append('div.item') 7652 item.append('span.legend-sq').st({background: color}) 7653 item.append('span').text(label) 7654 }) 7655} 7656 7657// Bordered panel with an A/B/C letter chip â the captions reference the 7658// panels by letter. 7659function panelBox(sel, letter){ 7660 var box = sel.append('div.panelbox') 7661 if (letter) box.append('div.pletter').text(letter) 7662 return box 7663} 7664 7665window.initSelfreportSoc = async function(opts){ 7666 var d = await loadData(opts) 7667 var sel = d3.select('.selfreport-soc').html('') 7668 // Two-column: example pairs (A) fill the wide left column; the score bars 7669 // (B, with the condition legend inside its card) stack over the J-lens 7670 // contents (C) in the right column, which stretches to match A's height. 7671 var grid = sel.append('div.srgrid') 7672 var left = panelBox(grid, 'A') 7673 d.excerpts.soc.forEach(pair => drawPair(left, pair)) 7674 var right = grid.append('div.srright') 7675 var bbox = panelBox(right, 'B') 7676 var conds = drawScoreBars(bbox.append('div.panel'), d, d.bars.soc, 7677 {width: 240, height: 200}) 7678 drawLegend(bbox, d, conds) 7679 var cbox = panelBox(right, 'C') 7680 drawLensTokens(cbox.append('div.panel'), d, {width: 200, rowH: 13}) 7681 drawSwatchLegend(cbox, [ 7682 ['Ablated layers (' + d.layers + ')', util.gray[600]], 7683 ['Final layer', util.tint(util.tol.blue, 0.65)], 7684 ]) 7685 util.colorCaption(sel, {'A:': 'capchip', 'B:': 'capchip', 'C:': 'capchip'}) 7686 sel.node()?.closest('figure')?.querySelectorAll('figcaption .capchip') 7687 .forEach(n => n.textContent = n.textContent.replace(/:$/, '')) 7688} 7689 7690// âââââ selfreport-other / selfreport-questions: pairs beside bars âââââ 7691 7692function pairsBesideBars(slug, key, opts={}){ 7693 return async function(initOpts){
7694 var d = await loadData(initOpts) 7695 var sel = d3.select('.' + slug).html('') 7696 var grid = sel.append('div.srgrid') 7697 var left = grid.append('div.srleft') 7698 var pairs = opts.pickPairs ? opts.pickPairs(d.excerpts[key]) : d.excerpts[key] 7699 pairs.forEach(pair => drawPair(left, pair, opts.pairOpts)) 7700 var match = opts.barHeight === 'match' 7701 var right = grid.append('div.srright').classed('vcenter', !match) 7702 var panel = right.append('div.panel') 7703 var conds = CONDS.filter(c => d.models.some(m => d.bars[key][m]?.[c.key])) 7704 var bh = opts.barHeight || 280 7705 if (match){ 7706 // Render the legend first so its laid-out height can be subtracted 7707 // from the budget; the bars then fill the rest of the example 7708 // column's height (no whitespace between them and the legend). 7709 drawLegend(right, d, conds).st({marginTop: 0}) 7710 var leg = right.select('.legend').node() 7711 bh = Math.max(220, left.node().offsetHeight - leg.offsetHeight - 14 - 50) 7712 } 7713 drawScoreBars(panel, d, d.bars[key], 7714 {width: opts.barWidth || 250, height: bh}) 7715 if (!match) drawLegend(right, d, conds) 7716 } 7717} 7718window.initSelfreportOther = pairsBesideBars('selfreport-other', 'other') 7719// Questions: show only the second example pair (the conversations are tall 7720// enough to read on their own), drop the per-pair prompt header, and let the 7721// bars panel match the example column's height. 7722window.initSelfreportQuestions = pairsBesideBars('selfreport-questions', 'questions', 7723 {pickPairs: ps => ps.slice(1, 2), pairOpts: {noHead: true}, 7724 barWidth: 290, barHeight: 'match'}) 7725 7726// âââââ selfreport-story: example pair over [craft bars | score bars] âââââ 7727 7728window.initSelfreportStory = async function(opts){ 7729 var d = await loadData(opts) 7730 var sel = d3.select('.selfreport-story').html('') 7731 var top = sel.append('div.srleft') 7732 d.excerpts.story.slice(0, 1).forEach(pair => drawPair(top, pair)) 7733 // The story bake nests {model: {cond: {craft, score}}}; reshape per metric 7734 // so the same grouped-bar helper draws both panels. 7735 var pick = which => Object.fromEntries(d.models 7736 .filter(m => d.story[m]).map(m => [m, Object.fromEntries( 7737 Object.entries(d.story[m]).map(([cond, v]) => [cond, v[which]]))])) 7738 // Size each chart so the row of two (margins + chartrow gap) fills the 7739 // same width as the example pair above. 7740 var topW = top.node().offsetWidth 7741 var barM = 46 + 8, gap = 22 7742 var bw = Math.max(220, Math.floor((topW - gap) / 2) - barM) 7743 var row = sel.append('div.chartrow.center.wrap') 7744 drawScoreBars(row.append('div.panel'), d, pick('craft'), 7745 {width: bw, height: 280, ylabel: 'Story quality (0â10)', yMax: 10, dots: false}) 7746 var conds = drawScoreBars(row.append('div.panel'), d, pick('score'), 7747 {width: bw, height: 280, dots: false}) 7748 drawLegend(sel, d, conds) 7749} 7750 7751// âââââ selfreport-controls: matched-control battery, by judgment âââââ 7752 7753var BATTERY_KIND_COLOR = { 7754 clean: util.gray[400], 7755 ablate: util.tol.red, 7756 control: util.tint(util.tol.blue, 0.5), 7757} 7758function batteryColor(e){ 7759 if (e.kind) return BATTERY_KIND_COLOR[e.kind] || BATTERY_KIND_COLOR.control 7760 // Older bakes carry only the display label; classify from it. 7761 if (e.label == 'baseline') return BATTERY_KIND_COLOR.clean 7762 if (e.label == 'J-space ablated') return BATTERY_KIND_COLOR.ablate 7763 return BATTERY_KIND_COLOR.control 7764} 7765 7766function drawBatteryPanel(sel, entries, grader, opts={}){ 7767 var rowH = 15 7768 var c = d3.conventions({ 7769 sel, 7770 width: 190, height: entries.length * rowH, 7771 margin: {left: opts.first ? 190 : 12, top: opts.title ? 24 : 8, right: 12, bottom: 24}, 7772 }) 7773 var y = d3.scaleBand().domain(d3.range(entries.length)).range([0, c.height]).padding(0.2) 7774 c.x.domain([0, 1]) 7775 c.xAxis.ticks(3).tickFormat(d3.format('.0%')) 7776 c.drawAxis() 7777 c.svg.select('.y').remove() 7778 util.ggPlot(c) 7779 7780 entries.forEach((e, i) => { 7781 var a = e.graders[grader] 7782 if (!a) return 7783 var by = y(i), bh = y.bandwidth(), cy = by + bh/2 7784 c.svg.append('rect').at({x: 0, y: by, width: c.x(a.p), height: bh,
7785 fill: batteryColor(e)}) 7786 c.svg.append('line').at({x1: c.x(a.lo), x2: c.x(a.hi), y1: cy, y2: cy, 7787 stroke: util.gray[700], strokeWidth: 1.2}) 7788 if (opts.first){ 7789 c.svg.append('text').text(e.label) 7790 .at({x: -6, y: cy, textAnchor: 'end', dy: '.33em'}) 7791 .st({fontSize: 'var(--fs-label)', fill: 'var(--tick)'}) 7792 } 7793 }) 7794 if (opts.title) c.svg.append('text.ptitle').text(opts.title) 7795 .at({x: c.width/2, y: -10, textAnchor: 'middle'}) 7796} 7797 7798window.initSelfreportControls = async function(opts){ 7799 var d = await loadData(opts) 7800 var sel = d3.select('.selfreport-controls').html('') 7801 var graders = Object.keys(d.grader_labels) 7802 var rows = [['soc', 'Stream of consciousness'], ['questions', 'Questions about experience']] 7803 rows.forEach(([panel, rowLabel], ri) => { 7804 sel.append('div.mlabel').text(rowLabel) 7805 var row = sel.append('div.chartrow.center.wrap') 7806 graders.forEach((g, i) => { 7807 drawBatteryPanel(row.append('div.panel'), d.battery[panel], g, 7808 {title: ri == 0 ? d.grader_labels[g] : null, first: i == 0}) 7809 }) 7810 }) 7811} 7812 7813// âââââ selfreport-dose: per-judgment dose-response line panels âââââ 7814 7815function drawDosePanel(sel, series, points, xKey, xLabel, opts={}){ 7816 var c = d3.conventions({ 7817 sel, 7818 width: 250, height: 190, 7819 margin: {left: opts.first ? 54 : 40, top: 24, right: 12, bottom: 42}, 7820 }) 7821 var x = xKey == 'k' 7822 ? d3.scaleLog().domain([1, 40]).range([0, c.width]) 7823 : d3.scalePoint().domain(points.map(p => p.band)).range([0, c.width]).padding(0.4) 7824 c.x = x 7825 c.y.domain([0, 1]) 7826 c.yAxis = d3.axisLeft(c.y).ticks(5).tickFormat(d3.format('.0%')) 7827 c.xAxis = xKey == 'k' 7828 ? d3.axisBottom(x).tickValues([1, 3, 5, 10, 20, 40]).tickFormat(d3.format('d')) 7829 : d3.axisBottom(x).tickFormat(s => s.replace('L', '')) 7830 c.drawAxis() 7831 util.ggPlot(c) 7832 7833 // Clean baseline (no ablation) as a dashed reference line, labeled directly 7834 // in the first panel of each row. 7835 c.svg.append('line').at({ 7836 x1: 0, x2: c.width, y1: c.y(series.baseline), y2: c.y(series.baseline), 7837 stroke: util.gray[500], strokeWidth: 1, strokeDasharray: '4,3', 7838 }) 7839 if (opts.first) util.directLabel(c.svg, { 7840 x: 4, y: c.y(series.baseline), dy: series.baseline > 0.5 ? 12 : -6, 7841 text: 'No-ablation baseline', color: util.gray[600], 7842 }) 7843 7844 // Single series â black line, gray whiskers. 7845 var xv = p => xKey == 'k' ? x(p.k) : x(p.band) 7846 points.forEach(p => { 7847 c.svg.append('line').at({x1: xv(p), x2: xv(p), y1: c.y(p.lo), y2: c.y(p.hi), 7848 stroke: util.gray[700], strokeWidth: 1.2}) 7849 }) 7850 c.svg.append('path').at({ 7851 d: d3.line().x(xv).y(p => c.y(p.p))(points), 7852 stroke: util.gray[900], fill: 'none', strokeWidth: 1.6, 7853 }) 7854 points.forEach(p => { 7855 c.svg.append('circle').at({cx: xv(p), cy: c.y(p.p), r: 2.5, fill: util.gray[900]}) 7856 }) 7857 7858 if (opts.title) c.svg.append('text.ptitle').text(opts.title).at({x: c.width/2, y: -10, textAnchor: 'middle'}) 7859 if (opts.first) c.svg.append('text.axlabel').text('Fraction of responses scored positive') 7860 .at({transform: 'rotate(-90)', x: -c.height/2, y: -40, textAnchor: 'middle'}) 7861 c.svg.append('text.axlabel').text(xLabel).at({x: c.width/2, y: c.height + 34, textAnchor: 'middle'}) 7862} 7863 7864window.initSelfreportDose = async function(opts){ 7865 var d = await loadData(opts) 7866 var sel = d3.select('.selfreport-dose').html('') 7867 var graders = Object.keys(d.dose) 7868 ;[['vs_k', 'Directions ablated (k)', 'k'], ['vs_layers', 'Ablated layer band', 'band']].forEach(([key, xlabel, xKey]) => { 7869 var row = sel.append('div.chartrow.center.wrap') 7870 graders.forEach((g, i) => { 7871 drawDosePanel(row.append('div.panel'), d.dose[g], d.dose[g][key], xKey, xlabel, 7872 {first: i == 0, title: d.dose[g].label}) 7873 }) 7874 }) 7875} 7876 7877}() 7878window.init?.() 7879 7880// âââ public/slice-inline/init-slice-inline.js âââ 7881window.initSliceInline = async function(opts){ 7882 opts = opts || {} 7883 var sel = d3.select(opts.sel || '.slice-inline').html('') 7884 var base = (opts.datapath || './').replace(/\/?$/, '/') 7885 var ttSel = d3.select('.tooltip') 7886 if (!ttSel.size()) ttSel = d3.select('body').append('div.tooltip.tooltip-hidden') 7887 7888 var K = 10 7889 7890 // Per-slug presentation: title + caption only. The card itself is a condensed 7891 // version of the lens-slice viewer's three core panels (top-1 grid, by-layer 7892 // readout, by-position readout) for a single prompt. 7893 var perSlug = { 7894 count_five: { 7895 title: 'Counting ahead', 7896 cap: 'Asked to count to five, the lens at the final prompt positions already reads out "One" â the first token of the upcoming count â before the assistant has produced anything. Hover any cell of the top-1 grid for the full readout.', 7897 }, 7898 introspection: { 7899 title: 'Introspection', 7900 cap: 'Asked to silently think of a sport before naming it, the lens at the final prompt positions already reads "Basketball" â the sport the model goes on to answer â while mid layers read "okay" and "thinking". Hover any cell of the top-1 grid for the full readout.', 7901 }, 7902 calc_ooo: { 7903 title: 'Mental arithmetic', 7904 cap: 'Before any answer token is produced, the intermediate value "42" â (4+17)Ã2 â rises to the top of the lens readout at the final prompt positions in late layers. Hover any cell of the top-1 grid for the full readout.', 7905 }, 7906 mars: { 7907 title: 'Multihop recall', 7908 cap: '"Mars" is never written in the prompt, but from the middle layers on the lens reads it over the final tokens â an intermediate resolved before the answer "red". Hover any cell of the top-1 grid for the full readout.', 7909 }, 7910 ascii_face: { 7911 title: 'ASCII face', 7912 cap: 'The lens reads "Face" over the drawing well before the assistant answers. Hover any cell of the top-1 grid for the full readout.', 7913 }, 7914 mutate_iter: { 7915 title: 'Bug in code', 7916 cap: '"Error" surfaces in the lens at the line that mutates the dict mid-iteration. Hover any cell of the top-1 grid for the full readout.', 7917 }, 7918 avgfp_raw: { 7919 title: 'Protein recognition', 7920 cap: 'A few residues into the sequence the lens reads "fluor" â the model has recognised green fluorescent protein. Hover any cell of the top-1 grid for the full readout.', 7921 }, 7922 } 7923 7924 var slugs = opts.slugs || ['count_five', 'introspection'] 7925 if (typeof slugs == 'string') slugs = slugs.split(',').map(s =>
7925 s.trim()).filter(Boolean) 7926 7927 var cards = sel.append('div.cards') 7928 for (var slug of slugs) await renderCard(cards.append('div.scard'), slug) 7929 7930 function renderTodo(card, slug, cfg){ 7931 var box = card.append('div.todo-card') 7932 box.append('div.todo-tag').text('TODO â no baked data') 7933 box.append('div.todo-note').text(cfg.todo || `No ${slug} payload found under ${base}`) 7934 } 7935 7936 // condensed card: prompt up top, then the slice viewer's three columns in a 7937 // row â top-1 (argmax) grid, by-layer readout, by-position readout. Hover the 7938 // grid for the full top-k; click anywhere to move the selected cell. 7939 async function renderCard(card, slug){ 7940 var cfg = perSlug[slug] || {} 7941 card.append('div.mlabel.card-title').text(cfg.title || slug) 7942 7943 var d = null 7944 try { d = await util.loadLensSlice(base, slug) } catch (e){ console.warn('slice-inline', slug, e.message) } 7945 if (!d) return renderTodo(card, slug, cfg) 7946 7947 var ctx = d3.clamp(0, d.default_ctx ?? d.nCtx - 1, d.nCtx - 1) 7948 var layer = d.layers[Math.floor(d.layers.length * 2/3)] 7949 7950 // display rank 1 = argmax regardless of whether the payload is 0- or 1-based 7951 var rankBase = d.cell(ctx, d.layers[0])[0]?.rank ?? 0 7952 var dispRank = r => r + 1 - rankBase 7953 7954 var promptToks = util.appendPromptPara(card.append('div.prompt-para'), d.ctxTokens) 7955 .on('click', (e, i) => setCtx(i)) 7956 7957 var row = card.append('div.srow') 7958 var col1 = row.append('div.scol.scol-grid') 7959 var col2 = row.append('div.scol.scol-layer') 7960 var col3 = row.append('div.scol.scol-pos') 7961 7962 if (cfg.cap) util.panelCaption(card, opts, cfg.cap).classed('cap', true) 7963 7964 var gridCells, ctxTokCells 7965 7966 function setCtx(c){ if (c != null && c !== ctx){ ctx = c; buildCol2(); updateSel() } } 7967 function setLayer(L){ if (L != null && L !== layer){ layer = L; buildCol3(); updateSel() } } 7968 7969 // âââââ col1: top-1 (argmax) grid, layer à pos âââââ 7970 function buildGrid(){ 7971 col1.append('div.col-title.mlabel').text('Top-1 readout · layer à pos') 7972 var table = col1.append('div.grid-wrap').append('table.argmax') 7973 var rows = table.append('tbody').appendMany('tr', [...d.layers].reverse()) 7974 rows.append('td.layer-label').text(util.layerLabel) 7975 rows.each(function(L){ 7976 d3.select(this).appendMany('td.cell', d3.range(d.nCtx).map(c => ({L, c}))) 7977 .text(p => util.ppCell(d.argmax(p.c, p.L))) 7978 .on('click', (e, p) => { setCtx(p.c); setLayer(p.L) }) 7979 .on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip) 7980 }) 7981 var ftok = table.append('tfoot').append('tr') 7982 ftok.append('td') 7983 ctxTokCells = ftok.appendMany('td.ctx-tok', d3.range(d.nCtx)) 7984 .text(c => util.ppCell(d.ctxTokens[c])).at({title: c => util.ppCell(d.ctxTokens[c])}) 7985 .on('click', (e, c) => setCtx(c)) 7986 gridCells = table.selectAll('td.cell') 7987 } 7988 7989 // âââââ col2: by-layer readout at the selected position âââââ 7990 function buildCol2(){ 7991 col2.html('') 7992 col2.append('div.col-title.mlabel').text(`By layer · pos ${ctx} ${util.ppCell(d.ctxTokens[ctx])}`) 7993 var rows = col2.append('div.rows-wrap').appendMany('div.lyr-row', [...d.layers].reverse()) 7994 .on('click', (e, L) => setLayer(L)) 7995 rows.append('span.row-label').text(util.layerLabel) 7996 rows.each(function(L){ 7997 util.appendLensTopk(d3.select(this).append('span.lens-topk'), d.cell(ctx, L), K) 7998 }) 7999 } 8000 8001 // âââââ col3: by-position readout at the selected layer âââââ 8002 function buildCol3(){ 8003 col3.html('') 8004 col3.st({'--label-ch': String(d.nCtx - 1).length + 'ch'}) 8005 col3.append('div.col-title.mlabel').text(`By pos · L${util.layerLabel(layer)}`) 8006 var rows = col3.append('div.rows-wrap').appendMany('div.pos-row', d3.range(d.nCtx)) 8007 .on('click', (e, c) => setCtx(c)) 8008 rows.append('span.row-label').text(c => c) 8009 rows.append('span.row-ctx').text(c => util.ppCell(d.ctxTokens[c])).at({title: c => util.ppCell(d.ctxTokens[c])}) 8010 rows.each(function(c){ 8011 util.appendLensTopk(d3.select(this).append('span.lens-topk'), d.cell(c, layer), K) 8012 }) 8013 } 8014 8015 // scroll only inside the card's own containers, never the page 8016 function updateSel(){ 8017 gridCells.classed('sel', p => p.L === layer && p.c === ctx) 8018 ctxTokCells.classed('sel', c => c === ctx) 8019 promptToks.classed('sel', i => i === ctx) 8020 col2.selectAll('.lyr-row').classed('sel', L => L === layer) 8021 col3.selectAll('.pos-row').classed('sel', c => c === ctx) 8022 var wrap = col1.select('.grid-wrap').node(), cell = ctxTokCells.nodes()[ctx] 8023 if (wrap && cell) wrap.scrollLeft = cell.offsetLeft - wrap.clientWidth / 2 8024 var rw = col3.select('.rows-wrap').node(), prow = rw?.children[ctx] 8025 if (prow) rw.scrollTop = ctx * prow.offsetHeight - rw.clientHeight / 2 8026 } 8027 8028 function showTip(ev, p){ 8029 moveTip(ev)
8030 ttSel.classed('tooltip-hidden', 0).html('').classed('si-tip', 1) 8031 ttSel.append('div.tt-hdr').text(`L${util.layerLabel(p.L)} · pos ${p.c} ${util.ppCell(d.ctxTokens[p.c])}`) 8032 var tbl = ttSel.append('table') 8033 d.cell(p.c, p.L).forEach(r => { 8034 var tr = tbl.append('tr') 8035 tr.append('td.r').text(util.ppRank(dispRank(r.rank))) 8036 tr.append('td.t').text(util.ppCell(r.str)) 8037 tr.append('td.p').text(Math.round(r.prob * 100) + '%') 8038 }) 8039 } 8040 function moveTip(ev){ 8041 var bb = ttSel.node().getBoundingClientRect() 8042 var left = Math.max(4, Math.min(ev.clientX + 12, innerWidth - bb.width - 4)) 8043 var top = Math.max(4, ev.clientY - bb.height - 8) 8044 ttSel.st({left, top}) 8045 } 8046 function hideTip(){ ttSel.classed('tooltip-hidden', 1).classed('si-tip', 0) } 8047 8048 buildGrid(); buildCol2(); buildCol3(); updateSel() 8049 } 8050} 8051 8052window.init?.() 8053 8054// âââ public/slice-stack/init-slice-stack.js âââ 8055window.initSliceStack = async function(opts){ 8056 var sel = d3.select(opts?.sel || '.slice-stack').html('') 8057 var base = (opts?.datapath || './').replace(/\/?$/, '/') 8058 var ttSel = d3.select('.tooltip') 8059 if (!ttSel.size()) ttSel = d3.select('body').append('div.tooltip.tooltip-hidden') 8060 8061 var K = 10 8062 var PALETTE = (util.lineColors || d3.schemeCategory10).filter(c => c.length === 7) 8063 8064 // Default-pinned tokens per card â the "interesting tokens" called out in the 8065 // article's slice/lens captions (exact data strings incl. â½/â markers). 8066 // Filtered against rankFiles at use time so a re-bake can't break a card. 8067 var DEFAULT_PINS = { 8068 mutate_iter: ['Errorâ½', 'â½TypeErrorâ½', 'â½doubledâ½', 'â½dictâ½'], 8069 ascii_face: ['â½faceâ½', 'â½smileâ½', 'â½eyesâ½', 'â½emojiâ½'], 8070 avgfp_raw: ['â½fluor', 'â½proteinâ½', 'â½colorsâ½', 'â½green'], 8071 mars: ['ââ½marsâ½', 'â½redâ½', 'â½colorâ½', 'â½planetâ½'], 8072 calc_ooo: ['21', '42', '49', 'â½answerâ½'], 8073 count_five: ['ââ½oneâ½', 'â½countingâ½', 'ââ½sureâ½', 'â½fiveâ½'], 8074 count_introspect: ['ââ½claudeâ½', 'ââ½doneâ½', 'â½consciousnessâ½', 'â½thoughtsâ½', 'â½halfwayâ½', 'â½AIâ½', 'â½countingâ½'], 8075 introspection: ['â½thinkingâ½', 'â½okayâ½', 'ââ½basketballâ½', 'â½sportâ½'], 8076 } 8077 8078 var manifest = await util.getFile(base + 'manifest.json') 8079 var slugs = manifest.slugs || [] 8080 if (opts?.slugs){ 8081 var want = (typeof opts.slugs == 'string' ? opts.slugs.split(',') : opts.slugs).map(s => s.trim()) 8082 slugs = want.map(w => slugs.find(s => s.slug === w) || {slug: w, title: w}) 8083 } 8084 var slices = await Promise.all(slugs.map(s => util.loadLensSlice(base, s.slug))) 8085 8086 var state = opts?.state || {} 8087 state.cards ??= {} 8088 8089 slices.forEach((d, i) => renderCard(sel.append('div.scard').at({id: 'slice-' + d.slug}), d, slugs[i])) 8090 8091 // links from the article embeds land here with ?slug= â scroll that card into view 8092 // (deliberate, post-render; per-card init never scrolls the page) 8093 var focus = opts?.focusSlug 8094 if (focus) sel.select('#slice-' + focus).node()?.scrollIntoView() 8095 8096 // ctx indices hidden in the prompt display: only the leading blank tokens 8097 // before "Human:" â the Human:/Assistant: role markers (and the blank line 8098 // before "Assistant:") stay visible 8099 function wrapperHidden(toks){ 8100 var hide = new Set() 8101 for (var i = 0; i < toks.length && !toks[i].trim(); i++) hide.add(i) 8102 return hide 8103 } 8104 8105 // every interaction below closes over this card's own cs/d/cols, so 8106 // selection and pins in one card never touch another 8107 function renderCard(card, d, meta){ 8108 var cs = state.cards[d.slug] ??= {ctx: null, layer: null, pinned: null, hoverStr: null} 8109 cs.pinned ??= new Map() 8110 cs.ctx = d3.clamp(0, cs.ctx ?? d.default_ctx ?? d.nCtx - 1, d.nCtx - 1) 8111 cs.layer = d.layers.includes(cs.layer) ? cs.layer : d.layers[Math.floor(d.layers.length * 2/3)] 8112 if (!cs.pinned.size){ 8113 var defs = d.default_pinned?.length ? d.default_pinned 8114 : (DEFAULT_PINS[d.slug] || []).filter(s => d.rankFiles?.[s] != null) 8115 defs.forEach((s, i) => cs.pinned.set(s, PALETTE[i % PALETTE.length])) 8116 } 8117 // last resort: pin the late-layer argmax at the default position so the 8118 // rank heatmap + charts have content before any interaction 8119 if (!cs.pinned.size){ 8120 var auto = d.argmax(cs.ctx, d.layers.at(-1)) 8121 if (auto != null) cs.pinned.set(auto, PALETTE[0]) 8122 } 8123 8124 // âââââ header: title top-left, then prompt (snug, ~1/3) + pinned chips/help âââââ 8125 if (slugs.length > 1) card.append('div.stitle').text(meta?.title || d.title || d.slug) 8126 var head = card.append('div.shead') 8127 var promptToks = util.appendPromptPara(head.append('div.sprompt'), d.ctxTokens) 8128 .on('click', (e, i) => setCtx(i)) 8129 shiftScrub(promptToks, (e, i) => setCtx(i)) 8130 var hideTok = wrapperHidden(d.ctxTokens) 8131 promptToks.classed('wrap-hidden', i => hideTok.has(i)) 8132 var side = head.append('div.sside') 8133 side.append('div.pinned-row') 8134 side.append('div.shelp').text('click tokens to pin · shift+hover to scrub') 8135 8136 var row = card.append('div.srow') 8137 var col1 = row.append('div.scol.scol-grid') 8138 var col2 = row.append('div.scol.scol-layer') 8139 var col3 = row.append('div.scol.scol-pos') 8140 8141 // line charts share the heatmap's plotting height so the columns align 8142 var chartH = d.layers.length * 11 + 32 8143 card.st({'--chart-h': chartH + 'px'}) 8144 8145 var gridCells, ctxTokCells, hmC, rcLens, hmPill 8146 8147 // normalize rank base so display rank 1 = argmax regardless of whether the 8148 // payload writes the top row as rank 0 or rank 1 (rankOf assumes 0-based) 8149 var rankBase = d.cell(cs.ctx, d.layers[0])[0]?.rank ?? 0 8150 var dispRank = r => r + 1 - rankBase 8151 var rankAt = (ctx, L, str) => { 8152 var v = d.rankOf(ctx, L, str) 8153 return v == null ? null : Math.max(1, v - rankBase) 8154 } 8155 8156 function setCtx(c){ if (c != null && c !== cs.ctx){ cs.ctx = c; render() } } 8157 function setLayer(L){ if (L != null && L !== cs.layer){ cs.layer = L; render() } } 8158 function shiftScrub(s, fn){ return s.on('mouseenter.scrub', (e, x) => { if (e.shiftKey) fn(e, x) }) } 8159 8160 function togglePin(str){ 8161 if (str == null) return 8162 if (cs.pinned.has(str)) cs.pinned.delete(str) 8163 else { 8164 var used = new Set(cs.pinned.values()) 8165 cs.pinned.set(str, PALETTE.find(c => !used.has(c)) || PALETTE[cs.pinned.size % PALETTE.length]) 8166 cs.hoverStr = str 8167 d.loadRanks(str).then(renderPinned, () => {}) 8168 } 8169 renderPinned() 8170 } 8171 function bindTopk(chips){ chips.on('click', (e, it) => { e.stopPropagation(); togglePin(it?.str) }) } 8172 8173 // âââââ col1: argmax grid + pinned chips + rank heatmap âââââ 8174 function buildGrid(){ 8175 col1.append('div.col-title.mlabel').text('argmax · layer à pos') 8176 var table = col1.append('div.grid-wrap').append('table.argmax') 8177 var rows = table.append('tbody').appendMany('tr', [...d.layers].reverse()) 8178 rows.append('td.layer-label').text(util.layerLabel) 8179 rows.each(function(L){ 8180 var cells = d3.select(this).appendMany('td.cell', d3.range(d.nCtx).map(c => ({L, c}))) 8181 .text(p => util.ppCell(d.argmax(p.c, p.L))) 8182 .on('click', (e, p) => { setCtx(p.c); setLayer(p.L); togglePin(d.argmax(p.c, p.L)) }) 8183 .on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip) 8184 shiftScrub(cells, (e, p) => { setCtx(p.c); setLayer(p.L) }) 8185 }) 8186 var ftok = table.append('tfoot').append('tr') 8187 ftok.append('td') 8188 ctxTokCells = ftok.appendMany('td.ctx-tok', d3.range(d.nCtx)) 8189 .text(c => util.ppCell(d.ctxTokens[c])).at({title: c => util.ppCell(d.ctxTokens[c])}) 8190 .on('click', (e, c) => setCtx(c)) 8191 shiftScrub(ctxTokCells, (e, c) => setCtx(c)) 8192 gridCells = table.selectAll('td.cell') 8193 8194 if (d.rankFiles){ 8195 var hm = col1.append('div.grid-bottom').append('div.rank-
8195heatmap') 8196 // single-line legend, flush with the heatmap's right edge: 8197 // [active-token pill] pinned token rank 1 ââââ 1k+ 8198 rcLens = d3.scaleSequential(d3.interpolateViridis).domain([3, 0]) // log10 rank, capped at 1k 8199 var hmHead = hm.append('div.hm-head') 8200 hmPill = hmHead.append('span.pinned-chip.hm-pill') 8201 hmHead.append('span.hm-lab').text('pinned token rank') 8202 hmHead.append('span.hm-end').text('1') 8203 hmHead.append('span.hm-grad').st({background: 'linear-gradient(to right, ' 8204 + d3.range(11).map(i => rcLens(i / 10 * 3) + ' ' + i * 10 + '%').join(',') + ')'}) 8205 hmHead.append('span.hm-end').text('1k+') 8206 hm.append('div.hm-chart') 8207 } 8208 } 8209 8210 function buildPinnedRow(){ 8211 var pr = card.select('.pinned-row').html('') 8212 var chips = pr.appendMany('span.pinned-chip', [...cs.pinned]) 8213 .st({borderColor: ([,c]) => c}) 8214 .classed('active', ([s]) => s === cs.hoverStr) 8215 .on('mouseenter', (e, [s]) => { cs.hoverStr = s; renderHover() }) 8216 chips.append('span.tok').text(([s]) => util.ppCell(s)) 8217 chips.append('span.unpin').text('Ã').on('click', (e, [s]) => { e.stopPropagation(); togglePin(s) }) 8218 } 8219 8220 function drawRankHeatmap(){ 8221 if (!d.rankFiles) return 8222 var active = cs.hoverStr && cs.pinned.has(cs.hoverStr) ? cs.hoverStr : [...cs.pinned.keys()][0] 8223 // one row per layer present in the payload; taller rows so each layer 8224 // reads as a solid band 8225 var nL = d.layers.length 8226 if (!hmC){ 8227 hmC = d3.conventions({sel: col1.select('.hm-chart').html('').append('div'), height: nL * 11, margin: {top: 4, right: 8, bottom: 20, left: 28}, layers: 'scs'}) 8228 hmC.x.domain([0, d.nCtx - 1]) 8229 hmC.y.domain([-0.5, nL - 0.5]) // index space: row i centered at y(i) 8230 hmC.xAxis.ticks(5) 8231 // round-number layer ticks (5, 10, 15, 20â¦) placed at their row position 8232 var idxOf = L => (L - d.layers[0]) / (d.layers.at(-1) - d.layers[0]) * (nL - 1) 8233 var tickLayers = d3.ticks(d.layers[0], d.layers.at(-1), 4) 8234 hmC.yAxis = d3.axisLeft(hmC.y).tickValues(tickLayers.map(idxOf)).tickFormat((v, i) => util.layerLabel(tickLayers[i])) 8235 hmC.drawAxis(); util.addAxisLabel(hmC, 'Pos â', 'Layer (reindexed) â') 8236 hmC.dot = hmC.layers[2].append('circle.sel-dot').at({r: 3.5, fill: '#fff', stroke: '#000', strokeWidth: 1.5}) 8237 var goTo = e => { 8238 var [px, py] = d3.pointer(e) 8239 setCtx(d3.clamp(0, Math.round(hmC.x.invert(px)), d.nCtx - 1)) 8240 setLayer(d.layers[d3.clamp(0, Math.round(hmC.y.invert(py)), nL - 1)]) 8241 } 8242 hmC.layers[2].append('rect').at({width: hmC.width, height: hmC.height, fill: 'transparent', cursor: 'pointer'}) 8243 .on('click', goTo).on('mousemove', e => { if (e.shiftKey) goTo(e) }) 8244 hmC.dot.raise() 8245 } 8246 var ctx = hmC.layers[1] 8247 var cw = hmC.width / d.nCtx, ch = hmC.height / nL 8248 ctx.fillStyle = '#f5f5f5'; ctx.fillRect(0, 0, hmC.width, hmC.height) 8249 if (active) d.layers.forEach((L, li) => { 8250 for (var i = 0; i < d.nCtx; i++){ 8251 var r = rankAt(i, L, active) 8252 if (!r) continue 8253 ctx.fillStyle = rcLens(Math.log10(r)) 8254 ctx.fillRect(i*cw, hmC.height - (li+1)*ch, cw + .5, ch + .5) 8255 } 8256 }) 8257 hmPill.text(active ? util.ppCell(active) : 'â') 8258 .st({borderColor: active ? cs.pinned.get(active) : 'var(--rule)'}) 8259 updateHeatmapDot() 8260 } 8261 function updateHeatmapDot(){ 8262 if (!hmC) return 8263 var cw = hmC.width / d.nCtx, ch = hmC.height / d.layers.length 8264 var li = d.layers.indexOf(cs.layer) 8265 hmC.dot.at({cx: (cs.ctx + 0.5) * cw, cy: hmC.height - (li + 0.5) * ch}) 8266 } 8267 8268 // âââââ col2/col3 are built once; render() repopulates the lens-topk spans 8269 // and reclasses .sel so row containers never get .html('')-ed (which 8270 // was the source of stale highlights and perceived scroll-jumps) âââââ 8271 var col2Title, col2Rows, col3Title, col3Rows, lastCtx = null, lastLayer = null 8272 8273 function buildCol2(){ 8274 col2Title = col2.append('div.col-title.mlabel') 8275 col2Rows = col2.append('div.rows-wrap').appendMany('div.lyr-row', [...d.layers].reverse()) 8276 .on('click', (e, L) => setLayer(L)) 8277 shiftScrub(col2Rows, (e, L) => setLayer(L)) 8278 col2Rows.append('span.row-label').text(util.layerLabel) 8279 col2Rows.append('span.lens-topk') 8280 col2.append('div.rank-chart') 8281 } 8282 8283 function buildCol3(){ 8284 col3.st({'--label-ch': String(d.nCtx - 1).length + 'ch', '--vis-rows': d.layers.length}) 8285 col3Title = col3.append('div.col-title.mlabel') 8286 col3Rows = col3.append('div.rows-wrap').appendMany('div.pos-row', d3.range(d.nCtx)) 8287 .on('click', (e, c) => setCtx(c)) 8288 shiftScrub(col3Rows, (e, c) => setCtx(c)) 8289 col3Rows.append('span.row-label').text(c => c) 8290 col3Rows.append('span.row-ctx').text(c => util.ppCell(d.ctxTokens[c])).at({title: c => util.ppCell(d.ctxTokens[c])}) 8291 col3Rows.append('span.lens-topk') 8292 col3.append('div.rank-chart') 8293 } 8294 8295 function drawRankChart(colSel, xs, xLabel, getRank, scrub, withLine, xFmt){ 8296 var rc = colSel.select('.rank-chart').html('') 8297 var c = d3.conventions({sel: rc.append('div'), height: chartH - 32, margin: {top: 8, right: 8, bottom: 20, left: 28}, layers: 'scs'}) 8298 c.x.domain(d3.extent(xs))
8299 c.y = d3.scaleLog().domain([1000, 1]).range([c.height, 0]).clamp(true) 8300 c.xAxis.ticks(5); if (xFmt) c.xAxis.tickFormat(xFmt) 8301 c.yAxis = d3.axisLeft(c.y).tickValues([1, 10, 100, 1000]).tickFormat(v => v >= 1000 ? '1k+' : v) 8302 c.drawAxis(); util.ggPlot(c) 8303 util.addAxisLabel(c, xLabel, 'Rank â') 8304 var ctx = c.layers[1] 8305 for (var [str, color] of cs.pinned){ 8306 ctx.strokeStyle = ctx.fillStyle = color 8307 if (withLine){ 8308 ctx.lineWidth = 1.5; ctx.beginPath() 8309 var prev = null 8310 for (var x of xs){ 8311 var r = getRank(x, str) 8312 if (r == null){ prev = null; continue } 8313 prev ? ctx.lineTo(c.x(x), c.y(r)) : ctx.moveTo(c.x(x), c.y(r)) 8314 prev = 1 8315 } 8316 ctx.stroke() 8317 } 8318 for (var x of xs){ 8319 var r = getRank(x, str) 8320 if (r == null) continue 8321 ctx.beginPath(); ctx.arc(c.x(x), c.y(r), 1.5, 0, Math.PI*2); ctx.fill() 8322 } 8323 } 8324 var top = c.layers[2] 8325 var scrubX = top.append('line.scrub').at({y1: 0, y2: c.height, stroke: '#000', strokeWidth: .5, strokeDasharray: '2 2'}) 8326 rc.node().__updateScrub = v => scrubX.at({x1: c.x(v), x2: c.x(v)}) 8327 rc.node().__updateScrub(scrub()) 8328 var goTo = e => scrub(d3.clamp(xs[0], Math.round(c.x.invert(d3.pointer(e)[0])), xs.at(-1))) 8329 top.append('rect').at({width: c.width, height: c.height, fill: 'transparent', cursor: 'pointer'}) 8330 .on('click', goTo).on('mousemove', e => { if (e.shiftKey) goTo(e) }) 8331 } 8332 function nearestLayer(v){ return d.layers.reduce((a, b) => Math.abs(b - v) < Math.abs(a - v) ? b : a) } 8333 function drawCol2Chart(){ drawRankChart(col2, d.layers, 'Layer (reindexed) â', (L,s) => rankAt(cs.ctx, L, s), v => { if (v != null) setLayer(nearestLayer(v)); return cs.layer }, true, util.layerLabel) } 8334 // contexts are short, so the rank-vs-pos chart reads fine as a line (matches rank-vs-layer) 8335 function drawCol3Chart(){ drawRankChart(col3, d3.range(d.nCtx), 'Pos â', (c,s) => rankAt(c, cs.layer, s), v => { if (v != null) setCtx(v); return cs.ctx }, true) } 8336 8337 function restylePinned(scope){ 8338 scope.selectAll('.lens-topk .lenstk').each(function(){ 8339 var it = d3.select(this).datum() 8340 d3.select(this).at({style: util.pinnedStyle(it && cs.pinned.get(it.str))}) 8341 }) 8342 } 8343 8344 // single render of everything that depends on {ctx, layer}. Row content is 8345 // only repopulated when the relevant axis actually changed; selection 8346 // classes, prompt highlight, heatmap dot and scrub lines are always 8347 // refreshed. Container scroll only ever touches the card's own scroll 8348 // containers â never the page. 8349 function render(){ 8350 if (cs.ctx !== lastCtx){ 8351 col2Title.text(`By layer · pos ${cs.ctx} ${util.ppCell(d.ctxTokens[cs.ctx])}`) 8352 col2Rows.each(function(L){ 8353 bindTopk(util.appendLensTopk(d3.select(this).select('.lens-topk').html(''), d.cell(cs.ctx, L), K)) 8354 }) 8355 drawCol2Chart(); restylePinned(col2) 8356 } 8357 if (cs.layer !== lastLayer){ 8358 col3Title.text(`By pos · L${util.layerLabel(cs.layer)}`) 8359 col3Rows.each(function(c){ 8360 bindTopk(util.appendLensTopk(d3.select(this).select('.lens-topk').html(''), d.cell(c, cs.layer), K)) 8361 }) 8362 drawCol3Chart(); restylePinned(col3) 8363 } 8364 col2Rows.classed('sel', L => L === cs.layer) 8365 col3Rows.classed('sel', c => c === cs.ctx) 8366 gridCells.classed('sel', p => p.L === cs.layer && p.c === cs.ctx) 8367 ctxTokCells.classed('sel', c => c === cs.ctx) 8368 promptToks.classed('tok-hit', i => i === cs.ctx) 8369 updateHeatmapDot() 8370 col2.select('.rank-chart').node()?.__updateScrub?.(cs.layer) 8371 col3.select('.rank-chart').node()?.__updateScrub?.(cs.ctx) 8372 // bring the selected cell/row into view only if it's outside the 8373 // container's visible range â shift-hover scrubs an already-visible row 8374 // so this is a no-op there, and the page itself never scrolls 8375 var wrap = col1.select('.grid-wrap').node(), cell = ctxTokCells.nodes()[cs.ctx] 8376 if (wrap && cell){ 8377 var cl = cell.offsetLeft, cr = cl + cell.offsetWidth 8378 if (cl < wrap.scrollLeft) wrap.scrollLeft = cl 8379 else if (cr > wrap.scrollLeft + wrap.clientWidth) wrap.scrollLeft = cr - wrap.clientWidth 8380 } 8381 var rw = col3.select('.rows-wrap').node(), prow = rw?.children[cs.ctx] 8382 if (rw && prow){ 8383 var rt = prow.offsetTop, rb = rt + prow.offsetHeight 8384 if (rt < rw.scrollTop) rw.scrollTop = rt 8385 else if (rb > rw.scrollTop + rw.clientHeight) rw.scrollTop = rb - rw.clientHeight 8386 } 8387 lastCtx = cs.ctx; lastLayer = cs.layer 8388 } 8389 function renderPinned(){ 8390 buildPinnedRow() 8391 restylePinned(card) 8392 gridCells.at({style: p => util.pinnedStyle(cs.pinned.get(d.argmax(p.c, p.L)))}) 8393 drawRankHeatmap(); drawCol2Chart(); drawCol3Chart() 8394 } 8395 function renderHover(){ 8396 card.select('.pinned-row').selectAll('.pinned-chip').classed('active', ([s]) => s === cs.hoverStr) 8397 drawRankHeatmap() 8398 } 8399 8400 function showTip(ev, p){ 8401 moveTip(ev)
8402 ttSel.classed('tooltip-hidden', 0).html('').classed('ss-tip', 1) 8403 ttSel.append('div.tt-hdr').text(`L${util.layerLabel(p.L)} · pos ${p.c} ${util.ppCell(d.ctxTokens[p.c])}`) 8404 var tbl = ttSel.append('table') 8405 d.cell(p.c, p.L).forEach(r => { 8406 var tr = tbl.append('tr').at({style: util.pinnedStyle(cs.pinned.get(r.str))}) 8407 tr.append('td.r').text(util.ppRank(dispRank(r.rank))) 8408 tr.append('td.t').text(util.ppCell(r.str)) 8409 tr.append('td.p').text(Math.round(r.prob * 100) + '%') 8410 }) 8411 } 8412 function moveTip(ev){ 8413 var bb = ttSel.node().getBoundingClientRect() 8414 var left = Math.max(4, Math.min(ev.clientX + 12, innerWidth - bb.width - 4)) 8415 var top = Math.max(4, ev.clientY - bb.height - 8) 8416 ttSel.st({left, top}) 8417 } 8418 function hideTip(){ ttSel.classed('tooltip-hidden', 1).classed('ss-tip', 0) } 8419 8420 buildGrid() 8421 buildCol2(); buildCol3() 8422 renderPinned() 8423 render() 8424 if (cs.pinned.size) Promise.all([...cs.pinned.keys()].map(s => d.loadRanks(s))).then(renderPinned, () => {}) 8425 8426 // re-render charts/heatmap at the new column width on resize 8427 var rw0 = row.node().offsetWidth 8428 new ResizeObserver(util.throttleDebounce(() => { 8429 var w = row.node().offsetWidth 8430 if (w === rw0) return 8431 rw0 = w; hmC = null; lastCtx = lastLayer = null 8432 drawRankHeatmap(); render() 8433 }, 150)).observe(row.node()) 8434 } 8435} 8436 8437window.init?.() 8438 8439// âââ public/metacog-alarm/init-metacog-alarm.js âââ 8440!function(){ 8441 8442var COLORS = { 8443 base: {think: util.tint(util.gray[600], 0.5), dont_think: util.gray[600]}, 8444 prod: {think: util.tint(util.tol.blue, 0.5), dont_think: util.tol.blue}, 8445} 8446var cap = s => s[0].toUpperCase() + s.slice(1) 8447 8448function drawExample(sel, d){ 8449 var ex = d.example 8450 var card = sel.append('div.ex-col') 8451 card.append('div.mlabel').text(`example: donât think about the ${ex.concept}`) 8452 8453 var prompt = card.append('div.prompt.prompt-block') 8454 prompt.append('span.prompt-role').text('Human: ') 8455 prompt.append('span').text(ex.human_text) 8456 var resp = prompt.append('div.resp-line') 8457 resp.append('span.prompt-role').text('Assistant: ') 8458 var hl = new Set(ex.cells.map(c => c.sent_idx)) 8459 ex.sent_toks.forEach((t, i) => { 8460 if (!ex.sent_glue[i] && i) resp.append('span').text(' ') 8461 resp.append('span').text(t).classed('tok-hit', hl.has(i)).classed('resp', true) 8462 }) 8463 8464 var grid = card.append('div.exgrid') 8465 ex.cells.forEach(c => { 8466 var cell = grid.append('div.cell') 8467 var head = cell.append('div.chead') 8468 head.append('span.swatch').st({background: COLORS[c.side].dont_think}) 8469 head.append('span').text(`${cap(c.side_label)} â '${c.pos_tok}' token, L${c.layer_lbl}`) 8470 var maxP = d3.max(c.entries, e => Math.exp(e.lp)) 8471 c.entries.forEach(e => { 8472 var row = cell.append('div.entry') 8473 row.append('div.bar') 8474 .st({width: (Math.exp(e.lp)/maxP*100).toFixed(1) + '%', 8475 background: COLORS[c.side].dont_think}) 8476 row.append('span.tok').text(e.tok) 8477 row.append('span.lp').text(e.lp.toFixed(2)) 8478 }) 8479 }) 8480} 8481 8482function drawBars(sel, d){ 8483 var col = sel.append('div.bars-col') 8484 var lab = col.append('div.mlabel') 8485 .text(`concept word, fail, and damn in the readout, ${d.n} concepts`) 8486 8487 // size the chart so the right column matches the example column's height 8488 var margin = {left: 64, right: 8, top: 46, bottom: 30} 8489 var exH = sel.select('.ex-col').node()?.offsetHeight || 0 8490 var labH = lab.node()?.offsetHeight || 0 8491 var chartH = Math.max(260, exH - labH - margin.top - margin.bottom) 8492 var c = d3.conventions({ 8493 sel: col.append('div'), 8494 width: 380, height: chartH, 8495 margin, layers: 's', 8496 }) 8497 var nF = d.fams.length 8498 var x0 = d3.scaleBand().domain(d3.range(nF)).range([0, c.width]) 8499 .paddingInner(0.3).paddingOuter(0.1) 8500 var x1 = d3.scaleBand().domain(d3.range(d.series.length)) 8501 .range([0, x0.bandwidth()]).paddingInner(0.1) 8502 c.y.domain([0, 1.12]) 8503 c.yAxis.ticks(5) 8504 c.drawAxis() 8505 c.svg.select('.x').remove() 8506 c.svg.select('.y .domain').remove() 8507 // left + bottom spines (matplotlib style) 8508 c.svg.append('path.spine').at({ 8509 d: `M0,0 V${c.height} H${c.width}`, stroke: '#888', strokeWidth: 0.7, fill: 'none', 8510 }) 8511 // two-line rotated y label 8512 var ylab = c.svg.select('.y').append('g').translate([-44, c.height/2]) 8513 .append('text.axis-label').at({textAnchor: 'middle', transform: 'rotate(-90)'}) 8514 ylab.append('tspan').text(`fraction of concepts (n=${d.n})`).at({x: 0, dy: '-0.4em'}) 8515 ylab.append('tspan') 8516 .text(`lens top-${d.top_n}, any copied token, ${d.band_lbl}`) 8517 .at({x: 0, dy: '1.2em'}) 8518 8519 c.svg.append('g.x-ticks').translate([0, c.height + 16]) 8520 .appendMany('text.tick-label', d3.range(nF)) 8521 .translate(i => [x0(i) + x0.bandwidth()/2, 0]) 8522 .at({textAnchor: 'middle'}) 8523 .text(i => d.fams[i].label) 8524 8525 var g = c.svg.appendMany('g.fam', d3.range(nF)).translate(i => [x0(i), 0])
8526 d.series.forEach((s, j) => { 8527 var fill = COLORS[s.side][s.cond] 8528 g.append('rect.bar').at({ 8529 x: x1(j), width: x1.bandwidth(), fill, 8530 y: i => c.y(s.vals[i].v), height: i => c.height - c.y(s.vals[i].v), 8531 }) 8532 var exx = x1(j) + x1.bandwidth()/2, cap = Math.min(2.5, x1.bandwidth()*0.3) 8533 g.append('path.err').at({d: i => 8534 `M${exx},${c.y(s.vals[i].lo)} V${c.y(s.vals[i].hi)}` + 8535 ` M${exx-cap},${c.y(s.vals[i].lo)} h${2*cap}` + 8536 ` M${exx-cap},${c.y(s.vals[i].hi)} h${2*cap}`, 8537 }) 8538 g.append('text.vlabel') 8539 .at({x: exx, textAnchor: 'middle', y: i => c.y(s.vals[i].hi) - 4}) 8540 .text(i => { 8541 var v = s.vals[i].v 8542 return v == 0 ? '0' : v == 1 ? '1' : d3.format('.2f')(v).replace(/0$/, '').replace(/^0/, '') 8543 }) 8544 }) 8545 8546 var leg = c.svg.append('g.legend').translate([0, -36]) 8547 var pos = [[0, 0], [0, 16], [190, 0], [190, 16]] 8548 d.series.forEach((s, j) => { 8549 var item = leg.append('g').translate(pos[j]) 8550 item.append('rect').at({width: 10, height: 10, y: -9, rx: 2, fill: COLORS[s.side][s.cond]}) 8551 item.append('text').text(cap(s.label)).at({x: 15}) 8552 }) 8553} 8554 8555window.initMetacogAlarm = async function(opts){ 8556 var sel = d3.select('.metacog-alarm').html('') 8557 var base = (opts?.datapath || './').replace(/\/?$/, '/') 8558 var d = await util.getFile(base + 'data.json') 8559 var row = sel.append('div.panel-row') 8560 drawExample(row, d) 8561 drawBars(row, d) 8562} 8563 8564}() 8565window.init?.() 8566 8567// âââ public/ignition/init-ignition.js âââ 8568window.initIgnition = async function ({datapath, sel: selStr, show}
8568) { 8569 var sel = d3.select(selStr || '.ignition').html('') 8570 show = show || 'abc' 8571 var d = await util.getFile(datapath + 'data.json') 8572 8573 var n = d.n_pub, [wlo, whi] = d.ws_band 8574 var ORANGE = util.tol.orange, DARK = util.gray[700] 8575 var rdbu = t => d3.interpolateRdBu(1 - t) // RdBu_r: 0âblue(B) 1âred(A) 8576 var greys = t => d3.interpolateGreys(1 - t) // Greys_r: 0âblack(rank 1) 4âwhite 8577 8578 // âââââ row 1: A | B+C âââââ 8579 if (show === 'abc'){ 8580 var row1 = sel.append('div.row') 8581 var a = row1.append('div.panel.schematic') 8582 a.append('div.plabel').html('A Mixing two tokensâ embeddings') 8583 a.append('div').html(d.schematic_html) 8584 8585 var bc = row1.append('div.panel.bc') 8586 var bcLabels = bc.append('div.bc-labels') 8587 bcLabels.append('div.plabel.lab-b') 8588 .html('B Activations given interpolated inputs') 8589 bcLabels.append('div.plabel.lab-c') 8590 .html('C J-lens behavior given interpolated inputs') 8591 var bcRow = bc.append('div.hm-row') 8592 8593 var HMS = [ 8594 {key: 'proj', title: 'Relative similarity to activations\ninduced by pure token embedding', 8595 vmax: 1, color: rdbu, cbar: {ticks: [0, 1], labels: ['B', 'A']}}, 8596 {key: 'logrank', title: 'J-lens rank of the\nhigher ranked concept', 8597 vmax: 4, color: greys, 8598 cbar: {ticks: [0, 1, 2, 3, 4], labels: ['1', '10', '100', '1k', '10k'], invert: true}}, 8599 {key: 'jspan', title: 'Relative similarity to J-lens\nvectors for concepts A and B', 8600 vmax: 1, color: rdbu, cbar: null}, 8601 ] 8602 HMS.forEach((h, hi) => drawHeatmap(bcRow.append('div.hm-cell'), h, hi)) 8603 } 8604 8605 // âââââ D: transition width (own figure) âââââ 8606 if (show === 'd'){ 8607 var dSel = sel.append('div.panel.panel-d') 8608 dSel.append('div.plabel').text('Transition width') 8609 drawWidths(dSel.append('div')) 8610 } 8611 8612 // âââââ E: ambiguous-input histograms (own figure) âââââ 8613 if (show === 'e'){ 8614 var eSel = sel.append('div.panel.panel-e') 8615 eSel.append('div.plabel').text( 8616 'Relative share in activations of concept A at maximally ambiguous ' + 8617 'interpolation coefficient, across prompts') 8618 drawHistRow(eSel.append('div.hist-row')) 8619 } 8620 8621 // âââââ panel B/C: one heatmap (canvas) + svg axes + optional colorbar âââââ 8622 function drawHeatmap(host, h, hi){ 8623 var W = 192, H = 244, cbW = h.cbar ? 30 : 14 8624 var m = {left: hi ? 8 : 36, right: cbW, top: 30, bottom: 36} 8625 var c = d3.conventions({sel: host, width: W, height: H, margin: m, layers: 'cs'}) 8626 var ctx = c.layers[0], svg = c.layers[1] 8627 var na = d.d_grid.length, cw = W / na, ch = H / n 8628 var grid = d.heatmaps[h.key] 8629 for (var li = 0; li < n; li++) for (var ai = 0; ai < na; ai++){ 8630 ctx.fillStyle = h.color(grid[li][ai] / h.vmax) 8631 ctx.fillRect(ai * cw, H - (li + 1) * ch, cw + 0.5, ch + 0.5) 8632 } 8633 c.x.domain([d.d_grid[0], d.d_grid[na - 1]]) 8634 c.y.domain([-0.5, n - 0.5]) 8635 c.xAxis.tickValues([-0.5, 0, 0.5]) 8636 .tickFormat(v => v < 0 ? 'Pure B' : v > 0 ? 'Pure A' : 'Ambiguous') 8637 .tickSizeOuter(0).tickSizeInner(3) 8638 c.yAxis.tickValues(util.layerTicks(n)).tickFormat(util.layerLabel) 8639 .tickSizeOuter(0).tickSizeInner(3) 8640 c.drawAxis() 8641 c.svg.select('.x').selectAll('text').st({fontSize: '9px'}) 8642 if (hi) c.svg.select('.y').selectAll('text').remove() 8643 util.addAxisLabel(c, 'input evidence', hi ? '' : 'Layer (reindexed) â') 8644 // ws-band guides â dashed horizontals at the band edges 8645 ;[wlo - 0.5, whi + 0.5].forEach(yy => svg.append('line.ws-guide') 8646 .at({x1: 0, x2: W, y1: c.y(yy), y2: c.y(yy)})) 8647 // two-line title 8648 var t = svg.append('text.hm-title').at({x: W / 2, textAnchor: 'middle'}) 8649 h.title.split('\n').forEach((s, i) => 8650 t.append('tspan').text(s).at({x: W / 2, y: -18 + i * 11})) 8651 // colorbar 8652 if (h.cbar){ 8653 var cb = svg.append('g.cbar').translate([W + 6, 0]), bw = 9, nseg = 60 8654 cb.appendMany('rect', d3.range(nseg)).at({ 8655 y: i => (h.cbar.invert ? i : nseg - 1 - i) * H / nseg, 8656 width: bw, height: H / nseg + 0.5, fill: i => h.color(i / (nseg - 1)), 8657 }) 8658 cb.appendMany('text', h.cbar.ticks).text((_, i) => h.cbar.labels[i]) 8659 .at({x: bw + 3, dy: '.35em', 8660 y: v => (h.cbar.invert ? v / h.vmax : 1 - v / h.vmax) * H}) 8661 } 8662 } 8663 8664 // âââââ panel D: median ± IQR transition width by pub-layer âââââ 8665 function drawWidths(host){ 8666 var c = d3.conventions({ 8667 sel: host, width: 420, height: 260, 8668 margin: {left: 46, right: 6, top: 8, bottom: 32}, 8669 }) 8670 c.x.domain([-0.5, n - 0.5])
8671 c.y = d3.scaleLog().domain([0.018, 1.2]).range([c.height, 0]) 8672 c.xAxis.tickValues(util.layerTicks(n)).tickFormat(util.layerLabel) 8673 c.yAxis.scale(c.y).tickValues([0.03, 0.1, 0.3, 1.0]).tickFormat(v => v) 8674 c.drawAxis(); util.ggPlot(c) 8675 util.addAxisLabel(c, 'Layer (reindexed) â', 'transition width Îα (10â90%)', '', 0, -4) 8676 util.addWsBand(c, wlo, whi, {label: false}) 8677 var xs = d3.range(n) 8678 var line = d3.line().x((_, i) => c.x(xs[i])).y(v => c.y(v)) 8679 var area = d3.area().x((_, i) => c.x(xs[i])) 8680 .y0(p => c.y(p.lo)).y1(p => c.y(p.hi)) 8681 var SER = [ 8682 {k: 'j', color: ORANGE, label: ['Projections onto concept', 'word J-lens vectors']}, 8683 {k: 'nj', color: DARK, label: ['Non-J-space component', 'of activations']}, 8684 ] 8685 SER.forEach(s => { 8686 var w = d.widths[s.k] 8687 c.svg.append('path').at({ 8688 d: area(xs.map(i => ({lo: w.q1[i], hi: w.q3[i]}))), 8689 fill: s.color, stroke: 'none', 8690 }).st({opacity: 0.15}) 8691 }) 8692 SER.forEach(s => { 8693 var w = d.widths[s.k] 8694 c.svg.append('path').at({d: line(w.med), fill: 'none', 8695 stroke: s.color, strokeWidth: 1.6}) 8696 c.svg.appendMany('circle', xs).at({ 8697 cx: i => c.x(i), cy: i => c.y(w.med[i]), r: 2.2, fill: s.color, 8698 }) 8699 }) 8700 igLegend(c.svg, [c.width - 2, 4], SER) 8701 } 8702 8703 // âââââ panel E: histogram row at four pub layers âââââ 8704 function drawHistRow(host){ 8705 var bins = d.hist.bin_edges, nb = bins.length - 1 8706 var ymax = d3.max(d.hist.pub_layers, (_, li) => 8707 d3.max(['j', 'nj'], k => d3.max(d.hist[k][li]))) 8708 var SER = [ 8709 {k: 'j', color: ORANGE, label: ['Projections onto concept', 'word J-lens vectors']}, 8710 {k: 'nj', color: DARK, label: ['Non-J-space component', 'of activations']}, 8711 ] 8712 d.hist.pub_layers.forEach((pub, li) => { 8713 var c = d3.conventions({ 8714 sel: host.append('div'), width: 156, height: 230, 8715 margin: {left: li ? 8 : 38, right: 4, top: 18, bottom: 32}, 8716 }) 8717 c.x.domain([0, 1]); c.y.domain([0, ymax * 1.08]) 8718 c.xAxis.tickValues([0, 0.5, 1]).tickFormat(v => v) 8719 c.yAxis.ticks(4) 8720 c.drawAxis(); util.ggPlot(c) 8721 if (li) c.svg.select('.y').selectAll('text').remove() 8722 c.svg.append('text.hm-title').text('L' + util.layerLabel(pub)) 8723 .at({x: c.width / 2, y: -6, textAnchor: 'middle'}) 8724 ;[['nj', DARK], ['j', ORANGE]].forEach(([k, col]) => { 8725 var counts = d.hist[k][li] 8726 var p = `M${c.x(bins[0])},${c.y(0)}` 8727 for (var b = 0; b < nb; b++) 8728 p += `V${c.y(counts[b])}H${c.x(bins[b + 1])}` 8729 p += `V${c.y(0)}` 8730 c.svg.append('path').at({d: p, fill: 'none', stroke: col, strokeWidth: 1.4}) 8731 }) 8732 if (li == 0) util.addAxisLabel(c, '', 'trials', '', 0, 0) 8733 if (li == 1) igLegend(c.svg, [c.width - 2, 6], SER) 8734 }) 8735 host.append('div.hist-xlabel') 8736 .text('Relative share of activations across ambiguous inputs') 8737 } 8738 8739 // Shared D/E legend: line swatch on the left, two-line label on the 8740 // right, right-aligned block (matches the matplotlib loc='upper right'). 8741 function igLegend(svg, at, ser){ 8742 var leg = svg.append('g.ig-legend').translate(at) 8743 ser.forEach((s, i) => { 8744 var g = leg.append('g').translate([0, i * 28]) 8745 // text first so we can place the swatch just to its left 8746 var w = 0 8747 s.label.forEach((t, j) => { 8748 var el = g.append('text').text(t) 8749 .at({x: 0, y: j * 12, dy: '.32em', textAnchor: 'end'}) 8750 w = Math.max(w, el.node().getComputedTextLength()) 8751 }) 8752 g.append('line').at({x1: -w - 20, x2: -w - 6, y1: 5, y2: 5, 8753 stroke: s.color, strokeWidth: 2.2}) 8754 }) 8755 } 8756} 8757 8758if (window.init) window.init() 8759 8760// âââ public/eval-awareness-probe/init-eval-awareness-probe.js âââ 8761window.initEvalAwarenessProbe = async function ({datapath}) { 8762 var sel = d3.select('.eval-awareness-probe').html('') 8763 var d = await util.getFile(datapath + 'data.json') 8764 sel.append('img') 8765 .at({src: 'data:image/png;base64,' + d.png}) 8766 .st({width: '100%', height: 'auto'}) 8767} 8768 8769if (window.init) window.init() 8770 8771// âââ public/dual-task-simple/init-dual-task-simple.js âââ 8772!function(){ 8773 8774var BLUE = '#2563eb', ORANGE = '#ff6b35', GREY = '#c7cdd4' 8775var TASK_A = '#16a34a', TASK_B = '#7c3aed' 8776 8777function markPhrases(sel, text, pa, pb){ 8778 var rest = text 8779 ;
8779[[pa, 'ph-a'], [pb, 'ph-b']].forEach(() => {}) 8780 // split on the two phrases, keeping order of first occurrence 8781 var parts = [] 8782 var ia = rest.indexOf(pa), ib = rest.indexOf(pb) 8783 var order = ia <= ib ? [[ia, pa, 'ph-a'], [ib, pb, 'ph-b']] : [[ib, pb, 'ph-b'], [ia, pa, 'ph-a']] 8784 var pos = 0 8785 order.forEach(([i, p, cls]) => { 8786 if (i < 0) return 8787 parts.push([rest.slice(pos, i), null]) 8788 parts.push([p, cls]) 8789 pos = i + p.length 8790 }) 8791 parts.push([rest.slice(pos), null]) 8792 parts.forEach(([t, cls]) => { 8793 if (!t) return 8794 sel.append('span').text(t).at({class: cls || null}) 8795 }) 8796} 8797 8798function drawExamples(sel, d){ 8799 var col = sel.append('div.panel.ex-panel') 8800 col.append('div.mlabel').text('example: the readout at two response tokens') 8801 d.blocks.forEach(b => { 8802 var blk = col.append('div.exblock') 8803 blk.append('div.exhead').text(b.head) 8804 .st({color: b.key == 'cc' ? BLUE : ORANGE}) 8805 var instr = blk.append('div.prompt') 8806 instr.append('span').text(`Write "${d.sentence}" `) 8807 markPhrases(instr, b.instr, b.phrase_a, b.phrase_b) 8808 instr.append('span').text(" Don't write anything else.") 8809 var resp = blk.append('div.resp') 8810 resp.append('span.role').text('A: ') 8811 var hl = new Set(b.cells.map(c => c.pos_tok)) 8812 d.sentence.split(/(crookedly|on)/).forEach(seg => { 8813 if (seg == 'crookedly') { 8814 resp.append('span').text('cr') 8815 resp.append('span.hl').text('ook') 8816 resp.append('span').text('edly') 8817 } else if (seg == 'on' && hl.has('on')) { 8818 resp.append('span.hl').text('on') 8819 } else { 8820 resp.append('span').text(seg) 8821 } 8822 }) 8823 var lens = blk.append('div.lens-readout') 8824 b.cells.forEach(c => { 8825 var line = lens.append('div') 8826 line.append('span.label').text(`${c.pos_tok} · L${c.layer_lbl}: `) 8827 c.chips.forEach(ch => { 8828 if (ch.cls == 'other') { 8829 line.append('span.tok-g').text(ch.tok) 8830 } else { 8831 var s = line.append('span').at({class: ch.cls == 'a' ? 'tok-a' : 'tok-b'}) 8832 s.append('span').text(ch.tok) 8833 s.append('span.tok-rk').text(ch.rank) 8834 } 8835 line.append('span').text(' ') 8836 }) 8837 }) 8838 }) 8839} 8840 8841function drawCoocc(sel, d){ 8842 var col = sel.append('div.panel') 8843 col.append('div.mlabel').text('co-occupancy at shared tokens') 8844 var c = d3.conventions({ 8845 sel: col.append('div'), 8846 width: 240, height: 290, 8847 margin: {left: 64, right: 6, top: 16, bottom: 52}, 8848 layers: 's', 8849 }) 8850 var conds = d.coocc.plotted.map(i => d.coocc.conds[i]) 8851 var cols = [BLUE, ORANGE] 8852 c.x.domain([-0.6, conds.length - 0.4]) 8853 c.y.domain([0, 1]) 8854 c.yAxis.ticks(5) 8855 c.drawAxis() 8856 c.svg.select('.x').remove() 8857 c.svg.selectAll('.domain').remove() 8858 c.svg.append('path.spine').at({ 8859 d: `M0,0 V${c.height} H${c.width}`, stroke: '#888', strokeWidth: 0.7, fill: 'none'}) 8860 var bw = 0.34 8861 conds.forEach((s, gi) => { 8862 var x1 = c.x(gi - (bw/2 + 0.02)), x2 = c.x(gi + (bw/2 + 0.02)) 8863 var w = c.x(bw) - c.x(0) 8864 ;[[x1, s.observed, s.ci_lo, s.ci_hi, cols[gi], '#374151'], 8865 [x2, s.null_mean, s.null_lo, s.null_hi, GREY, '#6b7280']].forEach(([xc, v, lo, hi, fill, ec]) => { 8866 c.svg.append('rect').at({ 8867 x: xc - w/2, width: w, y: c.y(v), height: c.height - c.y(v), fill}) 8868 c.svg.append('path').at({ 8869 d: `M${xc},${c.y(lo)} V${c.y(hi)} M${xc-3},${c.y(lo)} h6 M${xc-3},${c.y(hi)} h6`, 8870 stroke: ec, strokeWidth: 1.2, fill: 'none'}) 8871 c.svg.append('text.vlabel').text(d3.format('.2f')(v)) 8872 .at({x: xc, y: c.y(hi) - 4, textAnchor: 'middle'}) 8873 }) 8874 var lab = c.svg.append('text.gxlabel') 8875 .at({x: c.x(gi), y: c.height + 16, textAnchor: 'middle', fill: cols[gi]}) 8876 s.label.split(' + ').length > 1 8877 ? ['concept', '+ math'].forEach((t, k) => lab.append('tspan').text(t).at({x: c.x(gi), dy: k ? '1.15em' : 0})) 8878 : ['two', 'concepts'].forEach((t, k) => lab.append('tspan').text(t).at({x: c.x(gi), dy: k ? '1.15em' : 0})) 8879 }) 8880 // legend: shuffled control 8881 var leg = c.svg.append('g').translate([c.width - 4, 6]) 8882 leg.append('rect').at({x: -110, width: 11, height: 11, y: -9, fill: GREY}) 8883 leg.append('text').text('shuffled control').at({x: -95}).st({fontSize: 'var(--fs-small)'}) 8884 var ylab = c.svg.select('.y').append('g').translate([-44, c.height/2]) 8885 .append('text.axis-label').at({textAnchor: 'middle', transform: 'rotate(-90)'})
8886 ylab.append('tspan').text(`P(other task in lens top-${d.k_coocc})`).at({x: 0, dy: '-0.4em'}) 8887 ylab.append('tspan').text(`at tokens where this task is in top-${d.k_coocc}`).at({x: 0, dy: '1.2em'}) 8888} 8889 8890function drawReach(sel, d){ 8891 var col = sel.append('div.panel') 8892 col.append('div.mlabel').text('reachability: single vs dual task') 8893 var c = d3.conventions({ 8894 sel: col.append('div'), 8895 width: 290, height: 290, 8896 margin: {left: 56, right: 6, top: 16, bottom: 52}, 8897 layers: 's', 8898 }) 8899 c.svg.append('defs').append('pattern') 8900 .at({id: 'hatch-dual', width: 5, height: 5, patternUnits: 'userSpaceOnUse', 8901 patternTransform: 'rotate(45)'}) 8902 .append('rect').at({width: 2, height: 5, fill: '#fff'}) 8903 var cols = [BLUE, ORANGE, ORANGE] 8904 c.x.domain([-0.6, d.reach.length - 0.4]) 8905 c.y.domain([0, 1.12]) 8906 c.yAxis.ticks(5).tickFormat(d3.format('.0%')) 8907 c.drawAxis() 8908 c.svg.select('.x').remove() 8909 c.svg.selectAll('.domain').remove() 8910 c.svg.append('path.spine').at({ 8911 d: `M0,0 V${c.height} H${c.width}`, stroke: '#888', strokeWidth: 0.7, fill: 'none'}) 8912 var bw = 0.34 8913 d.reach.forEach((r, gi) => { 8914 var x1 = c.x(gi - (bw/2 + 0.02)), x2 = c.x(gi + (bw/2 + 0.02)) 8915 var w = c.x(bw) - c.x(0) 8916 c.svg.append('rect').at({ 8917 x: x1 - w/2, width: w, y: c.y(r.single), height: c.height - c.y(r.single), 8918 fill: cols[gi]}) 8919 var g2 = c.svg.append('g') 8920 g2.append('rect').at({ 8921 x: x2 - w/2, width: w, y: c.y(r.dual), height: c.height - c.y(r.dual), 8922 fill: cols[gi]}) 8923 g2.append('rect').at({ 8924 x: x2 - w/2, width: w, y: c.y(r.dual), height: c.height - c.y(r.dual), 8925 fill: 'url(#hatch-dual)'}) 8926 ;[[x1, r.single], [x2, r.dual]].forEach(([xc, v]) => { 8927 c.svg.append('text.vlabel').text(d3.format('.0%')(v)) 8928 .at({x: xc, y: c.y(v) - 4, textAnchor: 'middle'}) 8929 }) 8930 var lab = c.svg.append('text.gxlabel') 8931 .at({x: c.x(gi), y: c.height + 16, textAnchor: 'middle', fill: cols[gi]}) 8932 r.label.split(' ').length > 1 8933 ? r.label.replace('(w/ math)', '(w/·math)').split(' ').forEach((t, k) => 8934 lab.append('tspan').text(t.replace('·', ' ')).at({x: c.x(gi), dy: k ? '1.15em' : 0})) 8935 : lab.text(r.label) 8936 }) 8937 // legend 8938 var leg = c.svg.append('g').translate([4, 6]) 8939 leg.append('rect').at({width: 11, height: 11, y: -9, fill: '#555'}) 8940 leg.append('text').text('single task').at({x: 15}).st({fontSize: 'var(--fs-small)'}) 8941 var l2 = leg.append('g').translate([90, 0]) 8942 l2.append('rect').at({width: 11, height: 11, y: -9, fill: '#555'}) 8943 l2.append('rect').at({width: 11, height: 11, y: -9, fill: 'url(#hatch-dual)'}) 8944 l2.append('text').text('dual task').at({x: 15}).st({fontSize: 'var(--fs-small)'}) 8945 var ylab = c.svg.select('.y').append('g').translate([-40, c.height/2]) 8946 .append('text.axis-label').at({textAnchor: 'middle', transform: 'rotate(-90)'}) 8947 ylab.append('tspan').text(`reaches lens top-${d.topk}`).at({x: 0, dy: '-0.4em'}) 8948 ylab.append('tspan').text(`(band ${d.band_lbl}, any position)`).at({x: 0, dy: '1.2em'}) 8949} 8950 8951window.initDualTaskSimple = async function(opts){ 8952 var sel = d3.select('.dual-task-simple').html('') 8953 var base = (opts?.datapath || './').replace(/\/?$/, '/') 8954 var d = await util.getFile(base + 'data.json') 8955 var row = sel.append('div.panel-row') 8956 drawExamples(row, d) 8957 drawCoocc(row, d) 8958 drawReach(row, d) 8959} 8960 8961}() 8962window.init?.() 8963 8964// âââ public/capacity-final-band/init-capacity-final-band.js âââ 8965!function(){ 8966 8967var ORANGE = util.tol.orange, BLUE = util.tol.blue, GREY = util.gray[500] 8968var BLK_COLS = [util.tol.orange, util.tol.magenta, util.tol.teal, util.tol.cyan] 8969 8970function chipBlock(col, hdr, sub, chips){ 8971 var blk = col.append('div.chipblock') 8972 blk.append('div.chiphead').text(hdr) 8973 blk.append('div.chipsub').text(sub) 8974 var row = blk.append('div.chips') 8975 chips.forEach(ch => { 8976 row.append('span.chip').at({class: 'chip ' + ch.cls}).text(ch.tok) 8977 row.append('span').text(' ') 8978 }) 8979} 8980 8981function drawA(sel, d){ 8982 var col = sel.append('div.panel.chip-panel') 8983 col.append('div.mlabel').text('example: reading a single-family list') 8984 d.a.forEach(ex => { 8985 chipBlock(col, 8986 `after ${ex.n_read} word${ex.n_read > 1 ? 's' : ''} read`, 8987 'read: ' + ex.read_words.join(', '), 8988 ex.chips.map(ch => ({tok: ch.tok, cls: ch.cls == 'read' ? 'cur' : 'other'}))) 8989 }) 8990} 8991 8992function drawD(sel, d){ 8993 var col = sel.append('div.panel.chip-panel') 8994 col.append('div.mlabel').text('example: color words after the animals') 8995 d.d.forEach(ex => { 8996 chipBlock(col, 8997 `${ex.n_colors} color word${ex.n_colors > 1 ? 's' : ''} after the 8 animals`, 8998 'colors read: ' + ex.read_words.join(', '), 8999 ex.chips) 9000 }) 9001} 9002 9003function lineChart(sel, title, opts){ 9004 var col = sel.append('div.panel') 9005 col.append('div.mlabel').text(title) 9006 var c = d3.conventions({ 9007 sel: col.append('div'), 9008 width: 270, height: 280, 9009 margin: {left: 46, right: 10, top: 12, bottom: 40}, 9010 layers: 's', 9011 }) 9012 c.x.domain(opts.xdom) 9013 c.y.domain(opts.ydom) 9014 c.xAxis.ticks(5) 9015 c.yAxis.ticks(5) 9016 c.drawAxis() 9017 util.ggPlot(c) 9018 util.addAxisLabel(c, opts.xlabel, opts.ylabel, '', 6) 9019 return c 9020} 9021 9022function band(c, xs, q1, q3, color){ 9023 var area = d3.area().x((v, i) => c.x(xs[i])).y0(v => c.y(v[0])).y1(v => c.y(v[1])) 9024 c.svg.append('path').at({ 9025 d: area(q1.map((v, i) => [v, q3[i]])), fill: color, opacity: 0.10}) 9026} 9027 9028function line(c, xs, ys, color, opts){ 9029 var ln = d3.line().x((v, i) => c.x(xs[i])).y(v => c.y(v)) 9030 c.svg.append('path').at({ 9031 d: ln(ys), stroke: color, fill: 'none', 9032 strokeWidth: opts?.lw ?? 2, strokeDasharray: opts?.dash || null}) 9033} 9034 9035function legend(c, entries, pos){ 9036 var leg = c.svg.append('g.legend').translate(pos || [8, 8])
9037 entries.forEach((e, i) => { 9038 var g = leg.append('g').translate([0, i * 14]) 9039 g.append('line').at({x1: 0, x2: 14, stroke: e.color, 9040 strokeWidth: e.lw ?? 2, strokeDasharray: e.dash || null}) 9041 g.append('text').text(e.label).at({x: 18, dy: '0.32em'}) 9042 }) 9043} 9044 9045function drawB(sel, d){ 9046 var xs = d3.range(d.ng) 9047 var c = lineChart(sel, 'words in the readout: family vs random lists', { 9048 xdom: [0, d.ng - 1], ydom: [0, d.ylim_top], 9049 xlabel: 'comma #', ylabel: `# list words in top-${d.k25} (band-min)`, 9050 }) 9051 ;[['family', ORANGE], ['random', BLUE]].forEach(([lab, color]) => { 9052 var s = d.b[lab] 9053 band(c, xs, s.read_q1, s.read_q3, color) 9054 line(c, xs, s.read_med, color) 9055 line(c, xs, s.all_med, color, {lw: 1.5, dash: '4 2'}) 9056 }) 9057 legend(c, [ 9058 {label: 'family: read', color: ORANGE}, 9059 {label: 'family: all 80', color: ORANGE, dash: '4 2', lw: 1.5}, 9060 {label: 'random: read', color: BLUE}, 9061 {label: 'random: all 80', color: BLUE, dash: '4 2', lw: 1.5}, 9062 ]) 9063} 9064 9065function drawC(sel, d){ 9066 var c = lineChart(sel, 'read words vs rank threshold', { 9067 xdom: [0, 100], ydom: [0, 100], 9068 xlabel: 'rank threshold K (band-min)', ylabel: '# READ words ⤠K (near end of list)', 9069 }) 9070 c.svg.append('line').at({ 9071 x1: 0, x2: c.width, y1: c.y(80), y2: c.y(80), 9072 stroke: GREY, strokeWidth: 1, strokeDasharray: '4 2'}) 9073 c.svg.append('text').text('list length (80)') 9074 .at({x: c.width - 4, y: c.y(80) - 5, textAnchor: 'end', fontSize: 9, fill: 'var(--text-light)'}) 9075 line(c, d.c.ks, d.c.family, ORANGE) 9076 line(c, d.c.ks, d.c.random, BLUE) 9077 line(c, d.c.ks, d.c.control, GREY, {lw: 1.2}) 9078 legend(c, [ 9079 {label: 'family', color: ORANGE}, 9080 {label: 'random', color: BLUE}, 9081 {label: 'control', color: GREY, lw: 1.2}, 9082 ]) 9083} 9084 9085function drawE(sel, d){ 9086 var xs = d3.range(d.ng) 9087 var c = lineChart(sel, 'words in the readout: four-family block lists', { 9088 xdom: [0, d.ng - 1], ydom: [0, d.ylim_top], 9089 xlabel: 'comma #', ylabel: `# family words in top-${d.k25} (band-min)`, 9090 }) 9091 d.e.blocks.forEach((blk, bi) => { 9092 band(c, xs, blk.q1, blk.q3, BLK_COLS[bi]) 9093 line(c, xs, blk.med, BLK_COLS[bi], {lw: 1.8}) 9094 }) 9095 line(c, xs, d.e.random_med, GREY, {lw: 1.1}) 9096 legend(c, [ 9097 ...d.e.blocks.map((_, bi) => ({label: `block ${bi + 1}`, color: BLK_COLS[bi], lw: 1.8})), 9098 {label: 'random', color: GREY, lw: 1.1}, 9099 ], [8, 8]) 9100} 9101 9102function drawF(sel, d){ 9103 var col = sel.append('div.panel') 9104 col.append('div.mlabel').text('per-word presence in block lists') 9105 var c = d3.conventions({ 9106 sel: col.append('div'), 9107 width: 260, height: 280, 9108 margin: {left: 46, right: 56, top: 12, bottom: 40}, 9109 layers: 'cs', 9110 }) 9111 var nW = d.f.length, nG = d.f[0].length 9112 var x = d3.scaleLinear().domain([-0.5, nG - 0.5]).range([0, c.width]) 9113 var y = d3.scaleLinear().domain([-0.5, nW - 0.5]).range([c.height, 0]) 9114 var ctx = c.layers[0] 9115 d.f.forEach((row, wi) => row.forEach((v, gi) => { 9116 ctx.fillStyle = v == null ? '#f4f5f7' : d3.interpolateOranges(v) 9117 ctx.fillRect( 9118 Math.floor(x(gi - 0.5)), Math.floor(y(wi + 0.5)), 9119 Math.ceil(x(gi + 0.5)) - Math.floor(x(gi - 0.5)), 9120 Math.ceil(y(wi - 0.5)) - Math.floor(y(wi + 0.5))) 9121 })) 9122 ;[20, 40, 60].forEach(b => { 9123 c.svg.append('line').at({ 9124 x1: x(b - 0.5), x2: x(b - 0.5), y1: 0, y2: c.height, 9125 stroke: '#1f2937', strokeWidth: 0.6, opacity: 0.5}) 9126 c.svg.append('line').at({ 9127 x1: 0, x2: c.width, y1: y(b - 0.5), y2: y(b - 0.5), 9128 stroke: '#1f2937', strokeWidth: 0.6, opacity: 0.5}) 9129 }) 9130 c.x = x; c.y = y 9131 c.xAxis.scale(x).ticks(4) 9132 c.yAxis.scale(y).ticks(4) 9133 c.drawAxis() 9134 c.svg.selectAll('.domain').remove() 9135 util.addAxisLabel(c, 'readout comma', 'word read at #', '', 6) 9136 9137 var cbH = 195, cbW = 9, n = 50 9138 var cb = c.svg.append('g').translate([c.width + 12, (c.height - cbH)/2]) 9139 cb.appendMany('rect', d3.range(n)).at({ 9140 y: i => cbH - (i+1)*cbH/n, width: cbW, height: cbH/n + 1, 9141 fill: i => d3.interpolateOranges(i/(n-1)), 9142 }) 9143 var cy = d3.scaleLinear().domain([0, 1]).range([cbH, 0]) 9144 cb.appendMany('text', [0, 0.5, 1]).text(v => d3.format('.1f')(v)) 9145 .at({x: cbW + 4, y: v => cy(v) + 3, fontSize: 9, fill: '#555'}) 9146 cb.append('text').text(`P(band-min rank ⤠${d.k25})`) 9147 .at({transform: `translate(${cbW + 30},${cbH/2}) rotate(-90)`, 9148 textAnchor: 'middle', fontSize: 9, fill: '#555'}) 9149} 9150 9151window.initCapacityFinalBand = async function(opts){ 9152 var sel = d3.select('.capacity-final-band').html('') 9153 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9154 var d = await util.getFile(base + 'data.json') 9155 var r1 = sel.append('div.panel-row') 9156 drawA(r1, d) 9157 drawB(r1, d) 9158 drawC(r1, d) 9159 var r2 = sel.append('div.panel-row') 9160 drawD(r2, d) 9161 drawE(r2, d) 9162 drawF(r2, d) 9163} 9164 9165}() 9166window.init?.() 9167 9168// âââ public/flex-generalization-example/init-flex-generalization-example.js âââ 9169window.initFlexGenExample = async function(opts){ 9170 var sel = d3.select('.flex-generalization-example').html('') 9171 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9172 var d = await util.getFile(base + 'data.json') 9173 9174 var CLEAN = util.gray[500], SWAP = util.tol.orange 9175 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 9176 9177 var panels = sel.appendMany('div.fge-panel', d.blocks) 9178 9179 var left = panels.append('div.fge-left') 9180 left.append('div.fig-title').text(b => b.func.charAt(0).toUpperCase() + b.func.slice(1)) 9181 var splitHit = b => b.prompt.match(/^([\s\S]*?)([A-Za-z0-9]+|.)$/).slice(1) 9182 var prompt = left.append('div.fge-prompt') 9183 prompt.append('span').text(b => splitHit(b)[0]) 9184 prompt.append('span.tok-hit').text(b => splitHit(b)[1]) 9185 left.append('div.fge-swap').html( 9186 `J-lens coordinate swap: ${esc(d.swap_from)} â <span style="color:${SWAP}">${esc(d.swap_to)}</span>`) 9187 9188 var right = panels.append('div.fge-right') 9189 function col(side, label, color, barBg){ 9190 var c = right.append('div.fge-col.bar-table').classed(side, true) 9191 c.append('h6.mlabel').html((b, i) => 9192 `<span class="swatch" style="background:${color}"></span> ${esc(label)}` + 9193 (i == 0 && side == 'clean' ? ` <span class="hdr-sub">(log prob)</span>` : '')) 9194 c.appendMany('div.bar-row', b => b[side].map(e => ({...e, w: Math.exp(e.lp)/d.max_prob*100}))) 9195 .at({title: e => e.tok}) 9196 .html(e => `<div class="bar" style="width:${e.w.toFixed(1)}%;background:${barBg}"></div>` + 9197 `<span class="t">${esc(e.tok)}</span><span class="val">${e.lp.toFixed(2)}</span>`) 9198 } 9199 col('clean', 'Clean', CLEAN, util.gray[200]) 9200 col('patched', 'Swapped', SWAP, util.tint(SWAP, 0.65)) 9201} 9202 9203window.init?.() 9204 9205// âââ public/flex-generalization-systematic/init-flex-generalization-systematic.js âââ 9206!function(){ 9207 9208var INK = '#1f2937', GREY = '#9ca3af', LIGHT = '#d1d5db', MUTED = '#888' 9209 9210// Per-label nudges for scatter points that land on top of each other 9211// (mirrors the matplotlib figure). 9212// Points whose labels sit on top of a neighbour â drop the label, keep the dot. 9213var NO_LABEL = new Set(['class', 'square']) 9214 9215var NUDGE = { 9216 continent: [-7, -3, 'end'], holiday: [7, -3, 'start'], 9217 first_letter: [-7, 3, 'end'], square: [-7, -3, 'end'], 9218 legs: [-7, -3, 'end'], double: [7, -3, 'start'], 9219} 9220 9221function drawDots(sel, d){ 9222 var col = sel.append('div.panel') 9223 col.append('div.mlabel').text('fraction of swaps at top-1, by function') 9224 var catColor = {}
9225 d.cats.forEach(c => catColor[c.name] = c.color) 9226 var rows = [] 9227 d.cats.forEach(c => { 9228 var sub = d.funcs.filter(f => f.cat == c.name) 9229 .sort((a, b) => b.hit2 - a.hit2) 9230 rows.push(...sub) 9231 }) 9232 var c = d3.conventions({ 9233 sel: col.append('div'), 9234 width: 230, height: 300, 9235 margin: {left: 98, right: 12, top: 10, bottom: 42}, 9236 layers: 's', 9237 }) 9238 c.x.domain([0, 1]) 9239 c.xAxis.tickValues([0, 0.5, 1]) 9240 var y = i => (i + 0.5) * c.height / rows.length 9241 c.svg.append('g.x.axis').translate([0, c.height]).call(c.xAxis) 9242 c.svg.selectAll('.domain').remove() 9243 c.svg.append('path.spine').at({ 9244 d: `M0,0 V${c.height} H${c.width}`, stroke: '#888', strokeWidth: 0.7, fill: 'none'}) 9245 util.addAxisLabel(c, 'fraction at top-1', '', '', 6) 9246 9247 var catBreaks = [] 9248 var n = 0 9249 d.cats.slice(0, -1).forEach(cat => { 9250 n += d.funcs.filter(f => f.cat == cat.name).length 9251 catBreaks.push(n) 9252 }) 9253 catBreaks.forEach(b => { 9254 c.svg.append('line').at({ 9255 x1: -96, x2: c.width, y1: y(b - 0.5), y2: y(b - 0.5), 9256 stroke: LIGHT, strokeWidth: 0.5}) 9257 }) 9258 9259 rows.forEach((f, i) => { 9260 var color = catColor[f.cat] 9261 var h1 = f.hit1 / f.n, h2 = f.hit2 / f.n 9262 c.svg.append('line').at({ 9263 x1: 0, x2: c.x(h1), y1: y(i), y2: y(i), 9264 stroke: color, strokeWidth: 1.2, opacity: 0.35}) 9265 c.svg.append('line').at({ 9266 x1: 0, x2: c.x(h2), y1: y(i), y2: y(i), 9267 stroke: color, strokeWidth: 1, opacity: 0.35, strokeDasharray: '3 2'}) 9268 c.svg.append('circle').at({ 9269 cx: c.x(h1), cy: y(i), r: 4, fill: color, stroke: INK, strokeWidth: 0.4}) 9270 var g = c.svg.append('g').translate([c.x(h2), y(i)]) 9271 ;[[-3, -3, 3, 3], [-3, 3, 3, -3]].forEach(([a, b1, e, f2]) => { 9272 g.append('line').at({ 9273 x1: a, y1: b1, x2: e, y2: f2, stroke: color, strokeWidth: 1.3}) 9274 }) 9275 c.svg.append('text.ytick').text(f.func) 9276 .at({x: -5, y: y(i), dy: '0.32em', textAnchor: 'end'}) 9277 }) 9278 9279 var leg = c.svg.append('g.legend').translate([c.width - 64, c.height - 14 * (d.cats.length + 2)]) 9280 d.cats.forEach((cat, i) => { 9281 var g = leg.append('g').translate([0, i * 14]) 9282 g.append('circle').at({r: 4, fill: cat.color, stroke: INK, strokeWidth: 0.4}) 9283 g.append('text').text(cat.name).at({x: 9, dy: '0.32em'}) 9284 }) 9285 ;[['α=1', null, 'o'], ['α=2', '3 2', 'x']].forEach(([lab, dash, m], i) => { 9286 var g = leg.append('g').translate([0, (d.cats.length + i) * 14]) 9287 g.append('line').at({x1: -5, x2: 5, stroke: MUTED, strokeDasharray: dash}) 9288 m == 'o' 9289 ? g.append('circle').at({r: 3.5, fill: '#fff', stroke: MUTED}) 9290 : [[-3, -3, 3, 3], [-3, 3, 3, -3]].forEach(([a, b1, e, f2]) => 9291 g.append('line').at({x1: a, y1: b1, x2: e, y2: f2, stroke: MUTED, strokeWidth: 1.2})) 9292 g.append('text').text(lab).at({x: 9, dy: '0.32em'}) 9293 }) 9294} 9295 9296function drawScatter(sel, d){ 9297 var col = sel.append('div.panel') 9298 col.append('div.mlabel').text('swap effect vs workspace loading') 9299 var catColor = {} 9300 d.cats.forEach(c => catColor[c.name] = c.color) 9301 var xs = d.funcs.map(f => f.x_cos), ys = d.funcs.map(f => f.dlpc) 9302 var xspan = d3.max(xs) - d3.min(xs), yspan = d3.max(ys) - d3.min(ys) 9303 var xlo = d3.min(xs) - 0.06 * xspan, xhi = d3.max(xs) + 0.18 * xspan 9304 var ylo = Math.min(d3.min(ys), 0) - 0.06 * yspan, yhi = d3.max(ys) + 0.08 * yspan 9305 9306 var c = d3.conventions({ 9307 sel: col.append('div'), 9308 width: 330, height: 300, 9309 margin: {left: 52, right: 10, top: 10, bottom: 42}, 9310 layers: 's', 9311 }) 9312 c.x.domain([xlo, xhi]) 9313 c.y.domain([ylo, yhi]) 9314 c.xAxis.ticks(5) 9315 c.yAxis.ticks(5) 9316 c.drawAxis() 9317 c.svg.selectAll('.domain').remove() 9318 c.svg.append('path.spine').at({ 9319 d: `M0,0 V${c.height} H${c.width}`, stroke: '#888', strokeWidth: 0.7, fill: 'none'}) 9320 util.addAxisLabel(c, 9321 'workspace loading of the argument (cosine sim)', 9322 `log-prob shift toward the swapped-in answer (α=${d.scatter_alpha})`, '', 6) 9323 9324 c.svg.append('line').at({ 9325 x1: 0, x2: c.width, y1: c.y(0), y2: c.y(0), stroke: LIGHT, strokeWidth: 0.7}) 9326 var fx = [xlo, xhi] 9327 c.svg.append('line').at({ 9328 x1: c.x(fx[0]), y1: c.y(d.fit.slope * fx[0] + d.fit.intercept), 9329 x2: c.x(fx[1]), y2: c.y(d.fit.slope * fx[1] + d.fit.intercept), 9330 stroke: MUTED, strokeWidth: 1.1, strokeDasharray: '4 3', opacity: 0.55}) 9331 c.svg.append('text.rlabel').text(`r = ${d.r > 0 ? '+' : ''}${d3.format('.2f')(d.r)}`) 9332 .at({x: 8, y: 14}) 9333 9334 d.funcs.forEach(f => { 9335 c.svg.append('circle').at({ 9336 cx: c.x(f.x_cos), cy: c.y(f.dlpc), r: 4.5, 9337 fill: catColor[f.cat], stroke: INK, strokeWidth: 0.4, opacity: 0.9}) 9338 if (NO_LABEL.has(f.func)) return 9339 var [dx, dy, anchor] = NUDGE[f.func] || [5, -2, 'start'] 9340 c.svg.append('text.flabel').text(f.func) 9341 .at({x: c.x(f.x_cos) + dx, y: c.y(f.dlpc) + dy, dy: '0.32em', textAnchor: anchor}) 9342 }) 9343} 9344 9345window.initFlexGeneralizationSystematic = async function(opts){ 9346 var sel = d3.select('.flex-generalization-systematic').html('') 9347 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9348 var d = await util.getFile(base + 'data.json') 9349 var row = sel.append('div.panel-row') 9350 drawDots(row, d) 9351 drawScatter(row, d) 9352} 9353 9354}() 9355window.init?.() 9356 9357// âââ public/capacity-band-vs-single/init-capacity-band-vs-single.js âââ 9358!function(){ 9359 9360var ORANGE = '#ff6b35', BLUE = '#2563eb', GREY = '#9ca3af' 9361 9362function lineChart(sel, title, opts){ 9363 var col = sel.append('div.panel') 9364 col.append('div.mlabel').text(title) 9365 var c = d3.conventions({ 9366 sel: col.append('div'), 9367 width: 270, height: 250, 9368 margin: {left: 46, right: 10, top: 12, bottom: 40}, 9369 layers: 's', 9370 }) 9371 c.x.domain(opts.xdom) 9372 c.y.domain(opts.ydom) 9373 c.xAxis.ticks(5) 9374 c.yAxis.ticks(5) 9375 c.drawAxis() 9376 c.svg.selectAll('.domain').remove() 9377 c.svg.append('path.spine').at({ 9378 d: `M0,0 V${c.height} H${c.width}`, stroke: '#888', strokeWidth: 0.7, fill: 'none'}) 9379 util.addAxisLabel(c, opts.xlabel, opts.ylabel, '', 6) 9380 return c 9381} 9382 9383function band(c, xs, q1, q3, color){ 9384 var area = d3.area().x((v, i) => c.x(xs[i])).y0(v => c.y(v[0])).y1(v => c.y(v[1])) 9385 c.svg.append('path').at({ 9386 d: area(q1.map((v, i) => [v, q3[i]])), fill: color, opacity: 0.10}) 9387} 9388 9389function line(c, xs, ys, color, opts){ 9390 var ln = d3.line().x((v, i) => c.x(xs[i])).y(v => c.y(v)) 9391 c.svg.append('path').at({ 9392 d: ln(ys), stroke: color, fill: 'none', 9393 strokeWidth: opts?.lw ?? 2, strokeDasharray: opts?.dash || null}) 9394} 9395 9396function legend(c, entries){ 9397 var leg = c.svg.append('g.legend').translate([8, 8])
9398 entries.forEach((e, i) => { 9399 var g = leg.append('g').translate([0, i * 14]) 9400 g.append('line').at({x1: 0, x2: 14, stroke: e.color, 9401 strokeWidth: e.lw ?? 2, strokeDasharray: e.dash || null}) 9402 g.append('text').text(e.label).at({x: 18, dy: '0.32em'}) 9403 }) 9404} 9405 9406function drawOcc(sel, d, src, title, ylabel, ylimTop){ 9407 var xs = d3.range(d.ng) 9408 var c = lineChart(sel, title, { 9409 xdom: [0, d.ng - 1], ydom: [0, ylimTop], 9410 xlabel: 'comma #', ylabel, 9411 }) 9412 ;[['family', ORANGE], ['random', BLUE]].forEach(([lab, color]) => { 9413 var s = src.b[lab] 9414 band(c, xs, s.read_q1, s.read_q3, color) 9415 line(c, xs, s.read_med, color) 9416 line(c, xs, s.all_med, color, {lw: 1.5, dash: '4 2'}) 9417 }) 9418 legend(c, [ 9419 {label: 'family: read', color: ORANGE}, 9420 {label: 'family: all 80', color: ORANGE, dash: '4 2', lw: 1.5}, 9421 {label: 'random: read', color: BLUE}, 9422 {label: 'random: all 80', color: BLUE, dash: '4 2', lw: 1.5}, 9423 ]) 9424} 9425 9426function drawThresh(sel, d, src, title, xlabel){ 9427 var c = lineChart(sel, title, { 9428 xdom: [0, 100], ydom: [0, 100], 9429 xlabel, ylabel: '# read words ⤠K (near end of list)', 9430 }) 9431 c.svg.append('line').at({ 9432 x1: 0, x2: c.width, y1: c.y(80), y2: c.y(80), 9433 stroke: GREY, strokeWidth: 1, strokeDasharray: '4 2'}) 9434 c.svg.append('text').text('list length (80)') 9435 .at({x: c.width - 4, y: c.y(80) - 5, textAnchor: 'end', fontSize: 9, fill: '#6b7280'}) 9436 line(c, src.c.ks, src.c.family, ORANGE) 9437 line(c, src.c.ks, src.c.random, BLUE) 9438 line(c, src.c.ks, src.c.control, GREY, {lw: 1.2}) 9439 legend(c, [ 9440 {label: 'family', color: ORANGE}, 9441 {label: 'random', color: BLUE}, 9442 {label: 'control', color: GREY, lw: 1.2}, 9443 ]) 9444} 9445 9446window.initCapacityBandVsSingle = async function(opts){ 9447 var sel = d3.select('.capacity-band-vs-single').html('') 9448 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9449 var d = await util.getFile(base + 'data.json') 9450 // Shared, fixed y-limit for the occupancy panels â matches the main-text 9451 // capacity figure and keeps the legend clear of the curves. 9452 var ylimTop = 50 9453 9454 var r1 = sel.append('div.panel-row') 9455 drawOcc(r1, d, d.band, `words in the readout â any layer of the workspace (${d.band_lbl})`, 9456 `# list words in top-${d.k25} (band-min)`, ylimTop) 9457 drawThresh(r1, d, d.band, 'read words vs rank threshold â any workspace layer', 9458 'rank threshold K (band-min)') 9459 9460 var r2 = sel.append('div.panel-row') 9461 drawOcc(r2, d, d.single, `words in the readout â single layer (${d.single_lbl})`, 9462 `# list words in top-${d.k25}`, ylimTop) 9463 drawThresh(r2, d, d.single, `read words vs rank threshold â single layer (${d.single_lbl})`, 9464 'rank threshold K') 9465} 9466 9467}() 9468window.init?.() 9469 9470// âââ public/app-mt-examples/init-app-mt-examples.js âââ 9471window.initAppMtExamples = async function(opts){ 9472 var sel = d3.select('.app-mt-examples').html('') 9473 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9474 var d = await util.getFile(base + 'data.json') 9475 9476 var JL = util.lensColors.jacobian, TPL = util.tol.orange 9477 var esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 9478 9479 var row = sel.append('div.amx-row') 9480 9481 function barTable(host, label, color, entries){ 9482 var t = host.append('div.amx-bars.bar-table') 9483 var h = t.append('h6') 9484 h.append('span.swatch').st({background: color, marginRight: 5}) 9485 h.append('span').text(label) 9486 var max = d3.max(entries, e => Math.exp(e.lp)) 9487 var bg = util.tint(color, 0.7) 9488 t.appendMany('div.bar-row', entries) 9489 .at({title: e => util.ppCell(e.tok).trim()}) 9490 .html(e => 9491 `<div class="bar" style="width:${(Math.exp(e.lp)/max*100).toFixed(1)}%;background:${bg}"></div>` + 9492 `<span class="t${e.italic ? ' it' : ''}">${esc(util.ppCell(e.tok).trim())}</span>` + 9493 `<span class="val">${e.lp.toFixed(1)}</span>`) 9494 } 9495 9496 // ââ A: blackmail ââ 9497 var bm = d.blackmail 9498 var colA = row.append('div.amx-col') 9499 colA.append('div.fig-title').text('A. Reading out âblackmailâ') 9500 var prA = colA.append('div.amx-prompt.prompt-block') 9501 prA.append('span.ctx').text( 9502 '[in the middle of the blackmail evaluation scenario; ' + 9503 'both lenses are read at the highlighted token]\n') 9504 prA.append('span').text(bm.excerpt.pre) 9505 prA.append('span.tok-hit').text(bm.excerpt.hit) 9506 prA.append('span').text(bm.excerpt.post) 9507 var cA = colA.append('div.amx-cols') 9508 barTable(cA, `Jacobian lens (${bm.layer_label})`, JL, bm.jlens) 9509 barTable(cA, `template lens (${bm.layer_label})`, TPL, bm.template) 9510 9511 // ââ B: photosynthesis ââ 9512 var ph = d.photosynthesis 9513 var colB = row.append('div.amx-col') 9514 colB.append('div.fig-title').text('B. Reading out âphotosynthesisâ') 9515 var prB = colB.append('div.amx-prompt.prompt-block') 9516 prB.append('span').text(ph.prompt_pre) 9517 prB.append('span.tok-hit').text(ph.prompt_hit) 9518 prB.append('span').text(ph.prompt_post) 9519 var cB = colB.append('div.amx-cols') 9520 barTable(cB, `Jacobian lens (${ph.layer_label})`, JL, ph.jlens) 9521 barTable(cB, `template lens (${ph.layer_label})`, TPL, ph.template) 9522 9523 // ââ C: TchaikovskyâBeethoven swap ââ 9524 var colC = row.append('div.amx-col') 9525 colC.append('div.fig-title').text('C. Causal interventions') 9526 d.swaps.forEach(s => { 9527 colC.append('div.amx-prompt.prompt-block').text(s.prompt) 9528 var sw = colC.append('div.amx-swp') 9529 var r1 = sw.append('div.amx-row2') 9530 r1.append('span.lab').text("model's answer:") 9531 r1.append('span').append('span.out').append('b').text(s.ans) 9532 var r2 = sw.append('div.amx-row2') 9533 r2.append('span.lab').st({color: TPL}) 9534 .html(`swap <i>${esc(s.src)}</i>→<i>${esc(s.dst)}</i> templates:`) 9535 r2.append('span').append('span.out.hi').text(s.swapped) 9536 }) 9537} 9538 9539window.init?.() 9540 9541// âââ public/oracle-lens/init-oracle-lens.js âââ 9542window.initOracleLens = async function(opts){ 9543 var sel = d3.select('.oracle-lens').html('') 9544 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9545 var d = await util.getFile(base + 'data.json') 9546 9547 var grid = sel.append('div.ol-grid.figgrid')
9548 d.panels.forEach(p => { 9549 var card = grid.append('div.cell.ol-card') 9550 var hd = card.append('div.ol-hd') 9551 hd.append('span.ol-id').text(p.id) 9552 hd.append('span.fig-title').text(p.title) 9553 9554 var body = card.append('div.ol-body') 9555 var pb = body.append('div.prompt-block' + (p.nowrap ? '.ol-nowrap' : '')) 9556 if (p.ctx) pb.append('span.ctx').text(p.ctx + '\n') 9557 if (p.prompt.elide_lo && !p.suppress_lead_elision) 9558 pb.append('span.ctx').text('[â¦]\n') 9559 pb.append('span').text(p.prompt.pre) 9560 pb.append('span.tok-hit').text(p.prompt.hit) 9561 pb.append('span').text(p.prompt.post) 9562 if (p.prompt.elide_hi) pb.append('span.ctx').text('\n[â¦]') 9563 9564 var pcol = body.append('div.ol-pcol') 9565 pcol.append('div.mlabel') 9566 .html('Oracle lens <span class="ol-sub">(N=4, K=10)</span>') 9567 var bold = new Set(p.bold) 9568 pcol.append('ol.ol-phrases') 9569 .appendMany('li', p.phrases) 9570 .classed('bold', (_, i) => bold.has(i)) 9571 .text(ph => ph) 9572 }) 9573} 9574 9575window.init?.() 9576 9577// âââ public/app-mt-bytok/init-app-mt-bytok.js âââ 9578window.initAppMtBytok = async function(opts){ 9579 var sel = d3.select('.app-mt-bytok').html('') 9580 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9581 var d = await util.getFile(base + 'data.json') 9582 9583 var styleOf = { 9584 'jlens': {color: util.lensColors.jacobian, dash: null, marker: 'o'}, 9585 'jlens-dash': {color: util.lensColors.jacobian, dash: '4,3', marker: 'o-open'}, 9586 'template': {color: util.tol.orange, dash: null, marker: 's'}, 9587 'raw': {color: util.gray[500], dash: null, marker: 'd'}, 9588 } 9589 9590 var row = sel.append('div.panel-row').st({flexDirection: 'row'}) 9591 drawPanel(row.append('div.app-mt-panel'), d.readout, d.n_readout, 9592 'Decoding the intermediate concept', 'fraction in top 10', 9593 'tokens in the latent word') 9594 drawPanel(row.append('div.app-mt-panel'), d.swap, d.n_swap, 9595 'Swapping the intermediate concept', 9596 'P(answer follows the swap) at α = 1', 9597 'tokens in the anchor word') 9598 9599 var leg = sel.append('div.app-mt-legend') 9600 d.readout.forEach(s => { 9601 var st = styleOf[s.style] 9602 var item = leg.append('span.leg-item') 9603 item.append('svg').at({width: 24, height: 12}).append('line') 9604 .at({x1: 0, x2: 24, y1: 6, y2: 6, stroke: st.color, strokeWidth: 1.6, 9605 strokeDasharray: st.dash || null}) 9606 item.append('span').text(s.label) 9607 }) 9608 9609 function drawPanel(sel, series, ns, title, ylab, xlab){ 9610 var c = d3.conventions({ 9611 sel, width: 340, height: 240, 9612 margin: {left: 56, right: 12, top: 30, bottom: 44}, 9613 }) 9614 var x = d3.scalePoint().domain(d.buckets).range([0, c.width]).padding(0.2) 9615 c.y.domain([-0.03, 1.04]) 9616 c.yAxis.ticks(5).tickFormat(d3.format('.0%')) 9617 c.drawAxis() 9618 c.svg.select('.x').remove() 9619 util.ggPlot(c) 9620 util.addAxisLabel(c, xlab, ylab, title, 0, -8) 9621 9622 c.svg.appendMany('text.xlab', d.buckets) 9623 .translate((b, i) => [x(b), c.height + 14]) 9624 .at({textAnchor: 'middle'}).text(b => b) 9625 c.svg.appendMany('text.xlab-n', d.buckets) 9626 .translate((b, i) => [x(b), c.height + 26]) 9627 .at({textAnchor: 'middle', fill: util.gray[500]}) 9628 .style('font-size', '10px') 9629 .text((b, i) => '(n=' + ns[i] + ')') 9630 9631 var nM = series.length 9632 var off = i => (i - (nM - 1)/2) * 6 9633 series.forEach((s, i) => { 9634 var st = styleOf[s.style] 9635 var pts = s.points.map((p, j) => ({...p, xi: x(p.bucket) + off(i)})) 9636 c.svg.append('path') 9637 .at({d: d3.line().x(p => p.xi).y(p => c.y(p.p))(pts), 9638 stroke: st.color, strokeWidth: 1.6, fill: 'none', 9639 strokeDasharray: st.dash || null}) 9640 var g = c.svg.appendMany('g.pt', pts).translate(p => [p.xi, 0]) 9641 g.append('line') 9642 .at({y1: p => c.y(p.lo), y2: p => c.y(p.hi), 9643 stroke: st.color, strokeWidth: 1.0}) 9644 g.append('circle') 9645 .at({cy: p => c.y(p.p), r: 3.2, 9646 fill: st.marker == 'o-open' ? '#fff' : st.color, 9647 stroke: st.color, strokeWidth: 1.2}) 9648 g.append('title').text(p => `${s.label}\n${p.bucket} tok: ${p.k}/${p.n} (${d3.format('.0%')(p.p)})`) 9649 }) 9650 } 9651} 9652 9653// âââ public/app-mt-paper/init-app-mt-paper.js âââ 9654window.initAppMtPaper = async function(opts){ 9655 var sel = d3.select('.app-mt-paper').html('') 9656 var base = (opts?.datapath || './').replace(/\/?$/, '/') 9657 var d = await util.getFile(base + 'data.json') 9658 9659 var colorOf = { 9660 'jlens': util.lensColors.jacobian, 9661 'jlens-dash': util.lensColors.jacobian, 9662 'template': util.tol.orange, 9663 'raw': util.gray[500], 9664 } 9665 9666 var row = sel.append('div.panel-row').st({flexDirection: 'row'}) 9667 drawBars(row.append('div.app-mt-panel'), d.readout, 9668 'Decoding the intermediate concept', 'fraction in top 10') 9669 drawBars(row.append('div.app-mt-panel'), d.swap, 9670 'Swapping the intermediate concept', 9671 'P(answer follows the swap) at α = 1') 9672 sel.append('div.app-mt-n').text('n = ' + d.n) 9673 9674 function drawBars(sel, series, title, ylab){ 9675 var c = d3.conventions({ 9676 sel, width: 240, height: 220, 9677 margin: {left: 56, right: 12, top: 30, bottom: 18}, 9678 }) 9679 var x = d3.scaleBand().domain(series.map(s => s.key)) 9680 .range([0, c.width]).paddingInner(0.30).paddingOuter(0.18) 9681 c.y.domain([-0.03, 1.04]) 9682 c.yAxis.ticks(5).tickFormat(d3.format('.0%')) 9683 c.drawAxis() 9684 c.svg.select('.x').remove() 9685 util.ggPlot(c) 9686 util.addAxisLabel(c, '', ylab, title, 0, -8) 9687 9688 var g = c.svg.appendMany('g.mbar', series).translate(s => [x(s.key), 0]) 9689 g.append('rect').at({ 9690 width: x.bandwidth(), y: s => c.y(s.p), height: s => c.height - c.y(s.p), 9691 fill: s => colorOf[s.style], fillOpacity: 0.30, 9692 }) 9693 g.each(function(s){ 9694 util.barWhisker(d3.select(this), {x: x.bandwidth()/2, lo: c.y(s.lo), hi: c.y(s.hi)}) 9695 }) 9696 g.append('title').text(s => `${s.label}: ${s.k}/${s.n} (${d3.format('.0%')(s.p)})`) 9697 } 9698 9699 var leg = sel.append('div.app-mt-legend')
9700 d.swap.forEach(s => { 9701 var item = leg.append('span.leg-item') 9702 item.append('span.swatch').st({background: colorOf[s.style], opacity: 0.30}) 9703 item.append('span').text(s.label) 9704 }) 9705} 9706 9707// âââ public/init.js âââ 9708addEventListener('scroll', function(){ 9709 var b = document.querySelector('.draft-banner') 9710 if (b) b.classList.toggle('scrolled', scrollY > 120) 9711 // Floating TOC dropdown appears once the in-page contents has scrolled 9712 // off the top. 9713 var dc = document.querySelector('d-contents') 9714 var t = document.querySelector('.toc-float') 9715 if (t) t.classList.toggle('show', !dc || !dc.offsetHeight || dc.getBoundingClientRect().bottom < 0) 9716 window.updateTocFloatLabel() 9717}, {passive: true}) 9718 9719// Floating-TOC button label tracks the current section â the last TOC-listed 9720// heading above the viewport top, §-numbered like the d-contents entries. 9721// 'Contents' before the first section. 9722window.updateTocFloatLabel = function(){ 9723 var lbl = document.querySelector('.toc-float-btn .lbl') 9724 if (!lbl) return 9725 var cur = null, heads = window.__tocHeadings || [] 9726 for (var i = 0; i < heads.length && heads[i].getBoundingClientRect().top <= 80; i++) cur = heads[i] 9727 if (!cur){ lbl.textContent = 'Contents'; return } 9728 lbl.textContent = '' 9729 var num = lbl.appendChild(document.createElement('span')) 9730 num.className = 'num' 9731 num.textContent = '§ ' + cur.dataset.secnum + ' ' 9732 lbl.appendChild(document.createTextNode(cur.querySelector('a[id]').textContent)) 9733} 9734 9735window.buildToc = function(){ 9736 var nav = d3.select('d-contents').html('').append('nav') 9737 if (!nav.size()) return 9738 nav.append('h3').text('Contents') 9739 var ul, n = [0, 0, 0], h2 = 0, appx = 0 9740 window.__secNum = {} 9741 function entry(parent, num, id, text){ 9742 var a = parent.append('a').at({href: '#' + id}) 9743 a.append('span.toc-num').text(num) 9744 a.append('span').text(text) 9745 } 9746 // gdoc H1/H2/H3 â html h2/h3/h4 (process_doc remaps down one). TOC lists 9747 // h2+h3; h4 is numbered (N.M.K, \ref{}-able via __secNum) but not listed. 9748 d3.selectAll('d-article h2, d-article h3, d-article h4').each(function(){ 9749 var a = this.querySelector('a[id]') 9750 if (!a) return 9751 var num 9752 if (this.tagName == 'H2'){ 9753 // First H2 with id 'appendix' (and any later H2s) switch to letter 9754 // numbering: A, A.1, A.1.2. n[0] becomes a string; the '.'-concat 9755 // and n.join('.') below both handle that without further changes. 9756 n = (a.id == 'appendix' || appx) 9757 ? [String.fromCharCode(65 + appx++), 0, 0] 9758 : [++h2, 0, 0] 9759 num = window.__secNum[a.id] = '' + n[0] 9760 entry(nav.append('div'), num, a.id, a.textContent) 9761 ul = nav.append('ul') 9762 } else if (this.tagName == 'H3' && n[0]){ 9763 n[1] += 1; n[2] = 0 9764 num = window.__secNum[a.id] = n[0] + '.' + n[1] 9765 entry(ul.append('li'), num, a.id, a.textContent) 9766 } else if (this.tagName == 'H4' && n[1]){ 9767 n[2] += 1 9768 num = window.__secNum[a.id] = n.join('.') 9769 } 9770 // Prefix the number as a real <a> so it's clickable (scrolls + sets hash); 9771 // sits before the existing a[id] so a.textContent stays number-free for the 9772 // TOC entry above. 9773 if (num){ 9774 this.dataset.secnum = num 9775 d3.select(this).selectAll('a.secnum, a\\.secnum').remove() 9776 d3.select(this).insert('a', ':first-child') 9777 .at({class: 'secnum', href: '#' + a.id}).text(num) 9778 } 9779 }) 9780 9781 // Visual-TOC tiles (server-side from [[VISUAL_TOC]]) get the same §-number 9782 // via __secNum so the grid stays in sync with d-contents numbering. 9783 d3.selectAll('.visual-toc a.visual-toc-top').each(function(){ 9784 var slug = (this.getAttribute('href') || '').slice(1) 9785 var num = window.__secNum[slug] 9786 if (num) d3.select(this).select('.toc-num').text('§ ' + num) 9787 }) 9788 9789 // Floating top-left TOC dropdown â same entries as <d-contents>, shown 9790 // (via the scroll handler above) once the in-page contents has scrolled 9791 // away. The clone keeps the toc-num spans so numbering matches. 9792 d3.select('.toc-float').remove() 9793 var float = d3.select('body').append('div.toc-float') 9794 float.append('button.toc-float-btn') 9795 .at({'aria-label': 'Table of contents'}) 9796 .html('<span class="ico">â°</span><span class="lbl">Contents</span>') 9797 .on('click', function(){ float.classed('open', !float.classed('open')) }) 9798 var menu = float.append('div.toc-float-menu') 9799 menu.node().appendChild(nav.node().cloneNode(true)) 9800 menu.on('click', function(e){ 9801 if (e.target.closest('a')) float.classed('open', false) 9802 }) 9803 d3.select(document).on('click.tocfloat', function(e){ 9804 if (!float.node().contains(e.target)) float.classed('open', false) 9805 }) 9806 window.__tocHeadings = d3.selectAll('d-article h2[data-secnum], d-article h3[data-secnum]').nodes() 9807 window.updateTocFloatLabel() 9808} 9809 9810window.init = async function(){ 9811 var token = window.__initToken = {} 9812 if (!d3.select('.tooltip').size()) d3.select('body').append('div.tooltip.tooltip-hidden') 9813 window.buildToc() 9814 // figure data-fignum is server-side and __secNum is built above â resolve now 9815 // so refs don't sit at "??" through the (slow) figure data fetches below. 9816 util.resolveFigRefs() 9817 d3.selectAll('figure[data-fignum]').each(function(){ 9818 var m = this.firstElementChild 9819 if (m && m.offsetWidth) this.style.setProperty('--mount-w', m.offsetWidth + 'px') 9820 }) 9821 util.fitFigures() 9822 await Promise.all([ 9823 [window.initIntroFunctional, 'intro-functional'], 9824 // The first panel caption (Multihop recall) carries "Figure N:"; the init 9825 // hides the gdoc figcaption's number so it appears exactly once. 9826 [window.initLensInline, 'lens-inline'], 9827 // No figIntro preamble â the first panel caption stays as short as its siblings 9828 // ("Figure N:" prefix + the spider/legs/8 sentence only). 9829 [opts => window.initLensInline({...opts, sel: '.lens-inline-reasoning', sliceLink: false}), 'lens-inline-reasoning'], 9830 [opts => window.initSliceStack({...opts, sel: '.slice-stack-count', slugs: 'count_introspect'}), 'slice-stack-count', 'lens-slice-ranks-s2'], 9831 [opts => window.initSliceInline({...opts, sel: '.slice-inline-count', slugs: 'count_five'}), 'slice-inline-count', 'lens-slice-ranks-s2'], 9832 [opts => window.initSliceInline({...opts, sel: '.slice-inline-introspection', slugs: 'introspection'}), 'slice-inline-introspection', 'lens-slice-ranks-s2'], 9833 [window.initLensSlice, 'lens-slice', 'lens-slice-ranks-s2'], 9834 [window.initOooAbcd, 'ooo-abcd'], 9835 [window.initOooPatch, 'ooo-patch'], 9836 [window.initFeatureExamples, 'feature-examples'], 9837 [window.initFeatureCoverage, 'feature-coverage'], 9838 [window.initCompLines, 'comp-lines-attn-adj', 'comp-lines'], 9839 [opts => window.initCompStrata({...opts, variant: 'sae6'}), 'comp-strata', 'comp-strata'], 9840 [opts => window.initCompStrata({...opts, variant: 'nosae'}), 'comp-strata-nosae', 'comp-strata'], 9841 [window.initCompTailEnergy, 'comp-tail-energy'], 9842 [window.initMlpGain, 'mlp-gain'], 9843 [window.initLensSimilarity, 'lens-similarity'], 9844 [window.initTranscoderArith, 'transcoder-arith'], 9845 [window.initTranscoderTranslation, 'transcoder-translation'], 9846 [window.initJlensCircuitGraph, 'jlens-circuit-graph'], 9847 [window.initAttnPanel, 'attn-panel'], 9848 [window.initAttnPanelAppendix, 'attn-panel-appendix'], 9849 [window.initAttnBroadcastReselect, 'attn-broadcast-reselect'], 9850 [window.initAblationBars, 'ablation-bars'], 9851 [window.initAblationExamples, 'ablation-examples'], 9852 [window.initAblationStrength, 'ablation-strength'], 9853 [window.initBlackmailClamp, 'blackmail-clamp'], 9854 [opts => window.initLensCalloutMain({...opts, sel: '.lens-callout-blackmail', fig: 'blackmail'}), 9855 'lens-callout-blackmail', 'lens-callout-main/sonnet45'], 9856 [opts => window.initLensCalloutMain({...opts, sel: '.lens-callout-evil-anthropic', fig: 'evil_anthropic'}), 9857 'lens-callout-evil-anthropic', 'lens-callout-main/opus45'], 9858 [opts => window.initLensappPanel({...opts, slug: 'eval_aware'}), 'lensapp-eval-aware', 'lensapp-panel'], 9859 [window.initMisalignLens, 'misalign-lens'], 9860 [window.initMisalignLensExample, 'misalign-lens-example', 'misalign-lens'], 9861 [window.initRewardHackReadout, 'reward-hack-readout', 'misalign-lens'], 9862 [window.initRewardHackQuant, 'reward-hack-quant', 'misalign-lens'], 9863 [window.initRoleplayLens, 'roleplay-lens'], 9864 [window.initRoleplayLensExample, 'roleplay-lens-example', 'roleplay-lens'], 9865 [window.initJlensRmBias, 'jlens-rm-bias'], 9866 [window.initJlensAudit, 'jlens-audit'], 9867 [window.initJlensRmBiasExamples, 'jlens-rm-bias-examples'], 9868 [window.initPrefViolationLens, 'pref-violation-lens'], 9869 [window.initReflectionTraining, 'reflection-training'], 9870 [window.initDnTracecond, 'dn-tracecond'], 9871 [window.initDnExclusion, 'dn-exclusion'], 9872 [opts => window.initReflectionTraining({...opts, sel: '.reflection-fabrication', 9873 figTitle: 'Fabrication admission eval'}), 'reflection-fabrication'], 9874 [opts => window.initReflectionTraining({...opts, sel: '.reflection-deception', 9875 figTitle: 'Deception refusal eval'}), 'reflection-deception'], 9876 [window.initReflTrainingExamples, 'refl-training-examples'], 9877 [window.initModulationReadout, 'modulation-readout'],
9878 [window.initModulationLines, 'modulation-lines'], 9879 [window.initModulationPrompts, 'modulation-prompts'], 9880 [window.initMethodsQualitative, 'methods-qualitative'], 9881 [window.initLatentPatching, 'latent-patching'], 9882 [opts => window.initLatentPatching({...opts, sel: '.flex-gen-example'}), 'flex-gen-example'], 9883 [window.initMultihopSwapSuccess, 'multihop-swap-success'], 9884 [window.initRepeatSwitch, 'repeat-switch'], 9885 [window.initVerbalReport, 'verbal-report'], 9886 [window.initVerbalIntrospection, 'verbal-introspection'], 9887 [window.initTopDownSummoning, 'top-down-summoning'], 9888 [opts => window.initTopDownSummoning({...opts, sel: '.top-down-summoning-appendix', spec: 'appendix'}), 'top-down-summoning-appendix', 'top-down-summoning'], 9889 [window.initFlexGenSystematic, 'flex-gen-systematic'], 9890 [opts => window.initFlexGenSystematic({...opts, sel: '.flex-gen-appendix', spec: 'appendix'}), 'flex-gen-appendix', 'flex-gen-systematic'], 9891 [window.initSelectivityLinecount, 'selectivity-linecount'], 9892 [window.initSelectivityLanguage, 'selectivity-language'], 9893 [opts => window.initPostTraining({...opts, sel: '.post-training-tylenol', spec: 'tylenol'}), 'post-training-tylenol', 'post-training'], 9894 [opts => window.initPostTraining({...opts, sel: '.post-training-danger', spec: 'danger'}), 'post-training-danger', 'post-training'], 9895 [opts => window.initPostTraining({...opts, sel: '.post-training-condolences', spec: 'condolences'}), 'post-training-condolences', 'post-training'], 9896 [opts => window.initPostTraining({...opts, sel: '.post-training-introspection', spec: 'introspection'}), 'post-training-introspection', 'post-training'], 9897 [window.initIntroStructural, 'intro-structural'], 9898 [window.initLayerDiagram, 'layer-diagram'], 9899 [opts => window.initLinePanels({...opts, spec: 'main'}), 'layer-lines-main', 'layer-lines'], 9900 [opts => window.initLinePanels({...opts, spec: 'appendix'}), 'layer-lines-appendix', 'layer-lines'], 9901 [window.initCapacityFveOccupancy, 'capacity-fve-occupancy'], 9902 [window.initBroadcastAblation, 'broadcast-ablation'], 9903 [window.initModulationProbe, 'modulation-probe'], 9904 [window.initProbeSwap, 'probe-swap'], 9905 [window.initVerbalReportDecompositionMerged, 'verbal-report-decomposition-merged'], 9906 // The six selfreport figures share one data dir. 9907 [window.initSelfreportSoc, 'selfreport-soc', 'selfreport'], 9908 [window.initSelfreportOther, 'selfreport-other', 'selfreport'], 9909 [window.initSelfreportQuestions, 'selfreport-questions', 'selfreport'], 9910 [window.initSelfreportStory, 'selfreport-story', 'selfreport'], 9911 [window.initSelfreportExamples, 'selfreport-examples', 'selfreport'], 9912 [window.initSelfreportDose, 'selfreport-dose', 'selfreport'], 9913 [window.initSelfreportControls, 'selfreport-controls', 'selfreport'], 9914 [window.initMetacogAlarm, 'metacog-alarm'], 9915 [window.initIgnition, 'ignition'], 9916 [opts => window.initIgnition({...opts, sel: '.ignition-d', show: 'd'}), 'ignition-d', 'ignition'], 9917 [opts => window.initIgnition({...opts, sel: '.ignition-e', show: 'e'}), 'ignition-e', 'ignition'], 9918 [window.initEvalAwarenessProbe, 'eval-awareness-probe'], 9919 [window.initDualTaskSimple, 'dual-task-simple'], 9920 [window.initCapacityFinalBand, 'capacity-final-band'], 9921 [window.initFlexGenExample, 'flex-generalization-example'], 9922 [window.initFlexGeneralizationSystematic, 'flex-generalization-systematic'], 9923 [window.initCapacityBandVsSingle, 'capacity-band-vs-single'], 9924 [window.initAppMtExamples, 'app-mt-examples'], 9925 [window.initAppMtBytok, 'app-mt-bytok'], 9926 [window.initAppMtPaper, 'app-mt-paper'], 9927 [window.initOracleLens, 'oracle-lens'], 9928 ].map(async ([fn, dp, dataDp]) => { 9929 var mount = document.querySelector('.' + dp) 9930 if (!mount || !fn) return 9931 var figNum = +mount.closest('figure')?.dataset.fignum || null 9932 try { await fn({datapath: `data/${dataDp || dp}/`, figNum}) } catch(e){ console.warn(dp, e) } 9933 if (token !== window.__initToken) return 9934 util.fitMount(mount) 9935 })) 9936 if (token !== window.__initToken) return 9937 util.resolveFigRefs() 9938 util.fitFigures() 9939} 9940window.init()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.