1/* ======================================== 2 BIODYN, About, symbolic figures 3 Each figure has to read as the thing it argues, 4 not as an abstract cloud: 5 6 I a small organism, traced end to end 7 II an opaque output surface, cut open to 8 show the interior it hides 9 III disordered internals on one side, a 10 double helix drawn out of them 11 12 and one figure per application area, so the 13 detail view can morph a level deeper. 14 ======================================== */ 15 16(() => { 17 'use strict'; 18 19 const A = window.BiodynAbout; 20 if (!A) return; 21 22 const TAU = Math.PI * 2; 23 24 /* Places graph nodes first, then scatters the remaining budget along the 25 edges so the wiring reads as drawn rather than implied. */ 26 const fromGraph = (nodes, edges, n, options) => { 27 const opts = options || {}; 28 const rand = opts.rand || A.makeRandom(1); 29 const jitter = opts.jitter === undefined ? 0.006 : opts.jitter; 30 const fill = opts.fill === undefined ? 0.3 : opts.fill; 31 const pos = new Float32Array(n * 3); 32 const intensity = new Float32Array(n); 33 const nodeCount = Math.min(nodes.length, n); 34 35 for (let i = 0; i < nodeCount; i += 1) { 36 pos[i * 3] = nodes[i][0]; 37 pos[i * 3 + 1] = nodes[i][1]; 38 pos[i * 3 + 2] = nodes[i][2]; 39 intensity[i] = opts.nodeIntensity ? opts.nodeIntensity(i) : 0.95; 40 } 41 42 const edgeCount = edges.length / 2; 43 for (let i = nodeCount; i < n; i += 1) { 44 const e = edgeCount ? (i - nodeCount) % edgeCount : 0; 45 const a = nodes[edges[e * 2]] || [0, 0, 0]; 46 const b = nodes[edges[e * 2 + 1]] || [0, 0, 0]; 47 const t = rand(); 48 pos[i * 3] = a[0] + (b[0] - a[0]) * t + A.gaussian(rand) * jitter; 49 pos[i * 3 + 1] = a[1] + (b[1] - a[1]) * t + A.gaussian(rand) * jitter; 50 pos[i * 3 + 2] = a[2] + (b[2] - a[2]) * t + A.gaussian(rand) * jitter; 51 intensity[i] = typeof fill === 'function' ? fill(i) : fill; 52 } 53 54 return { pos, intensity, edges: nodeCount >= nodes.length ? edges : null }; 55 }; 56 57 /* ======================================= 58 I, A small organism, traced end to end 59 ======================================= */ 60 61 const modelOrganism = (n) => { 62 const rand = A.makeRandom(31337); 63 const CORD = 34; 64 const GANGLIA = 22; 65 const nodes = []; 66 const edges = []; 67 68 // Tapered body profile, fatter at the head. 69 const radiusAt = (t) => 0.42 * Math.pow(Math.sin(Math.PI * Math.min(1, t * 1.06)), 0.62) 70 * (1 + 0.42 * Math.exp(-Math.pow((t - 0.16) / 0.16, 2))); 71 72 // Dorsal nerve cord down the length of the animal. 73 for (let i = 0; i < CORD; i += 1) { 74 const t = i / (CORD - 1); 75 nodes.push([(t - 0.5) * 1.68, radiusAt(t) * 0.42 + 0.02, 0]); 76 if (i > 0) edges.push(i - 1, i); 77 } 78 79 // Ganglia hanging off the cord, alternating left and right. 80 for (let g = 0; g < GANGLIA; g += 1) { 81 const anchor = 2 + Math.floor((g / GANGLIA) * (CORD - 4)); 82 const side = g % 2 === 0 ? 1 : -1; 83 const t = anchor / (CORD - 1); 84 const r = radiusAt(t); 85 const index = nodes.length; 86 nodes.push([ 87 (t - 0.5) * 1.68 + A.gaussian(rand) * 0.02, 88 r * -0.24, 89 side * r * 0.82 90 ]); 91 edges.push(anchor, index); 92 if (g >= 2) edges.push(index - 2, index); 93 } 94 95 const built = fromGraph(nodes, edges, n, { 96 rand, 97 jitter: 0.008, 98 nodeIntensity: (i) => (i < CORD ? 1 : 0.82), 99 fill: 0.4 100 }); 101 102 // Body wall, dim, so the silhouette of the animal is legible. 103 const surfaceStart = Math.min(nodes.length, n) + Math.floor((n - nodes.length) * 0.34); 104 for (let i = surfaceStart; i < n; i += 1) { 105 const t = rand(); 106 const a = rand() * TAU; 107 const r = radiusAt(t); 108 built.pos[i * 3] = (t - 0.5) * 1.68; 109 built.pos[i * 3 + 1] = Math.sin(a) * r; 110 built.pos[i * 3 + 2] = Math.cos(a) * r; 111 built.intensity[i] = 0.24; 112 } 113 114 A.fit(built.pos, n, 1); 115 return built; 116 }; 117 118 /* ======================================= 119 II, Output surface, cut open 120 ======================================= */ 121 122 const cutawayAudit = (n) => { 123 const rand = A.makeRandom(556677); 124 const CORE_NODES = 46; 125 const nodes = []; 126 const edges = []; 127 128 // A compact, wired interior: organised knowledge, not noise. 129 for (let i = 0; i < CORE_NODES; i += 1) { 130 const phi = Math.acos(1 - (2 * (i + 0.5)) / CORE_NODES); 131 const theta = Math.PI * (1 + Math.sqrt(5)) * (i + 0.5); 132 const r = 0.28 + rand() * 0.2; 133 nodes.push([ 134 r * Math.sin(phi) * Math.cos(theta), 135 r * Math.cos(phi), 136 r * Math.sin(phi) * Math.sin(theta) 137 ]); 138 } 139 for (let i = 0; i < CORE_NODES; i += 1) { 140 for (let j = i + 1; j < CORE_NODES; j += 1) { 141 const d = Math.hypot( 142 nodes[i][0] - nodes[j][0], 143 nodes[i][1] - nodes[j][1], 144 nodes[i][2] - nodes[j][2] 145 ); 146 if (d < 0.31) edges.push(i, j); 147 } 148 } 149 150 const built = fromGraph(nodes, edges, n, { 151 rand, 152 jitter: 0.005, 153 nodeIntensity: () => 1, 154 fill: 0.55 155 }); 156 157 // The shell is the output surface. A wedge is missing, so you can see that 158 // there is far more inside than the surface ever shows.
159 const shellStart = Math.floor(n * 0.46); 160 const GAP_FROM = -0.62; 161 const GAP_TO = 0.72; 162 for (let i = shellStart; i < n; i += 1) { 163 let theta = 0; 164 let phi = 0; 165 // Rejection-sample the sphere minus the wedge. 166 for (let tries = 0; tries < 24; tries += 1) { 167 const k = rand(); 168 phi = Math.acos(1 - 2 * k); 169 theta = rand() * TAU - Math.PI; 170 if (theta < GAP_FROM || theta > GAP_TO) break; 171 } 172 const r = 1 + A.gaussian(rand) * 0.01; 173 built.pos[i * 3] = r * Math.sin(phi) * Math.cos(theta); 174 built.pos[i * 3 + 1] = r * Math.cos(phi); 175 built.pos[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta); 176 built.intensity[i] = 0.26; 177 } 178 179 A.fit(built.pos, n, 1); 180 return built; 181 }; 182 183 /* ======================================= 184 III, A helix drawn out of the internals 185 ======================================= */ 186 187 const extraction = (n) => { 188 const rand = A.makeRandom(8080808); 189 const TURNS = 2.6; 190 const RUNGS = 26; 191 const nodes = []; 192 const edges = []; 193 const strandA = []; 194 const strandB = []; 195 196 // Ordered structure, extracted: a double helix standing on the right. 197 for (let i = 0; i < RUNGS; i += 1) { 198 const t = i / (RUNGS - 1); 199 const angle = t * TAU * TURNS; 200 const y = -0.86 + t * 1.72; 201 const x = 0.52; 202 strandA.push(nodes.length); 203 nodes.push([x + Math.cos(angle) * 0.26, y, Math.sin(angle) * 0.26]); 204 strandB.push(nodes.length); 205 nodes.push([x - Math.cos(angle) * 0.26, y, -Math.sin(angle) * 0.26]); 206 edges.push(strandA[i], strandB[i]); 207 if (i > 0) { 208 edges.push(strandA[i - 1], strandA[i]); 209 edges.push(strandB[i - 1], strandB[i]); 210 } 211 } 212 213 const built = fromGraph(nodes, edges, n, { 214 rand, 215 jitter: 0.005, 216 nodeIntensity: () => 0.95, 217 fill: 0.5 218 }); 219 220 const helixBudget = Math.floor(n * 0.52); 221 222 // The internals it was read out of: a diffuse, unordered mass on the left. 223 const cloudEnd = Math.floor(n * 0.84); 224 for (let i = helixBudget; i < cloudEnd; i += 1) { 225 built.pos[i * 3] = -0.78 + A.gaussian(rand) * 0.29; 226 built.pos[i * 3 + 1] = -0.04 + A.gaussian(rand) * 0.34; 227 built.pos[i * 3 + 2] = A.gaussian(rand) * 0.29; 228 built.intensity[i] = 0.24; 229 } 230 231 // The stream between them: the extraction itself. 232 for (let i = cloudEnd; i < n; i += 1) { 233 const t = rand(); 234 const x = -0.5 + t * 0.98; 235 const spread = 0.2 * (1 - t) + 0.03; 236 built.pos[i * 3] = x; 237 built.pos[i * 3 + 1] = -0.05 + A.gaussian(rand) * spread + Math.sin(t * 2.6) * 0.06; 238 built.pos[i * 3 + 2] = A.gaussian(rand) * spread; 239 built.intensity[i] = 0.34 + t * 0.5; 240 } 241 242 A.fit(built.pos, n, 1); 243 return built; 244 }; 245 246 /* ======================================= 247 Application figures 248 ======================================= */ 249 250 /* Layered, directed: regulators above, targets below. */ 251 const grnFigure = (n) => { 252 const rand = A.makeRandom(4242); 253 const LAYERS = [9, 14, 18]; 254 const nodes = []; 255 const edges = []; 256 const layerIndex = []; 257 258 LAYERS.forEach((size, l) => { 259 const y = 0.72 - l * 0.72; 260 const ring = []; 261 for (let i = 0; i < size; i += 1) { 262 const a = (i / size) * TAU + l * 0.4; 263 const r = 0.34 + l * 0.24; 264 ring.push(nodes.length); 265 nodes.push([Math.cos(a) * r, y + A.gaussian(rand) * 0.03, Math.sin(a) * r]); 266 } 267 layerIndex.push(ring); 268 }); 269 270 for (let l = 0; l < layerIndex.length - 1; l += 1) { 271 layerIndex[l].forEach((from) => { 272 const below = layerIndex[l + 1]; 273 const fanout = 2 + Math.floor(rand() * 2); 274 for (let k = 0; k < fanout; k += 1) { 275 edges.push(from, below[Math.floor(rand() * below.length)]); 276 } 277 }); 278 } 279 // A few skip edges, because real networks are not tidy. 280 for (let k = 0; k < 6; k += 1) { 281 edges.push( 282 layerIndex[0][Math.floor(rand() * layerIndex[0].length)], 283 layerIndex[2][Math.floor(rand() * layerIndex[2].length)] 284 ); 285 } 286 287 const built = fromGraph(nodes, edges, n, { 288 rand, 289 nodeIntensity: (i) => (i < LAYERS[0] ? 1 : 0.7), 290 fill: 0.3 291 }); 292 A.fit(built.pos, n, 1); 293 return built; 294 }; 295 296 /* A long trajectory that thins with time, and the points that hold. */ 297 const longevityFigure = (n) => { 298 const rand = A.makeRandom(19191); 299 const STEPS = 78; 300 const nodes = []; 301 const edges = []; 302 303 for (let i = 0; i < STEPS; i += 1) { 304 const t = i / (STEPS - 1); 305 const angle = t * TAU * 3.1; 306 const r = 0.52 - t * 0.33; 307 nodes.push([(t - 0.5) * 1.9, Math.sin(angle) * r, Math.cos(angle) * r]); 308 if (i > 0) edges.push(i - 1, i); 309 } 310 311 const built = fromGraph(nodes, edges, n, { 312 rand, 313 jitter: 0.007, 314 // Signal decays along the trajectory; a periodic few stay bright. 315 nodeIntensity: (i) => (i % 9 === 0 ? 1 : 0.9 - (i / STEPS) * 0.62), 316 fill: (i) => 0.34 - (i / n) * 0.12 317 }); 318 A.fit(built.pos, n, 1); 319 return built; 320 }; 321 322 /* One neuron: soma, dendritic arbor above, axon and terminals below. */ 323 const neuroFigure = (n) => { 324 const rand = A.makeRandom(6501); 325 const nodes = [[0, -0.05, 0]]; 326 const edges = []; 327 328 const deflect = (dir, spread) => { 329 const v = [ 330 dir[0] + A.gaussian(rand) * spread, 331 dir[1] + A.gaussian(rand) * spread * 0.5, 332 dir[2] + A.gaussian(rand) * spread 333 ]; 334 const len = Math.hypot(v[0], v[1], v[2]) || 1; 335 return [v[0] / len, v[1] / len, v[2] / len]; 336 }; 337 338 const grow = (from, dir, length, gen, max) => { 339 const p = nodes[from]; 340 const to = [p[0] + dir[0] * length, p[1] + dir[1] * length, p[2] + dir[2] * length]; 341 const index = nodes.length; 342 nodes.push(to); 343 edges.push(from, index); 344 if (gen >= max) return;
345 const kids = gen < 1 ? 2 : (rand() < 0.55 ? 2 : 3); 346 for (let k = 0; k < kids; k += 1) grow(index, deflect(dir, 0.62), length * 0.74, gen + 1, max); 347 }; 348 349 // Dendrites fan upward. 350 for (let d = 0; d < 4; d += 1) { 351 const a = (d / 4) * TAU; 352 grow(0, deflect([Math.cos(a) * 0.5, 1, Math.sin(a) * 0.5], 0.2), 0.34, 0, 3); 353 } 354 355 // A single axon runs down, then breaks into terminals. 356 let axon = 0; 357 for (let s = 0; s < 7; s += 1) { 358 const p = nodes[axon]; 359 const index = nodes.length; 360 nodes.push([p[0] + A.gaussian(rand) * 0.02, p[1] - 0.15, p[2] + A.gaussian(rand) * 0.02]); 361 edges.push(axon, index); 362 axon = index; 363 } 364 for (let d = 0; d < 5; d += 1) { 365 const a = (d / 5) * TAU; 366 grow(axon, deflect([Math.cos(a) * 0.8, -0.5, Math.sin(a) * 0.8], 0.2), 0.2, 1, 2); 367 } 368 369 const built = fromGraph(nodes, edges, n, { 370 rand, 371 jitter: 0.006, 372 nodeIntensity: (i) => (i === 0 ? 1 : 0.62), 373 fill: 0.3 374 }); 375 376 // Soma. 377 const somaStart = Math.floor(n * 0.9); 378 for (let i = somaStart; i < n; i += 1) { 379 built.pos[i * 3] = A.gaussian(rand) * 0.07; 380 built.pos[i * 3 + 1] = -0.05 + A.gaussian(rand) * 0.07; 381 built.pos[i * 3 + 2] = A.gaussian(rand) * 0.07; 382 built.intensity[i] = 1; 383 } 384 385 A.fit(built.pos, n, 1); 386 return built; 387 }; 388 389 /* A cell: membrane, nucleus, organelles, cytoskeleton. */ 390 const cellFigure = (n) => { 391 const rand = A.makeRandom(770077); 392 // Few, faint spokes: the cytoskeleton should read behind the membrane and 393 // nucleus, not turn the cell into a starburst. 394 const SPOKES = 16; 395 const nodes = []; 396 const edges = []; 397 398 for (let i = 0; i < SPOKES; i += 1) { 399 const phi = Math.acos(1 - (2 * (i + 0.5)) / SPOKES); 400 const theta = Math.PI * (1 + Math.sqrt(5)) * (i + 0.5); 401 const dir = [ 402 Math.sin(phi) * Math.cos(theta), 403 Math.cos(phi), 404 Math.sin(phi) * Math.sin(theta) 405 ]; 406 const inner = nodes.length; 407 nodes.push([dir[0] * 0.3, dir[1] * 0.3, dir[2] * 0.3]); 408 const outer = nodes.length; 409 nodes.push([dir[0] * 0.93, dir[1] * 0.93, dir[2] * 0.93]); 410 edges.push(inner, outer); 411 } 412 413 const built = fromGraph(nodes, edges, n, { 414 rand, 415 jitter: 0.006, 416 nodeIntensity: (i) => (i % 2 === 0 ? 0.5 : 0.3), 417 fill: 0.13 418 }); 419 420 const nucleusStart = Math.floor(n * 0.24); 421 const organelleStart = Math.floor(n * 0.5); 422 const membraneStart = Math.floor(n * 0.64); 423 424 for (let i = nucleusStart; i < organelleStart; i += 1) { 425 built.pos[i * 3] = A.gaussian(rand) * 0.16; 426 built.pos[i * 3 + 1] = A.gaussian(rand) * 0.16; 427 built.pos[i * 3 + 2] = A.gaussian(rand) * 0.16; 428 built.intensity[i] = 1; 429 } 430 431 const centres = []; 432 for (let k = 0; k < 6; k += 1) { 433 const phi = Math.acos(1 - (2 * (k + 0.5)) / 6); 434 const theta = Math.PI * (1 + Math.sqrt(5)) * (k + 0.5); 435 centres.push([ 436 0.58 * Math.sin(phi) * Math.cos(theta), 437 0.58 * Math.cos(phi), 438 0.58 * Math.sin(phi) * Math.sin(theta) 439 ]); 440 } 441 for (let i = organelleStart; i < membraneStart; i += 1) { 442 const c = centres[i % centres.length]; 443 built.pos[i * 3] = c[0] + A.gaussian(rand) * 0.09; 444 built.pos[i * 3 + 1] = c[1] + A.gaussian(rand) * 0.09; 445 built.pos[i * 3 + 2] = c[2] + A.gaussian(rand) * 0.09; 446 built.intensity[i] = 0.72; 447 } 448 449 for (let i = membraneStart; i < n; i += 1) { 450 const phi = Math.acos(1 - (2 * ((i - membraneStart) + 0.5)) / (n - membraneStart)); 451 const theta = Math.PI * (1 + Math.sqrt(5)) * (i + 0.5); 452 const r = 1 + A.gaussian(rand) * 0.012; 453 built.pos[i * 3] = r * Math.sin(phi) * Math.cos(theta); 454 built.pos[i * 3 + 1] = r * Math.cos(phi); 455 built.pos[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta); 456 built.intensity[i] = 0.38; 457 } 458 459 A.fit(built.pos, n, 1); 460 return built; 461 }; 462 463 /* ======================================= 464 Capability-audit figures 465 ======================================= */ 466 467 /* Two readout surfaces at one budget: the sanctioned output above, the 468 internal layer below, and the measured distance between them. */ 469 const gapFigure = (n) => { 470 const rand = A.makeRandom(2402); 471 const COLS = 11; 472 const nodes = []; 473 const edges = []; 474 475 for (let i = 0; i < COLS; i += 1) { 476 for (let j = 0; j < COLS; j += 1) { 477 const x = ((i / (COLS - 1)) - 0.5) * 1.7; 478 const z = ((j / (COLS - 1)) - 0.5) * 1.7; 479 if (Math.hypot(x, z) > 0.92) continue; 480 const upper = nodes.length; 481 nodes.push([x, 0.52, z]); 482 const lower = nodes.length; 483 nodes.push([x, -0.52, z]); 484 // Risers make the gap itself the thing you see. 485 if ((i + j) % 2 === 0) edges.push(upper, lower); 486 } 487 } 488 489 const built = fromGraph(nodes, edges, n, { 490 rand, 491 jitter: 0.008,
492 // Sanctioned surface sparse and dim; the internal layer dense and bright. 493 nodeIntensity: (i) => (i % 2 === 0 ? 0.3 : 1), 494 fill: 0.16 495 }); 496 497 // Thicken the lower surface, so the two planes read as unequal. 498 const denseStart = Math.floor(n * 0.62); 499 for (let i = denseStart; i < n; i += 1) { 500 const a = rand() * TAU; 501 const r = Math.sqrt(rand()) * 0.9; 502 built.pos[i * 3] = Math.cos(a) * r; 503 built.pos[i * 3 + 1] = -0.52 + A.gaussian(rand) * 0.035; 504 built.pos[i * 3 + 2] = Math.sin(a) * r; 505 built.intensity[i] = 0.82; 506 } 507 508 A.fit(built.pos, n, 1); 509 return built; 510 }; 511 512 /* Containment giving way: a bounded mass with part of it pulled out. */ 513 const recoverabilityFigure = (n) => { 514 const rand = A.makeRandom(9931); 515 const RING = 30; 516 const nodes = []; 517 const edges = []; 518 519 // The boundary, broken over one arc. 520 for (let i = 0; i < RING; i += 1) { 521 const a = (i / RING) * TAU; 522 nodes.push([Math.cos(a) * 0.86, Math.sin(a) * 0.86, 0]); 523 const gap = a > 5.1 && a < 6.05; 524 if (i > 0 && !gap) edges.push(i - 1, i); 525 } 526 527 const built = fromGraph(nodes, edges, n, { 528 rand, 529 jitter: 0.006, 530 nodeIntensity: () => 0.34, 531 fill: 0.22 532 }); 533 534 const held = Math.floor(n * 0.34); 535 const escaped = Math.floor(n * 0.78); 536 537 // Still contained. 538 for (let i = held; i < escaped; i += 1) { 539 const a = rand() * TAU; 540 const r = Math.sqrt(rand()) * 0.62; 541 built.pos[i * 3] = Math.cos(a) * r; 542 built.pos[i * 3 + 1] = Math.sin(a) * r; 543 built.pos[i * 3 + 2] = A.gaussian(rand) * 0.16; 544 built.intensity[i] = 0.5; 545 } 546 547 // Pulled out through the break, brightening as it goes. 548 for (let i = escaped; i < n; i += 1) { 549 const t = rand(); 550 const a = 5.58 + A.gaussian(rand) * 0.13; 551 const r = 0.7 + t * 0.95; 552 built.pos[i * 3] = Math.cos(a) * r; 553 built.pos[i * 3 + 1] = Math.sin(a) * r; 554 built.pos[i * 3 + 2] = A.gaussian(rand) * 0.07; 555 built.intensity[i] = 0.55 + t * 0.45; 556 } 557 558 A.fit(built.pos, n, 1); 559 return built; 560 }; 561 562 /* A sieve: six gates, and most of what enters does not come out. */ 563 const standardFigure = (n) => { 564 const rand = A.makeRandom(60606); 565 const GATES = 6; 566 const PER = 22; 567 const nodes = []; 568 const edges = []; 569 570 for (let g = 0; g < GATES; g += 1) { 571 const y = 0.92 - (g / (GATES - 1)) * 1.84; 572 const radius = 0.78 - g * 0.085; 573 const first = nodes.length; 574 for (let i = 0; i < PER; i += 1) { 575 const a = (i / PER) * TAU; 576 nodes.push([Math.cos(a) * radius, y, Math.sin(a) * radius]); 577 if (i > 0) edges.push(first + i - 1, first + i); 578 } 579 edges.push(first + PER - 1, first); 580 } 581 582 const built = fromGraph(nodes, edges, n, { 583 rand, 584 jitter: 0.005, 585 nodeIntensity: (i) => 0.34 + 0.4 * (Math.floor(i / PER) / (GATES - 1)), 586 fill: 0.2 587 }); 588 589 // The candidates. Each one stops at the gate that rejects it; a few reach 590 // the bottom, and those are the ones bright enough to see. 591 const streamStart = Math.floor(n * 0.56); 592 for (let i = streamStart; i < n; i += 1) { 593 const roll = rand(); 594 const survived = roll > 0.78 ? GATES - 1 : Math.floor(Math.pow(roll, 0.7) * (GATES - 1)); 595 const y = 0.92 - (survived / (GATES - 1)) * 1.84 + 0.06 + A.gaussian(rand) * 0.03; 596 const a = rand() * TAU; 597 const r = Math.sqrt(rand()) * (0.62 - survived * 0.07); 598 built.pos[i * 3] = Math.cos(a) * r; 599 built.pos[i * 3 + 1] = y; 600 built.pos[i * 3 + 2] = Math.sin(a) * r; 601 built.intensity[i] = survived === GATES - 1 ? 1 : 0.24; 602 } 603 604 A.fit(built.pos, n, 1); 605 return built; 606 }; 607 608 /* The coverage grid: axes against modalities, mostly still empty. */ 609 const coverageFigure = (n) => { 610 const rand = A.makeRandom(101010); 611 const AXES = 10; 612 const MODES = 5; 613 const DEPTH = 3; 614 const nodes = []; 615 const edges = []; 616 617 for (let a = 0; a < AXES; a += 1) { 618 for (let m = 0; m < MODES; m += 1) { 619 const x = ((a / (AXES - 1)) - 0.5) * 1.9; 620 const y = ((m / (MODES - 1)) - 0.5) * 1.0; 621 const index = nodes.length; 622 nodes.push([x, y, 0]); 623 if (a > 0) edges.push(index - MODES, index); 624 if (m > 0) edges.push(index - 1, index); 625 } 626 } 627 628 const built = fromGraph(nodes, edges, n, { 629 rand, 630 jitter: 0.004, 631 // Only the corner of the grid that has cards behind it is lit. 632 nodeIntensity: (i) => (Math.floor(i / MODES) < 2 && i % MODES < 1 ? 1 : 0.22), 633 fill: 0.12 634 }); 635 636 // The filled cells get real depth; the rest stay a flat lattice. 637 const fillStart = Math.floor(n * 0.86); 638 for (let i = fillStart; i < n; i += 1) { 639 const a = Math.floor(rand() * 2); 640 const x = ((a / (AXES - 1)) - 0.5) * 1.9; 641 built.pos[i * 3] = x + A.gaussian(rand) * 0.05; 642 built.pos[i * 3 + 1] = -0.5 + A.gaussian(rand) * 0.05; 643 built.pos[i * 3 + 2] = (rand() - 0.5) * 0.4 * DEPTH * 0.3; 644 built.intensity[i] = 0.9; 645 } 646 647 A.fit(built.pos, n, 1); 648 return built; 649 }; 650 651 /* ======================================= 652 Model-organism figures 653 ======================================= */ 654 655 /* A toy beside a real specimen: a tidy planted lattice on one side, the 656 traced animal on the other. */ 657 const thesisFigure = (n) => { 658 const rand = A.makeRandom(13579); 659 const nodes = []; 660 const edges = []; 661 const SIDE = 4; 662 663 // The toy: a perfect little cube, entirely of your own making. 664 for (let i = 0; i < SIDE; i += 1) { 665 for (let j = 0; j < SIDE; j += 1) { 666 for (let k = 0; k < SIDE; k += 1) { 667 const index = nodes.length; 668 nodes.push([ 669 -0.92 + (i / (SIDE - 1) - 0.5) * 0.5, 670 (j / (SIDE - 1) - 0.5) * 0.5, 671 (k / (SIDE - 1) - 0.5) * 0.5 672 ]); 673 if (i > 0) edges.push(index - SIDE * SIDE, index); 674 if (j > 0) edges.push(index - SIDE, index); 675 if (k > 0) edges.push(index - 1, index); 676 } 677 } 678 } 679 680 // The organism: irregular, and not yours. 681 const CORD = 20; 682 const cordStart = nodes.length; 683 for (let i = 0; i < CORD; i += 1) { 684 const t = i / (CORD - 1); 685 nodes.push([0.34 + t * 0.92, Math.sin(t * 2.6) * 0.14 + 0.08, A.gaussian(rand) * 0.05]); 686 if (i >
686 0) edges.push(cordStart + i - 1, cordStart + i); 687 if (i % 3 === 0 && i > 0) { 688 const side = i % 6 === 0 ? 1 : -1; 689 const g = nodes.length; 690 nodes.push([0.34 + t * 0.92, Math.sin(t * 2.6) * 0.14 - 0.22, side * 0.2]); 691 edges.push(cordStart + i, g); 692 } 693 } 694 695 const built = fromGraph(nodes, edges, n, { 696 rand, 697 jitter: 0.007, 698 nodeIntensity: (i) => (i < SIDE * SIDE * SIDE ? 0.36 : 1), 699 fill: 0.24 700 }); 701 702 A.fit(built.pos, n, 1); 703 return built; 704 }; 705 706 /* Everything traced: a dense graph with no unvisited part. */ 707 const exhaustiveFigure = (n) => { 708 const rand = A.makeRandom(24680); 709 const NODES = 40; 710 const nodes = []; 711 const edges = []; 712 713 for (let i = 0; i < NODES; i += 1) { 714 const phi = Math.acos(1 - (2 * (i + 0.5)) / NODES); 715 const theta = Math.PI * (1 + Math.sqrt(5)) * (i + 0.5); 716 nodes.push([ 717 Math.sin(phi) * Math.cos(theta) * 0.88, 718 Math.cos(phi) * 0.88, 719 Math.sin(phi) * Math.sin(theta) * 0.88 720 ]); 721 } 722 // Every pair. The point is that nothing is left out. 723 for (let i = 0; i < NODES; i += 1) { 724 for (let j = i + 1; j < NODES; j += 1) edges.push(i, j); 725 } 726 727 const built = fromGraph(nodes, edges, n, { 728 rand, 729 jitter: 0.004, 730 nodeIntensity: () => 1, 731 fill: 0.14 732 }); 733 734 A.fit(built.pos, n, 1); 735 return built; 736 }; 737 738 /* Two independent structures, and the correspondences that survive between 739 them: the model on one side, the experiment on the other. */ 740 const groundTruthFigure = (n) => { 741 const rand = A.makeRandom(97531); 742 const PAIRS = 16; 743 const nodes = []; 744 const edges = []; 745 746 for (let i = 0; i < PAIRS; i += 1) { 747 const t = i / (PAIRS - 1) - 0.5; 748 const left = nodes.length; 749 nodes.push([-0.62, t * 1.65, A.gaussian(rand) * 0.1]); 750 const right = nodes.length; 751 nodes.push([0.62, t * 1.65 + A.gaussian(rand) * 0.09, A.gaussian(rand) * 0.1]); 752 // Only some correspondences hold; the rest are claims that failed. 753 if (rand() < 0.42) edges.push(left, right); 754 } 755 756 const built = fromGraph(nodes, edges, n, { 757 rand, 758 jitter: 0.006, 759 nodeIntensity: (i) => (i % 2 === 0 ? 0.9 : 0.55), 760 fill: 0.42 761 }); 762 763 A.fit(built.pos, n, 1); 764 return built; 765 }; 766 767 /* A tensor network: a chain of cores with bond legs and free indices, the 768 computation sitting in the structure rather than in the input. */ 769 const tensorFigure = (n) => { 770 const rand = A.makeRandom(86420); 771 const CORES = 7; 772 const nodes = []; 773 const edges = []; 774 const cores = []; 775 776 for (let i = 0; i < CORES; i += 1) { 777 const x = (i / (CORES - 1) - 0.5) * 1.75; 778 const index = nodes.length; 779 cores.push(index); 780 nodes.push([x, 0, 0]); 781 if (i > 0) edges.push(cores[i - 1], index); 782 783 // Free indices, up and down. 784 const up = nodes.length; 785 nodes.push([x, 0.52, 0]); 786 edges.push(index, up); 787 const down = nodes.length; 788 nodes.push([x, -0.52, 0]); 789 edges.push(index, down); 790 791 // A second rank of bonds behind the chain. 792 const back = nodes.length; 793 nodes.push([x, 0, 0.46]); 794 edges.push(index, back); 795 if (i > 0) edges.push(back - 4, back); 796 } 797 798 const built = fromGraph(nodes, edges, n, { 799 rand, 800 jitter: 0.005, 801 nodeIntensity: (i) => (cores.indexOf(i) >= 0 ? 1 : 0.42), 802 fill: 0.3 803 }); 804 805 A.fit(built.pos, n, 1); 806 return built; 807 }; 808 809 // Two renderers can live on one page; the geometry is identical, so build it 810 // once and hand out the same set. 811 const cache = new Map(); 812 813 window.BiodynFigures = { 814 build: (n) => { 815 if (!cache.has(n)) cache.set(n, buildAll(n)); 816 return cache.get(n); 817 } 818 }; 819 820 function buildAll(n) { 821 return ({ 822 cases: [modelOrganism(n), cutawayAudit(n), extraction(n)], 823 organisms: { 824 thesis: thesisFigure(n), 825 exhaustive: exhaustiveFigure(n), 826 groundtruth: groundTruthFigure(n), 827 tensor: tensorFigure(n) 828 }, 829 audits: { 830 gap: gapFigure(n), 831 recoverability: recoverabilityFigure(n),
832 standard: standardFigure(n), 833 coverage: coverageFigure(n) 834 }, 835 applications: { 836 grn: grnFigure(n), 837 longevity: longevityFigure(n), 838 neuro: neuroFigure(n), 839 cell: cellFigure(n) 840 } 841 }); 842 } 843})();
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.