ferrotherm 0.6.0

Thermodynamic computing in pure Rust: sparse energy-based models, chromatic block-Gibbs, parallel tempering, thermodynamic linear algebra, stochastic differentiable programs, a variational compiler onto device topologies, exact inference by variable elimination, planted instances with known optima, sampler certificates, and a first-class joules ledger. std-only, zero dependencies, wasm-clean, deterministic by seed.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Thermodynamic sampling on an FPGA you can buy · ferrotherm</title>
<meta name="description" content="Step by step: run a thermodynamic sampler on an Alchitry Pt V2. Pure Rust, no vendor tools. Every number on this page was measured.">
<style>
  :root{
    --bg:#0b1220; --panel:#111a2e; --line:#24314d;
    --ink:#d8e1f0; --ink-dim:#a7b3c9; --ink-faint:#7d8aa3;
    --gold:#cfaa5b; --gold-dim:rgba(207,170,91,.55);
    --ok:#7dd87d; --cool:#5b8fd4;
    --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Helvetica,Arial,sans-serif;
  }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);
       font-size:17px;line-height:1.68;-webkit-font-smoothing:antialiased}
  .wrap{max-width:74ch;margin:0 auto;padding:0 24px}
  header.hero{padding:88px 0 56px;border-bottom:1px solid var(--line)}
  .mark{font-family:var(--mono);color:var(--gold);letter-spacing:.14em;
        text-transform:uppercase;font-size:12px}
  h1{font-size:clamp(34px,6vw,54px);line-height:1.06;margin:18px 0 20px;
     letter-spacing:-.02em;text-wrap:balance;font-weight:600}
  h2{font-size:clamp(24px,3.4vw,32px);line-height:1.18;margin:0 0 14px;
     letter-spacing:-.015em;text-wrap:balance;font-weight:600}
  h3{font-size:19px;margin:34px 0 10px;font-weight:600}
  .lede{font-size:20px;color:var(--ink-dim);max-width:62ch}
  section{padding:56px 0;border-bottom:1px solid var(--line)}
  .eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.14em;
           text-transform:uppercase;color:var(--gold);margin:0 0 10px}
  p{margin:0 0 16px;color:var(--ink-dim)}
  p.tight{margin-bottom:8px}
  b,strong{color:var(--ink);font-weight:600}
  a{color:var(--gold);text-decoration:none;border-bottom:1px solid var(--gold-dim)}
  a:hover{border-bottom-color:var(--gold)}
  code{font-family:var(--mono);font-size:.88em;color:var(--ink);
       background:rgba(255,255,255,.05);padding:1px 5px;border-radius:4px}
  pre{font-family:var(--mono);font-size:13px;line-height:1.6;background:#080e18;
      border:1px solid var(--line);border-radius:10px;padding:14px 16px;
      overflow-x:auto;color:var(--ink-dim);margin:0 0 8px}
  pre b{color:var(--gold);font-weight:600}
  pre .ok{color:var(--ok)}
  .cap{font-family:var(--mono);font-size:12px;color:var(--ink-faint);margin:0 0 26px}
  canvas{display:block;width:100%;height:auto;background:#080e18;
         border:1px solid var(--line);border-radius:10px}
  .step{border-left:2px solid var(--gold-dim);padding-left:20px;margin:34px 0}
  .step .n{font-family:var(--mono);font-size:12px;color:var(--gold);letter-spacing:.1em}
  table{width:100%;border-collapse:collapse;font-size:14px;margin:8px 0 18px}
  th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);
        font-family:var(--mono);font-size:13px;color:var(--ink-dim)}
  th{color:var(--ink-faint);font-weight:500;text-transform:uppercase;
     letter-spacing:.08em;font-size:11px}
  td b{color:var(--gold);font-weight:600}
  .tw{overflow-x:auto;margin:8px 0 4px}
  .tw table{min-width:640px}
  tr.alt td{color:var(--ink-faint);background:rgba(91,143,212,.06)}
  .eq{font-family:var(--mono);font-size:15px;color:var(--ink);text-align:center;
      background:#080e18;border:1px solid var(--line);border-radius:10px;
      padding:16px 12px;margin:4px 0 16px;overflow-x:auto}
  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
  @media(max-width:640px){.grid2{grid-template-columns:1fr}}
  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 20px}
  .card h4{margin:0 0 6px;font-size:15px;color:var(--ink)}
  .card p{font-size:14px;margin:0;color:var(--ink-faint)}
  .note{border-left:2px solid var(--cool);padding-left:18px;color:var(--ink-faint);font-size:15px}
  footer{padding:56px 0 90px;color:var(--ink-faint);font-size:14px}
  .ctl{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:10px 0 4px}
  button{font-family:var(--mono);font-size:12px;letter-spacing:.05em;
          background:transparent;color:var(--gold);border:1px solid var(--gold-dim);
          border-radius:6px;padding:7px 14px;cursor:pointer}
  button:hover{border-color:var(--gold)}
  input[type=range]{accent-color:var(--gold);width:190px}
  .readout{font-family:var(--mono);font-size:12px;color:var(--ink-faint)}
</style>
</head>
<body>

<header class="hero"><div class="wrap">
  <p class="mark">Institute for Physical AI @ BMI · ferrotherm</p>
  <h1>Thermodynamic sampling on an FPGA you can buy.</h1>
  <p class="lede">A new class of chip computes by drawing random samples with its own thermal
  noise. The physics is a century old and belongs to everyone. This page shows you how to build
  that machine yourself, on a 350 dollar board, in pure Rust, with no vendor toolchain anywhere in
  the path. Every number here was measured on real silicon.</p>
</div></header>

<section><div class="wrap">
  <p class="eyebrow">What you are building</p>
  <h2>A binary stochastic neuron is a coin whose bias is set by its neighbours.</h2>
  <p>An ordinary bit is 0 or 1. The unit here is a coin that gets flipped over and over, and the
  only thing you control is how the coin is weighted. Its neighbours set that weight: the more of
  them that agree, the more strongly the coin leans one way.</p>
  <p>You may have met this unit as a <em>p-bit</em>. That name was coined in 2016 and is now
  widespread in device physics, but the object is much older and has names in two established
  fields. We use the literature names on this page and give the full lineage
  <a href="#names">below</a>.</p>
  <p>Drag the field below and watch the coin's bias follow it. This one curve, the sigmoid, is the
  entire computation a thermodynamic chip performs, repeated across hundreds of thousands of
  cells.</p>
  <canvas id="c-pbit" width="900" height="260"></canvas>
  <div class="ctl">
    <span class="readout">local field</span>
    <input id="f-pbit" type="range" min="-300" max="300" value="80">
    <span class="readout" id="r-pbit"></span>
  </div>
  <p class="cap">Interactive. The bar is the running fraction of samples that came up +1.</p>

  <h3>Half the lattice can flip at once</h3>
  <p>If two neurons are not connected, nothing they do can disturb each other, so they may be
  updated at the same instant. Colour the grid like a checkerboard and every square of one colour
  is independent of every other. That is why this computation suits hardware: one sweep is two
  parallel half sweeps, not thousands of sequential steps.</p>
  <canvas id="c-lattice" width="900" height="300"></canvas>
  <div class="ctl">
    <button id="b-lattice">Run</button>
    <span class="readout">temperature</span>
    <input id="t-lattice" type="range" min="20" max="140" value="60">
    <span class="readout" id="r-lattice"></span>
  </div>
  <p class="cap">Interactive. Lower the temperature and order appears on its own. Nothing here is
  scripted: the pattern is the sampler settling.</p>
</div></section>

<section id="names"><div class="wrap">
  <p class="eyebrow">What to call it</p>
  <h2>One unit, a century of names.</h2>
  <p>Every name in the table below denotes the same object, and every field writes the same rule.
  Site <em>i</em> is set to +1 with probability</p>
  <p class="eq">P(s<sub>i</sub> = +1 | rest) &nbsp;=&nbsp; &sigma;( 2&beta; ( &Sigma;<sub>j</sub>
  J<sub>ij</sub> s<sub>j</sub> + h<sub>i</sub> ) )</p>
  <p>which is the sigmoid you dragged above. The differences between the rows are vocabulary and
  discipline, not mathematics.</p>

  <div class="tw"><table>
    <thead><tr><th>Name</th><th>Field</th><th>Where it comes from</th><th>Status</th></tr></thead>
    <tbody>
    <tr><td>spin</td><td>statistical physics</td>
      <td>Lenz 1920; Ising, <em>Z. Phys.</em> <b>31</b>, 253 (1925)</td><td>academic</td></tr>
    <tr><td>Glauber dynamics, heat-bath update</td><td>statistical physics</td>
      <td>Glauber, <em>J. Math. Phys.</em> <b>4</b>, 294 (1963)</td><td>academic</td></tr>
    <tr><td>unit, binary unit</td><td>neural networks</td>
      <td>Ackley, Hinton &amp; Sejnowski, <em>Cognitive Science</em> <b>9</b>, 147 (1985)</td>
      <td>academic</td></tr>
    <tr><td>stochastic neuron</td><td>finite-temperature Hopfield nets</td>
      <td>Little 1974; Amit, Gutfreund &amp; Sompolinsky, <em>Phys. Rev. A</em> <b>32</b>, 1007
      (1985)</td><td>academic</td></tr>
    <tr><td>sigmoid unit, unit of a sigmoid belief network</td><td>machine learning</td>
      <td>Neal, <em>Artificial Intelligence</em> <b>56</b>, 71 (1992)</td><td>academic</td></tr>
    <tr><td><b>binary stochastic neuron</b> (BSN)</td><td>deep learning; spintronics</td>
      <td>Bengio, L&eacute;onard &amp; Courville, arXiv:1308.3432 (2013); Camsari, Sutton &amp;
      Datta, <em>Appl. Phys. Rev.</em> <b>6</b>, 011305 (2019)</td>
      <td>academic</td></tr>
    <tr><td>p-bit, probabilistic bit</td><td>device physics</td>
      <td>Camsari, Faria, Sutton &amp; Datta, arXiv:1610.00377 &rarr; <em>Phys. Rev. X</em>
      <b>7</b>, 031014 (2017)</td><td>academic coinage</td></tr>
    <tr><td>pbit, thermodynamic sampling unit</td><td>company usage</td>
      <td>Extropic Corp. (2025)</td><td><b>branded</b></td></tr>
    <tr class="alt"><td>stochastic number, bit stream</td><td>computer arithmetic</td>
      <td>von Neumann 1956; Gaines, <em>AFIPS SJCC</em> (1967)</td>
      <td><b>different concept</b></td></tr>
    </tbody>
  </table></div>
  <p class="cap">Bold names are the ones this project uses. The last row is a false friend, explained
  below.</p>

  <h3>How the naming went</h3>
  <p>Lenz proposed the model in 1920 and his student Ising solved the one dimensional case in 1925,
  but that model was static: an energy over spins, with no rule for how they change. Roy Glauber
  supplied the dynamics in 1963, deriving the flip rate from the requirement that the chain settle
  to thermal equilibrium. His rule and a sigmoid resample are the same rule written two ways.</p>
  <p>The unit entered machine learning through the Boltzmann machine, where Ackley, Hinton and
  Sejnowski called it simply a <em>unit</em>: "a unit is always in one of two states, on or off, and
  it adopts these states as a probabilistic function of the states of its neighboring units." Their
  own paper notes the physical parallel: such a system "in contact with a heat bath at a given
  temperature will eventually reach thermal equilibrium."</p>
  <p>By 1992 the operation had its modern name. Neal's section 2.2 is headed "Gibbs sampling for
  Boltzmann machines," and his equation 5 gives the conditional as
  &sigma;(x* &Sigma;<sub>j&ne;i</sub> s<sub>j</sub> w<sub>ij</sub>), with the note that
  "the 'sigmoid' function, &sigma;(t), is defined as 1/(1 + exp(&minus;t))." That is the rule this
  project implements and, word for word, the rule the current hardware papers describe.</p>
  <p>The term <em>p-bit</em> was introduced in October 2016 by Camsari, Faria, Sutton and Datta. It
  is a real academic term, now standard in device physics, and the same group made the identity
  explicit in 2019: their figure 2 is captioned "p-bit: Binary Stochastic Neuron." The coining paper
  had already said so, writing that its defining equations "are essentially the same as the defining
  equations for Boltzmann machines introduced by Hinton and his collaborators." What was new there
  was the hardware, a three terminal device with gain and input–output isolation, and the use of it
  for invertible logic. The unit itself was thirty years old and the physics sixty.</p>
  <p>Extropic's unhyphenated <em>pbit</em> is company vocabulary layered on that coinage. Their
  peer-facing paper is the clearer guide to what they built: it does not use the word at all,
  describing each node as "a single Bernoulli random variable" advanced by "the Gibbs sampling
  update rule," and noting that Boltzmann machines are "also known as Ising models in physics."</p>

  <h3>One false friend</h3>
  <p><em>Stochastic computing</em>, from von Neumann in 1956 and named by Gaines in 1967, is a
  different and older idea that the similar name invites you to conflate. There, a number is encoded
  as the fraction of ones in a long random bit stream, so that a multiply becomes a single AND gate.
  The randomness is a carrier you average away to recover an ordinary arithmetic answer. Here the
  randomness is the product: the unit's job is to draw a real sample, so that a network of them
  performs Markov chain Monte Carlo and visits states at their Boltzmann frequency. One is
  arithmetic done with probabilities, the other is sampling done by physics.</p>

  <p class="cap">A footnote for the careful reader: Ackley, Hinton and Sejnowski describe their own
  sigmoid rule as "a form of the Metropolis algorithm." It is not. Metropolis accepts with
  min(1, e<sup>&minus;&Delta;E/T</sup>); their rule is Glauber's. The slip has been repeated in
  textbooks ever since.</p>
</div></section>

<section><div class="wrap">
  <p class="eyebrow">The board</p>
  <h2>Alchitry Pt V2.</h2>
  <p>An Artix-7 XC7A100T in a 484 ball package, with 63,400 six input lookup tables. Each lookup
  table can hold one neuron. A dual channel USB chip on the board speaks JTAG to the FPGA, which is
  the only wire you need: identify the chip, load a design, read its state back.</p>
  <canvas id="c-board" width="900" height="420"></canvas>
  <p class="cap">Diagram, not a photograph. Positions are schematic; part numbers and capacities
  are from the vendor listing and the AMD datasheet.</p>
  <div class="grid2">
    <div class="card"><h4>XC7A100T-2FGG484I</h4>
      <p>63,400 LUT6 · 126,800 flip flops · 4,860 Kb block RAM · 240 DSP slices. The package on
      the vendor listing reads FGG84I; the board's own schematic says FGG484.</p></div>
    <div class="card"><h4>FT2232H</h4>
      <p>Two USB channels. Channel A drives JTAG in MPSSE mode: four pins, TCK TDI TDO TMS. This
      is the entire interface used on this page.</p></div>
  </div>
</div></section>

<section><div class="wrap">
  <p class="eyebrow">Step by step</p>
  <h2>From an empty board to a fabric on silicon.</h2>
  <p>Six steps. Each one prints something you can check. The outputs shown are real, copied from
  a run against a Pt V2 driven over a network link from the other side of the world.</p>

  <div class="step">
    <p class="n">STEP 0</p>
    <h3>Install</h3>
    <p>Two crates, no dependencies beyond the USB transport. The core runs anywhere, including a
    browser; the silicon layer talks to the board.</p>
    <pre>cargo add ferrotherm
cargo add ferrotherm-silicon --features flash</pre>
  </div>

  <div class="step">
    <p class="n">STEP 1</p>
    <h3>Ask the chip what it is</h3>
    <p>Every FPGA holds a hardwired identity code. Reading it proves the whole path works: USB,
    the MPSSE engine, the JTAG state machine, and the chip itself.</p>
    <pre>$ cargo run --features flash --example probe

board: <b>Alchitry Pt V2</b>
IDCODE (JTAG DR):        <b>0x13631093</b>  -> XC7A100T
IDCODE (config port):    <b>0x13631093</b>
cross-check: <span class="ok">PASS</span> - two independent paths agree</pre>
    <p>Read the identity twice, by two unrelated mechanisms. If the packet layer were wrong they
    would disagree. A value you can predict is the right first thing to measure.</p>
  </div>

  <div class="step">
    <p class="n">STEP 2</p>
    <h3>Prove the sampler is a sampler</h3>
    <p>Before trusting any hardware, check the mathematics against an answer nobody chose. Onsager
    solved the two dimensional Ising model exactly in 1944. Our sampler must reproduce it.</p>
    <pre>$ cargo run --release --example onsager

     beta  |M| sampled      M exact   |delta|
     0.50       0.9111       0.9113    0.0002  ok
     0.60       0.9736       0.9736    0.0000  ok
     0.70       0.9902       0.9902    0.0000  ok
verdict: <span class="ok">PASS</span> - sampler reproduces the exact 2D Ising solution</pre>
    <p>Four decimal places against a closed form from 1944. This gate runs before every release.</p>
  </div>

  <div class="step">
    <p class="n">STEP 3</p>
    <h3>Build a fabric</h3>
    <p>Each neuron becomes one lookup table holding a stochastic threshold, placed into a real
    slice, with its couplings routed through real interconnect. The tool resolves every switch to
    physical configuration bits.</p>
    <pre>$ cargo run --release --example bsn_fabric -- $DB/xc7a100t/tilegrid.json ...

placing 8 binary stochastic neurons, one LUT6 each:
  neuron 0: CLBLL_L_X2Y102 site SLICE_X0Y102
  ...
stochastic-threshold LUT (threshold 3): INIT = <b>0xFFFEFEE8FEE8E880</b>
8/8 truth tables written into frames
couplings routed: <b>7</b> (22 PIPs total), failed: 0
bitstream: <b>11,416 bytes</b>, 26 frames, <b>398</b> configuration bits set
verdict: <span class="ok">FABRIC EMITTED</span> - every neuron placed, every coupling routed</pre>
  </div>

  <div class="step">
    <p class="n">STEP 4</p>
    <h3>Load it</h3>
    <p>Clear the configuration memory, stream the bitstream through the configuration port,
    release the startup sequencer. The status register tells you what happened at each stage.</p>
    <pre>$ cargo run --features flash --example config_trace

  at start          STAT=0x5000190C
JPROGRAM (clear configuration memory):
  after 20k clocks  STAT=<b>0x00000000</b>   <span style="color:#7d8aa3">the fabric is now blank</span>
CFG_IN (stream the payload):
  after payload     STAT=<b>0x601079FC</b>   <span style="color:#7d8aa3">configured</span></pre>
    <p>The top digit of that status word encodes how the chip was configured: 5 after booting from
    its own flash, 6 after a load over JTAG. Comparing whole status words across different
    configuration paths will tell you a successful load failed.</p>
  </div>

  <div class="step">
    <p class="n">STEP 5</p>
    <h3>Ask the silicon where the bits went</h3>
    <p>Read the configuration back out of the chip and compare it against a bitstream produced by
    a different toolchain entirely. This is the only version of the test that means anything:
    checking a readback against your own writes proves nothing, because a wrong address agrees
    with itself.</p>
    <pre>$ cargo run --features flash --example readback

reference: FAR 0x00000000, 25856 payload words (256 frames)
reading 4 frames from FAR 0x00000000...
<b>404/404 words match (100.0%)</b>
verdict: <span class="ok">THE DEVICE AGREES</span></pre>
  </div>
</div></section>

<section><div class="wrap">
  <p class="eyebrow">What it costs</p>
  <h2>Every sample has a price, and the price is mostly not the sample.</h2>
  <p>A thermodynamic chip is cheap per update and expensive per crossing. Using the published
  figures for one such design, a single node update costs about seven femtojoules, reading one
  node out costs the same as 239 updates, and writing one costs about 21,700. The architecture
  wins where many local updates happen between rare crossings, and loses where they do not.</p>
  <table>
    <tr><th>operation</th><th>energy</th><th>in updates</th></tr>
    <tr><td>sample one node</td><td><b>7.09 fJ</b></td><td>1</td></tr>
    <tr><td>read one node out</td><td>1.692 pJ</td><td>239</td></tr>
    <tr><td>write one node</td><td>153.6 pJ</td><td><b>21,665</b></td></tr>
  </table>
  <p class="cap">Pre silicon circuit simulation figures from the vendor's own appendix
  (arXiv:2608.01615, Table IV), not measured silicon. ferrotherm carries these as a swappable
  price list, so any workload can be costed in joules before it is built.</p>
  <div class="note">
    <p>Measured on our side for comparison: the same sampler in a browser tab on a laptop GPU runs
    at 9.35 billion node updates per second, which puts it between 151 and 905 times the projected
    per update energy of the dedicated chip depending on the wattage you charge it. Two to three
    orders of magnitude, not the four often quoted. Both biases are stated: our figure covers the
    whole platform, theirs excludes input and output entirely.</p>
  </div>
</div></section>

<section><div class="wrap">
  <p class="eyebrow">Status</p>
  <h2>Roadmap.</h2>

  <h3>Shipped</h3>
  <table>
    <tr><th>capability</th><th>evidence</th></tr>
    <tr><td>identify the chip</td><td>two independent paths agree</td></tr>
    <tr><td>load a design</td><td>104 KB streamed over JTAG, chip reports configured</td></tr>
    <tr><td>read configuration back</td><td><b>404/404</b> words vs a foreign bitstream</td></tr>
    <tr><td>place and route a fabric</td><td>8 neurons, 7 couplings, 22 PIPs, 398 bits</td></tr>
    <tr><td>sampler correctness</td><td>Onsager to four decimals</td></tr>
    <tr><td>throughput, browser GPU</td><td>9.35e9 node updates per second</td></tr>
  </table>

  <h3>In progress</h3>
  <p class="tight">The emitted fabric is combinational today: lookup tables and the wires between
  them. Two pieces turn it into a running sampler, and both sit on layers already built.</p>
  <div class="grid2">
    <div class="card"><h4>Sequential fabric</h4>
      <p>A flip flop per neuron to hold state, and a clock to advance it. Needs the AFF and AFFMUX
      configuration bits plus clock routing through a global buffer.</p></div>
    <div class="card"><h4>State readout</h4>
      <p>CAPTURE latches the flip flops into configuration frames, which the readback path on this
      page already reads correctly. No input or output pins required.</p></div>
  </div>

  <h3>Next</h3>
  <div class="grid2">
    <div class="card"><h4>The rest of the ladder</h4>
      <p>Alchitry Cu V2 and Au V2, Kria KV260, the M.2 Artix, Alveo U55C and AWS F2. The same
      fabric compiler, different chip databases.</p></div>
    <div class="card"><h4>Scale</h4>
      <p>Thousands of neurons per die rather than eight, and the energy ledger reported per
      completed sample rather than per update.</p></div>
  </div>
</div></section>

<footer><div class="wrap">
  <p><a href="https://github.com/dcharlot-physicalai-bmi/ferrotherm">Repository</a> ·
     <a href="https://crates.io/crates/ferrotherm">ferrotherm</a> ·
     <a href="https://crates.io/crates/ferrotherm-silicon">ferrotherm-silicon</a> ·
     <a href="https://energy.physicalai-bmi.org/thermo">The energy programme</a></p>
  <p style="font-size:13px;margin-top:14px">Physics: Ising 1925 · Glauber 1963 · Onsager 1944 ·
  Geman and Geman 1984. Fabric data: Project X-Ray database, ISC licence. Apache 2.0.
  Institute for Physical AI at BMI.</p>
</div></footer>

<script>
// ---- p-bit: a coin whose bias is a sigmoid of its field -------------------
(function(){
  var c=document.getElementById('c-pbit'), x=c.getContext('2d');
  var f=document.getElementById('f-pbit'), r=document.getElementById('r-pbit');
  var hist=[], up=0, n=0;
  function draw(){
    var field=+f.value/100, p=1/(1+Math.exp(-2*field));
    var W=c.width,H=c.height; x.clearRect(0,0,W,H);
    // sigmoid curve
    x.strokeStyle='rgba(207,170,91,.85)'; x.lineWidth=2; x.beginPath();
    for(var i=0;i<=300;i++){
      var fx=-3+6*i/300, py=1/(1+Math.exp(-2*fx));
      var px=60+ (W-140)*(fx+3)/6, pyy=H-50-(H-100)*py;
      i?x.lineTo(px,pyy):x.moveTo(px,pyy);
    }
    x.stroke();
    // axes
    x.strokeStyle='rgba(255,255,255,.12)'; x.lineWidth=1;
    x.beginPath(); x.moveTo(60,H-50); x.lineTo(W-80,H-50); x.stroke();
    x.beginPath(); x.moveTo(60,30); x.lineTo(60,H-50); x.stroke();
    // marker
    var mx=60+(W-140)*(field+3)/6, my=H-50-(H-100)*p;
    x.strokeStyle='rgba(255,255,255,.2)'; x.setLineDash([3,4]);
    x.beginPath(); x.moveTo(mx,H-50); x.lineTo(mx,my); x.lineTo(60,my); x.stroke(); x.setLineDash([]);
    x.fillStyle='#cfaa5b'; x.beginPath(); x.arc(mx,my,5,0,6.283); x.fill();
    // sample stream
    for(var k=0;k<3;k++){ var s=Math.random()<p; hist.push(s); up+=s?1:0; n++; if(hist.length>90){ up-=hist.shift()?1:0; } }
    var bx=W-64;
    for(var i=0;i<hist.length;i++){
      x.fillStyle=hist[i]?'rgba(207,170,91,.9)':'rgba(91,143,212,.55)';
      x.fillRect(bx, H-56-i*2.3, 34, 1.9);
    }
    x.font='11px ui-monospace,Menlo,monospace'; x.fillStyle='#7d8aa3'; x.textAlign='center';
    x.fillText('samples', bx+17, H-34);
    x.textAlign='left';
    x.fillText('P(+1)', 20, 40); x.fillText('field', W-140, H-30);
    r.textContent='field '+field.toFixed(2)+'   P(+1) = '+p.toFixed(3)+
                  '   measured '+(hist.length? (up/hist.length).toFixed(3):'-');
    requestAnimationFrame(draw);
  }
  f.addEventListener('input',function(){hist=[];up=0;});
  draw();
})();

// ---- lattice: chromatic sweep, order appearing as temperature drops -------
(function(){
  var c=document.getElementById('c-lattice'), x=c.getContext('2d');
  var b=document.getElementById('b-lattice'), t=document.getElementById('t-lattice');
  var r=document.getElementById('r-lattice');
  var N=48, s=new Int8Array(N*N), running=true, phase=0;
  for(var i=0;i<N*N;i++) s[i]=Math.random()<0.5?-1:1;
  function sweep(){
    var beta=+t.value/100;
    for(var y=0;y<N;y++) for(var xx=0;xx<N;xx++){
      if(((xx+y)&1)!==phase) continue;
      var i=y*N+xx, f=0;
      f+=s[y*N+((xx+1)%N)]; f+=s[y*N+((xx+N-1)%N)];
      f+=s[((y+1)%N)*N+xx]; f+=s[((y+N-1)%N)*N+xx];
      s[i]=(Math.random()<1/(1+Math.exp(-2*beta*f)))?1:-1;
    }
    phase^=1;
  }
  function draw(){
    if(running){ sweep(); sweep(); }
    var W=c.width,H=c.height, cell=Math.min(H-30,(W-30))/N, ox=(W-cell*N)/2, oy=(H-cell*N)/2;
    x.clearRect(0,0,W,H);
    for(var y=0;y<N;y++) for(var xx=0;xx<N;xx++){
      x.fillStyle = s[y*N+xx]>0 ? 'rgba(207,170,91,.88)' : 'rgba(70,95,130,.7)';
      x.fillRect(ox+xx*cell, oy+y*cell, cell-0.6, cell-0.6);
    }
    var m=0; for(var i=0;i<N*N;i++) m+=s[i];
    r.textContent='beta '+(+t.value/100).toFixed(2)+'   |M| '+Math.abs(m/(N*N)).toFixed(3);
    requestAnimationFrame(draw);
  }
  b.addEventListener('click',function(){running=!running;this.textContent=running?'Pause':'Run';});
  b.textContent='Pause';
  draw();
})();

// ---- board diagram --------------------------------------------------------
(function(){
  var c=document.getElementById('c-board'), x=c.getContext('2d');
  var W=c.width,H=c.height;
  function box(px,py,w,h,fill,stroke){
    x.fillStyle=fill; x.strokeStyle=stroke; x.lineWidth=1.5;
    x.beginPath(); x.roundRect(px,py,w,h,6); x.fill(); x.stroke();
  }
  function label(tx,ty,s,col,size,align){
    x.font=(size||12)+'px ui-monospace,Menlo,monospace'; x.fillStyle=col||'#a7b3c9';
    x.textAlign=align||'center'; x.fillText(s,tx,ty);
  }
  x.clearRect(0,0,W,H);
  // PCB
  box(70,40,W-140,H-80,'rgba(20,32,54,.9)','rgba(90,120,170,.5)');
  label(W/2,H-24,'Alchitry Pt V2  ·  schematic diagram, not to scale','#7d8aa3',12);
  // FPGA
  box(W/2-110,H/2-90,220,180,'rgba(207,170,91,.12)','rgba(207,170,91,.7)');
  label(W/2,H/2-58,'XC7A100T','#cfaa5b',18);
  label(W/2,H/2-34,'Artix-7  ·  FGG484','#a7b3c9',12);
  label(W/2,H/2+2,'63,400 LUT6','#d8e1f0',13);
  label(W/2,H/2+22,'126,800 flip flops','#a7b3c9',12);
  label(W/2,H/2+42,'4,860 Kb block RAM','#a7b3c9',12);
  label(W/2,H/2+68,'each LUT6 = one neuron','#cfaa5b',12);
  // FT2232H
  box(110,H/2-40,120,80,'rgba(91,143,212,.12)','rgba(91,143,212,.6)');
  label(170,H/2-14,'FT2232H','#5b8fd4',13);
  label(170,H/2+6,'USB / JTAG','#a7b3c9',11);
  label(170,H/2+24,'MPSSE','#7d8aa3',11);
  // DDR3
  box(W-230,H/2-40,120,80,'rgba(255,255,255,.05)','rgba(120,140,180,.5)');
  label(W-170,H/2-14,'DDR3L','#a7b3c9',13);
  label(W-170,H/2+8,'256 MB','#7d8aa3',11);
  // USB-C
  box(74,H/2-22,34,44,'rgba(255,255,255,.08)','rgba(120,140,180,.5)');
  label(91,H/2+38,'USB-C','#7d8aa3',10);
  // connections
  x.strokeStyle='rgba(91,143,212,.65)'; x.lineWidth=2;
  x.beginPath(); x.moveTo(230,H/2); x.lineTo(W/2-110,H/2); x.stroke();
  label((230+W/2-110)/2,H/2-10,'4 wires','#5b8fd4',11);
  x.strokeStyle='rgba(120,140,180,.4)';
  x.beginPath(); x.moveTo(W/2+110,H/2); x.lineTo(W-230,H/2); x.stroke();
  x.strokeStyle='rgba(91,143,212,.65)';
  x.beginPath(); x.moveTo(108,H/2); x.lineTo(110,H/2); x.stroke();
  // headers
  for(var i=0;i<3;i++){
    box(150+i*200,58,150,18,'rgba(255,255,255,.04)','rgba(120,140,180,.35)');
    box(150+i*200,H-76,150,18,'rgba(255,255,255,.04)','rgba(120,140,180,.35)');
  }
  label(W/2,52,'Alchitry Element headers','#7d8aa3',11);
})();
</script>
</body>
</html>