<!-- Three panels: build classical Hadamard matrices, search for one by backtracking, and map which orders are known. -->
<div class="tabs" role="tablist">
<button type="button" class="tab active" data-panel="p-build">Build & check</button>
<button type="button" class="tab" data-panel="p-search">Search by hand</button>
<button type="button" class="tab" data-panel="p-orders">Which orders?</button>
</div>
<section id="p-build" class="panel">
<p class="hint">Pick a construction. Black squares are +1, white squares are −1. Press <b>Check</b> to compute HHᵀ: for a Hadamard matrix every pair of distinct rows agrees in exactly half the positions, so off the diagonal everything is 0.</p>
<div class="ctl">
<label>Construction
<select id="fam">
<option value="syl">Sylvester (1867)</option>
<option value="p1">Paley I (q ≡ 3 mod 4)</option>
<option value="p2">Paley II (q ≡ 1 mod 4)</option>
</select>
</label>
<label>Size <select id="par"></select></label>
</div>
<div class="btns">
<button id="btnCheck" type="button">Check HHᵀ</button>
<button id="btnFlip" type="button" class="ghost">Flip a random entry</button>
<button id="btnRebuild" type="button" class="ghost">Rebuild</button>
</div>
<div class="views">
<figure><canvas id="cvH" aria-label="The matrix H as a grid of black and white squares"></canvas><figcaption id="capH"></figcaption></figure>
<figure><canvas id="cvG" aria-label="Heatmap of the product H times H transposed"></canvas><figcaption id="capG">HHᵀ appears here after you press Check.</figcaption></figure>
</div>
<div class="status" id="buildStatus" aria-live="polite"></div>
</section>
<section id="p-search" class="panel" hidden>
<p class="hint">The computer fixes the first row and column to +1, then adds rows one at a time, each with exactly half its entries −1 and orthogonal to every row above. When it gets stuck it backtracks. Try 6: the search proves no Hadamard matrix of order 6 exists.</p>
<div class="btns" id="searchBtns"></div>
<div class="btns">
<button id="btnStop" type="button" class="ghost">Stop</button>
</div>
<canvas id="cvS" aria-label="The partial matrix found by the backtracking search"></canvas>
<div class="status" id="searchStatus" aria-live="polite">Choose an order to start the search.</div>
</section>
<section id="p-orders" class="panel" hidden>
<p class="hint">Every multiple of 4 from 4 to 2000, colored by the simplest way we know to build a Hadamard matrix of that order. Tap a square for details.</p>
<div class="legend" id="legend"></div>
<div class="orders" id="orders"></div>
<div class="status small" id="orderInfo">Tap a square to see how that order is built.</div>
</section>
/* Color tokens for light mode; overridden below for dark mode. */
:root { --fg: #1f2933; --muted: #52606d; --line: #d9e2ec; --panel: #f4f7fa; --accent: #1d3557; --bg: #ffffff;
--c-syl: #1d3557; --c-p1: #2b6cd4; --c-p2: #6ea0ff; --c-kron: #8fb3d9; --c-other: #e9c46a; --c-new: #e76f51; }
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; }
.hint { font-size: .88rem; color: var(--muted); margin: 0 0 .6rem; line-height: 1.45; }
.tabs { display: flex; gap: .3rem; flex-wrap: wrap; margin-bottom: .7rem; }
.tab { background: transparent; color: var(--accent); border: 1px solid var(--line); }
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.ctl { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .85rem; }
.ctl label { display: flex; flex-direction: column; gap: .2rem; color: var(--muted); }
select { font: 14px system-ui, sans-serif; padding: .3rem .4rem; border-radius: 6px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); }
.btns { display: flex; gap: .45rem; flex-wrap: wrap; margin: .35rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .8rem; border: 1px solid var(--accent);
background: var(--accent); color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button:disabled { opacity: .5; cursor: default; }
.views { display: flex; gap: .8rem; flex-wrap: wrap; }
figure { margin: .3rem 0; flex: 1 1 140px; max-width: 240px; min-width: 0; }
canvas { display: block; width: 100%; max-width: 240px; height: auto; image-rendering: pixelated;
border: 1px solid var(--line); border-radius: 4px; background: var(--panel); }
#cvS { max-width: 240px; margin-top: .4rem; }
figcaption { font-size: .75rem; color: var(--muted); margin-top: .25rem; max-width: 240px; }
.status { font-size: .9rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.small { font-weight: 500; font-size: .82rem; }
.legend { display: flex; flex-wrap: wrap; gap: .35rem .8rem; font-size: .75rem; color: var(--muted); margin-bottom: .45rem; }
.legend span { display: inline-flex; align-items: center; gap: .3rem; }
.legend i { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.orders { display: flex; flex-wrap: wrap; gap: 1px; }
.orders b { width: 11px; height: 11px; border-radius: 2px; cursor: pointer; }
.orders b.sel { outline: 2px solid var(--fg); outline-offset: 0; }
.syl { background: var(--c-syl); } .p1 { background: var(--c-p1); } .p2 { background: var(--c-p2); }
.kron { background: var(--c-kron); } .other { background: var(--c-other); } .y2026 { background: var(--c-new); }
@media (prefers-color-scheme: dark) {
:root { --fg: #e4e9ef; --muted: #a3b1bf; --line: #34404d; --panel: #1b222b; --accent: #9fb2c8; --bg: #12171d;
--c-syl: #cfe0f5; --c-kron: #4f6f93; }
.tab.active { color: #12171d; }
button { color: #12171d; }
button.ghost { background: transparent; color: var(--accent); }
.status.ok { color: #5fd38a; }
.status.bad { color: #ff7b85; }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Core math: Sylvester doubling, Paley constructions, HHᵀ, backtracking search, order classification.
function sylvester(n) {
let H = [[1]];
while (H.length < n) {
const m = H.length, G = [];
for (let i = 0; i < 2 * m; i++) {
const r = [];
for (let j = 0; j < 2 * m; j++) { const s = H[i % m][j % m]; r.push(i >= m && j >= m ? -s : s); }
G.push(r);
}
H = G;
}
return H;
}
// Quadratic character mod a prime q: +1 for nonzero squares, −1 for non-squares, 0 for 0 (Euler's criterion).
function chi(a, q) {
a = ((a % q) + q) % q;
if (a === 0) return 0;
let r = 1, b = a, e = (q - 1) / 2;
while (e > 0) { if (e & 1) r = (r * b) % q; b = (b * b) % q; e = Math.floor(e / 2); }
return r === 1 ? 1 : -1;
}
function jacobsthal(q) {
const Q = [];
for (let i = 0; i < q; i++) { const r = []; for (let j = 0; j < q; j++) r.push(chi(j - i, q)); Q.push(r); }
return Q;
}
// Paley I: for q ≡ 3 (mod 4), H = I + S where S borders the Jacobsthal matrix Q (order q + 1).
function paley1(q) {
const Q = jacobsthal(q), n = q + 1, H = [];
for (let i = 0; i < n; i++) {
const r = [];
for (let j = 0; j < n; j++) {
let s;
if (i === 0 && j === 0) s = 0; else if (i === 0) s = 1; else if (j === 0) s = -1; else s = Q[i - 1][j - 1];
r.push(s + (i === j ? 1 : 0));
}
H.push(r);
}
return H;
}
// Paley II: for q ≡ 1 (mod 4), replace each entry of the bordered symmetric matrix by a 2×2 block (order 2(q + 1)).
function paley2(q) {
const Q = jacobsthal(q), m = q + 1, H = [];
const A = [[1, -1], [-1, -1]], B = [[1, 1], [1, -1]];
for (let i = 0; i < 2 * m; i++) H.push(new Array(2 * m).fill(0));
for (let i = 0; i < m; i++) for (let j = 0; j < m; j++) {
const c = (i === 0 && j === 0) ? 0 : (i === 0 || j === 0) ? 1 : Q[i - 1][j - 1];
for (let a = 0; a < 2; a++) for (let b = 0; b < 2; b++) H[2 * i + a][2 * j + b] = c === 0 ? A[a][b] : c * B[a][b];
}
return H;
}
// HHᵀ: entry (i, j) is the dot product of rows i and j; a Hadamard matrix gives n on the diagonal and 0 elsewhere.
function gram(H) {
const n = H.length, G = [];
for (let i = 0; i < n; i++) {
const r = [];
for (let j = 0; j < n; j++) { let s = 0; for (let k = 0; k < n; k++) s += H[i][k] * H[j][k]; r.push(s); }
G.push(r);
}
return G;
}
function badPairs(G) {
let bad = 0;
for (let i = 0; i < G.length; i++) for (let j = i + 1; j < G.length; j++) if (G[i][j] !== 0) bad++;
return bad;
}
// Backtracking: rows are bitmasks (bit = 1 means −1). Each new row has exactly n/2 minus signs, starts with +1, and must differ in exactly n/2 places from every earlier row.
function popcount(x) { let c = 0; while (x) { x &= x - 1; c++; } return c; }
function makeSearch(n) {
const half = n / 2, cand = [];
if (n % 2 === 0) for (let m = 0; m < (1 << n); m += 2) if (popcount(m) === half) cand.push(m);
return { n: n, half: half, cand: cand, rows: [0], chosen: [], ptr: 0, nodes: 0, backtracks: 0, done: false, found: false };
}
function stepSearch(S) {
if (S.done) return;
if (S.rows.length === S.n) { S.done = true; S.found = true; return; }
for (let i = S.ptr; i < S.cand.length; i++) {
const c = S.cand[i];
let ok = true;
for (const r of S.rows) if (popcount(c ^ r) !== S.half) { ok = false; break; }
if (ok) { S.rows.push(c); S.chosen.push(i); S.ptr = i + 1; S.nodes++; return; }
}
if (!S.chosen.length) { S.done = true; S.found = false; return; }
S.rows.pop(); S.ptr = S.chosen.pop() + 1; S.backtracks++;
}
function rowsToMatrix(rows, n) { return rows.map(m => { const r = []; for (let j = 0; j < n; j++) r.push((m >> j) & 1 ? -1 : 1); return r; }); }
// Classify each multiple of 4 by the simplest classical construction that reaches it.
function isPrimePower(x) {
if (x < 2) return false;
let p = 2;
while (p * p <= x && x % p !== 0) p++;
if (x % p !== 0) p = x;
while (x % p === 0) x /= p;
return x === 1;
}
const FOUND_2026 = [668, 716, 892, 1132, 1244, 1388, 1436, 1676, 1772, 1916, 1948, 1964];
function classify(maxN) {
const out = {}, classical = new Set([1, 2]);
for (let n = 4; n <= maxN; n += 4) {
let c = null, a = 0, b = 0;
if ((n & (n - 1)) === 0) c = 'syl';
else if (isPrimePower(n - 1) && (n - 1) % 4 === 3) c = 'p1';
else if (isPrimePower(n / 2 - 1) && (n / 2 - 1) % 4 === 1) c = 'p2';
else for (const x of classical) if (x > 1 && n % x === 0 && classical.has(n / x)) { c = 'kron'; a = x; b = n / x; break; }
if (c) classical.add(n); else c = FOUND_2026.includes(n) ? 'y2026' : 'other';
out[n] = { c: c, a: a, b: b };
}
return out;
}
// Drawing helpers and theme colors.
const dark = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
const THEME = dark ? { plus: '#e4e9ef', minus: '#1b222b', mid: [40, 48, 58] } : { plus: '#1f2933', minus: '#ffffff', mid: [233, 237, 242] };
function drawMatrix(cv, M, n, pending) {
const cell = Math.max(2, Math.floor(240 / Math.max(n, 1)));
cv.width = n * cell; cv.height = n * cell;
const ctx = cv.getContext('2d');
ctx.fillStyle = THEME.mid.length ? 'rgb(' + THEME.mid.join(',') + ')' : '#ccc';
ctx.fillRect(0, 0, cv.width, cv.height);
for (let i = 0; i < M.length; i++) for (let j = 0; j < n; j++) {
ctx.fillStyle = M[i][j] === 1 ? THEME.plus : THEME.minus;
ctx.fillRect(j * cell, i * cell, cell, cell);
}
if (pending && M.length < n) { ctx.fillStyle = 'rgba(231,111,81,.35)'; ctx.fillRect(0, M.length * cell, cv.width, cell); }
}
function heat(v, n) {
const t = Math.max(-1, Math.min(1, v / n));
const to = t >= 0 ? [43, 108, 212] : [231, 111, 81], a = Math.abs(t);
return 'rgb(' + THEME.mid.map((m, k) => Math.round(m + (to[k] - m) * a)).join(',') + ')';
}
function drawGram(cv, G) {
const n = G.length, cell = Math.max(2, Math.floor(240 / n));
cv.width = n * cell; cv.height = n * cell;
const ctx = cv.getContext('2d');
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) { ctx.fillStyle = heat(G[i][j], n); ctx.fillRect(j * cell, i * cell, cell, cell); }
}
// Tab switching.
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x === t));
document.querySelectorAll('.panel').forEach(p => { p.hidden = p.id !== t.dataset.panel; });
}));
// Panel 1: build, check and break a matrix.
const PARAMS = { syl: [1, 2, 4, 8, 16, 32], p1: [3, 7, 11, 19, 23, 31, 43], p2: [5, 13, 17] };
const fam = document.getElementById('fam'), par = document.getElementById('par');
const cvH = document.getElementById('cvH'), cvG = document.getElementById('cvG');
const capH = document.getElementById('capH'), capG = document.getElementById('capG');
const buildStatus = document.getElementById('buildStatus');
let H = [[1]], flips = 0;
function fillParams() {
par.innerHTML = '';
for (const v of PARAMS[fam.value]) {
const o = document.createElement('option'); o.value = v;
const n = fam.value === 'syl' ? v : fam.value === 'p1' ? v + 1 : 2 * (v + 1);
o.textContent = fam.value === 'syl' ? i18nFmt("n = %n%", { n: n }) : i18nFmt("q = %q% → n = %n%", { q: v, n: n });
par.appendChild(o);
}
if (fam.value === 'syl') par.value = '8';
}
function setStatus(el, t, cls) { el.textContent = t; el.className = 'status' + (cls ? ' ' + cls : ''); }
function build() {
const v = Number(par.value);
H = fam.value === 'syl' ? sylvester(v) : fam.value === 'p1' ? paley1(v) : paley2(v);
flips = 0;
drawMatrix(cvH, H, H.length, false);
capH.textContent = i18nFmt("H of order %n% · black = +1, white = −1", { n: H.length });
const c = cvG.getContext('2d'); cvG.width = cvH.width; cvG.height = cvH.height; c.clearRect(0, 0, cvG.width, cvG.height);
capG.textContent = "HHᵀ appears here after you press Check.";
setStatus(buildStatus, i18nFmt("Built a %n%×%n% matrix of ±1 entries. Is every pair of rows orthogonal?", { n: H.length }), '');
}
function check() {
const G = gram(H), n = H.length, pairs = n * (n - 1) / 2, bad = badPairs(G);
drawGram(cvG, G);
capG.textContent = i18nFmt("HHᵀ: blue diagonal = %n%, gray = 0, orange = negative", { n: n });
if (bad === 0) setStatus(buildStatus, i18nFmt("✓ HHᵀ = %n%·I — all %pairs% pairs of rows are orthogonal. This is a Hadamard matrix.", { n: n, pairs: pairs }), 'ok');
else setStatus(buildStatus, i18nFmt("✗ %bad% of %pairs% row pairs are no longer orthogonal (after %flips% flip(s)). One wrong sign breaks it.", { bad: bad, pairs: pairs, flips: flips }), 'bad');
}
function flip() {
const n = H.length, i = Math.floor(Math.random() * n), j = Math.floor(Math.random() * n);
H[i][j] = -H[i][j]; flips++;
drawMatrix(cvH, H, n, false);
setStatus(buildStatus, i18nFmt("Flipped the entry in row %r%, column %c%. Check again.", { r: i + 1, c: j + 1 }), '');
}
fam.addEventListener('change', () => { fillParams(); build(); });
par.addEventListener('change', build);
document.getElementById('btnCheck').addEventListener('click', check);
document.getElementById('btnFlip').addEventListener('click', flip);
document.getElementById('btnRebuild').addEventListener('click', build);
fillParams(); build();
// Panel 2: animated backtracking search.
const cvS = document.getElementById('cvS'), searchStatus = document.getElementById('searchStatus');
const searchBtns = document.getElementById('searchBtns');
let S = null, timer = null;
function stopSearch() { if (timer) { clearTimeout(timer); timer = null; } }
function tick() {
const per = 1;
for (let k = 0; k < per && !S.done; k++) stepSearch(S);
drawMatrix(cvS, rowsToMatrix(S.rows, S.n), S.n, !S.done);
if (S.done) {
timer = null;
if (S.found) setStatus(searchStatus, i18nFmt("✓ Found a Hadamard matrix of order %n% after %nodes% placements and %bt% backtracks.", { n: S.n, nodes: S.nodes, bt: S.backtracks }), 'ok');
else setStatus(searchStatus, i18nFmt("✗ Search exhausted after %nodes% placements: no Hadamard matrix of order %n% exists.", { n: S.n, nodes: S.nodes, bt: S.backtracks }), 'bad');
return;
}
setStatus(searchStatus, i18nFmt("Rows placed: %k%/%n% · placements tried: %nodes% · backtracks: %bt%", { k: S.rows.length, n: S.n, nodes: S.nodes, bt: S.backtracks }), '');
timer = setTimeout(tick, 140);
}
[4, 6, 8, 12].forEach(n => {
const b = document.createElement('button'); b.type = 'button';
b.textContent = i18nFmt("Search n = %n%", { n: n });
b.addEventListener('click', () => { stopSearch(); S = makeSearch(n); tick(); });
searchBtns.appendChild(b);
});
document.getElementById('btnStop').addEventListener('click', () => { stopSearch(); setStatus(searchStatus, "Search stopped.", ''); });
drawMatrix(cvS, [], 4, false);
// Panel 3: map of orders up to 2000.
const CLS = classify(2000);
const NAMES = { syl: "Sylvester (power of 2)", p1: "Paley I", p2: "Paley II", kron: "Product of the above", other: "Other methods", y2026: "Open until Aug 2026" };
const legend = document.getElementById('legend'), orders = document.getElementById('orders'), info = document.getElementById('orderInfo');
const counts = {};
Object.values(CLS).forEach(o => { counts[o.c] = (counts[o.c] || 0) + 1; });
Object.keys(NAMES).forEach(k => {
const s = document.createElement('span');
const i = document.createElement('i'); i.className = k; s.appendChild(i);
s.appendChild(document.createTextNode(i18nFmt("%name% (%count%)", { name: NAMES[k], count: counts[k] || 0 })));
legend.appendChild(s);
});
function describe(n) {
const o = CLS[n];
if (n === 92) return "92: the first order beyond Sylvester, Paley and their products; found in 1962 by Baumert, Golomb and Hall with a computer search (Williamson type).";
if (n === 428) return "428: the smallest open case until Kharaghani and Tayfeh-Rezaie built it (announced 2004, published 2005). After that, 668 was the smallest open order.";
if (o.c === 'syl') return i18nFmt("%n% = 2^%k%: Sylvester’s doubling construction (1867).", { n: n, k: Math.round(Math.log2(n)) });
if (o.c === 'p1') return i18nFmt("%n% = %q% + 1 with %q% a prime power ≡ 3 (mod 4): Paley’s first construction (1933).", { n: n, q: n - 1 });
if (o.c === 'p2') return i18nFmt("%n% = 2(%q% + 1) with %q% a prime power ≡ 1 (mod 4): Paley’s second construction (1933).", { n: n, q: n / 2 - 1 });
if (o.c === 'kron') return i18nFmt("%n% = %a% × %b%: Kronecker product of two smaller Sylvester/Paley matrices.", { n: n, a: o.a, b: o.b });
if (o.c === 'y2026') return i18nFmt("%n%: no matrix was known for decades; a construction was announced in August 2026 by Alpöge, Voinov and Reynolds-Haertle, working with an AI model.", { n: n });
return i18nFmt("%n%: out of reach of Sylvester, Paley and their products; known through other methods (Williamson, Goethals–Seidel, Turyn, computer searches…).", { n: n });
}
let selected = null;
Object.keys(CLS).forEach(k => {
const n = Number(k), b = document.createElement('b');
b.className = CLS[n].c; b.title = String(n);
b.addEventListener('click', () => { if (selected) selected.classList.remove('sel'); selected = b; b.classList.add('sel'); info.textContent = describe(n); });
orders.appendChild(b);
});
<!-- Three panels: build classical Hadamard matrices, search for one by backtracking, and map which orders are known. -->
<div class="tabs" role="tablist">
<button type="button" class="tab active" data-panel="p-build">Build & check</button>
<button type="button" class="tab" data-panel="p-search">Search by hand</button>
<button type="button" class="tab" data-panel="p-orders">Which orders?</button>
</div>
<section id="p-build" class="panel">
<p class="hint">Pick a construction. Black squares are +1, white squares are −1. Press <b>Check</b> to compute HHᵀ: for a Hadamard matrix every pair of distinct rows agrees in exactly half the positions, so off the diagonal everything is 0.</p>
<div class="ctl">
<label>Construction
<select id="fam">
<option value="syl">Sylvester (1867)</option>
<option value="p1">Paley I (q ≡ 3 mod 4)</option>
<option value="p2">Paley II (q ≡ 1 mod 4)</option>
</select>
</label>
<label>Size <select id="par"></select></label>
</div>
<div class="btns">
<button id="btnCheck" type="button">Check HHᵀ</button>
<button id="btnFlip" type="button" class="ghost">Flip a random entry</button>
<button id="btnRebuild" type="button" class="ghost">Rebuild</button>
</div>
<div class="views">
<figure><canvas id="cvH" aria-label="The matrix H as a grid of black and white squares"></canvas><figcaption id="capH"></figcaption></figure>
<figure><canvas id="cvG" aria-label="Heatmap of the product H times H transposed"></canvas><figcaption id="capG">HHᵀ appears here after you press Check.</figcaption></figure>
</div>
<div class="status" id="buildStatus" aria-live="polite"></div>
</section>
<section id="p-search" class="panel" hidden>
<p class="hint">The computer fixes the first row and column to +1, then adds rows one at a time, each with exactly half its entries −1 and orthogonal to every row above. When it gets stuck it backtracks. Try 6: the search proves no Hadamard matrix of order 6 exists.</p>
<div class="btns" id="searchBtns"></div>
<div class="btns">
<button id="btnStop" type="button" class="ghost">Stop</button>
</div>
<canvas id="cvS" aria-label="The partial matrix found by the backtracking search"></canvas>
<div class="status" id="searchStatus" aria-live="polite">Choose an order to start the search.</div>
</section>
<section id="p-orders" class="panel" hidden>
<p class="hint">Every multiple of 4 from 4 to 2000, colored by the simplest way we know to build a Hadamard matrix of that order. Tap a square for details.</p>
<div class="legend" id="legend"></div>
<div class="orders" id="orders"></div>
<div class="status small" id="orderInfo">Tap a square to see how that order is built.</div>
</section>
/* Color tokens for light mode; overridden below for dark mode. */
:root { --fg: #1f2933; --muted: #52606d; --line: #d9e2ec; --panel: #f4f7fa; --accent: #1d3557; --bg: #ffffff;
--c-syl: #1d3557; --c-p1: #2b6cd4; --c-p2: #6ea0ff; --c-kron: #8fb3d9; --c-other: #e9c46a; --c-new: #e76f51; }
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; }
.hint { font-size: .88rem; color: var(--muted); margin: 0 0 .6rem; line-height: 1.45; }
.tabs { display: flex; gap: .3rem; flex-wrap: wrap; margin-bottom: .7rem; }
.tab { background: transparent; color: var(--accent); border: 1px solid var(--line); }
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.ctl { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .85rem; }
.ctl label { display: flex; flex-direction: column; gap: .2rem; color: var(--muted); }
select { font: 14px system-ui, sans-serif; padding: .3rem .4rem; border-radius: 6px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); }
.btns { display: flex; gap: .45rem; flex-wrap: wrap; margin: .35rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .8rem; border: 1px solid var(--accent);
background: var(--accent); color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button:disabled { opacity: .5; cursor: default; }
.views { display: flex; gap: .8rem; flex-wrap: wrap; }
figure { margin: .3rem 0; flex: 1 1 140px; max-width: 240px; min-width: 0; }
canvas { display: block; width: 100%; max-width: 240px; height: auto; image-rendering: pixelated;
border: 1px solid var(--line); border-radius: 4px; background: var(--panel); }
#cvS { max-width: 240px; margin-top: .4rem; }
figcaption { font-size: .75rem; color: var(--muted); margin-top: .25rem; max-width: 240px; }
.status { font-size: .9rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.small { font-weight: 500; font-size: .82rem; }
.legend { display: flex; flex-wrap: wrap; gap: .35rem .8rem; font-size: .75rem; color: var(--muted); margin-bottom: .45rem; }
.legend span { display: inline-flex; align-items: center; gap: .3rem; }
.legend i { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.orders { display: flex; flex-wrap: wrap; gap: 1px; }
.orders b { width: 11px; height: 11px; border-radius: 2px; cursor: pointer; }
.orders b.sel { outline: 2px solid var(--fg); outline-offset: 0; }
.syl { background: var(--c-syl); } .p1 { background: var(--c-p1); } .p2 { background: var(--c-p2); }
.kron { background: var(--c-kron); } .other { background: var(--c-other); } .y2026 { background: var(--c-new); }
@media (prefers-color-scheme: dark) {
:root { --fg: #e4e9ef; --muted: #a3b1bf; --line: #34404d; --panel: #1b222b; --accent: #9fb2c8; --bg: #12171d;
--c-syl: #cfe0f5; --c-kron: #4f6f93; }
.tab.active { color: #12171d; }
button { color: #12171d; }
button.ghost { background: transparent; color: var(--accent); }
.status.ok { color: #5fd38a; }
.status.bad { color: #ff7b85; }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Core math: Sylvester doubling, Paley constructions, HHᵀ, backtracking search, order classification.
function sylvester(n) {
let H = [[1]];
while (H.length < n) {
const m = H.length, G = [];
for (let i = 0; i < 2 * m; i++) {
const r = [];
for (let j = 0; j < 2 * m; j++) { const s = H[i % m][j % m]; r.push(i >= m && j >= m ? -s : s); }
G.push(r);
}
H = G;
}
return H;
}
// Quadratic character mod a prime q: +1 for nonzero squares, −1 for non-squares, 0 for 0 (Euler's criterion).
function chi(a, q) {
a = ((a % q) + q) % q;
if (a === 0) return 0;
let r = 1, b = a, e = (q - 1) / 2;
while (e > 0) { if (e & 1) r = (r * b) % q; b = (b * b) % q; e = Math.floor(e / 2); }
return r === 1 ? 1 : -1;
}
function jacobsthal(q) {
const Q = [];
for (let i = 0; i < q; i++) { const r = []; for (let j = 0; j < q; j++) r.push(chi(j - i, q)); Q.push(r); }
return Q;
}
// Paley I: for q ≡ 3 (mod 4), H = I + S where S borders the Jacobsthal matrix Q (order q + 1).
function paley1(q) {
const Q = jacobsthal(q), n = q + 1, H = [];
for (let i = 0; i < n; i++) {
const r = [];
for (let j = 0; j < n; j++) {
let s;
if (i === 0 && j === 0) s = 0; else if (i === 0) s = 1; else if (j === 0) s = -1; else s = Q[i - 1][j - 1];
r.push(s + (i === j ? 1 : 0));
}
H.push(r);
}
return H;
}
// Paley II: for q ≡ 1 (mod 4), replace each entry of the bordered symmetric matrix by a 2×2 block (order 2(q + 1)).
function paley2(q) {
const Q = jacobsthal(q), m = q + 1, H = [];
const A = [[1, -1], [-1, -1]], B = [[1, 1], [1, -1]];
for (let i = 0; i < 2 * m; i++) H.push(new Array(2 * m).fill(0));
for (let i = 0; i < m; i++) for (let j = 0; j < m; j++) {
const c = (i === 0 && j === 0) ? 0 : (i === 0 || j === 0) ? 1 : Q[i - 1][j - 1];
for (let a = 0; a < 2; a++) for (let b = 0; b < 2; b++) H[2 * i + a][2 * j + b] = c === 0 ? A[a][b] : c * B[a][b];
}
return H;
}
// HHᵀ: entry (i, j) is the dot product of rows i and j; a Hadamard matrix gives n on the diagonal and 0 elsewhere.
function gram(H) {
const n = H.length, G = [];
for (let i = 0; i < n; i++) {
const r = [];
for (let j = 0; j < n; j++) { let s = 0; for (let k = 0; k < n; k++) s += H[i][k] * H[j][k]; r.push(s); }
G.push(r);
}
return G;
}
function badPairs(G) {
let bad = 0;
for (let i = 0; i < G.length; i++) for (let j = i + 1; j < G.length; j++) if (G[i][j] !== 0) bad++;
return bad;
}
// Backtracking: rows are bitmasks (bit = 1 means −1). Each new row has exactly n/2 minus signs, starts with +1, and must differ in exactly n/2 places from every earlier row.
function popcount(x) { let c = 0; while (x) { x &= x - 1; c++; } return c; }
function makeSearch(n) {
const half = n / 2, cand = [];
if (n % 2 === 0) for (let m = 0; m < (1 << n); m += 2) if (popcount(m) === half) cand.push(m);
return { n: n, half: half, cand: cand, rows: [0], chosen: [], ptr: 0, nodes: 0, backtracks: 0, done: false, found: false };
}
function stepSearch(S) {
if (S.done) return;
if (S.rows.length === S.n) { S.done = true; S.found = true; return; }
for (let i = S.ptr; i < S.cand.length; i++) {
const c = S.cand[i];
let ok = true;
for (const r of S.rows) if (popcount(c ^ r) !== S.half) { ok = false; break; }
if (ok) { S.rows.push(c); S.chosen.push(i); S.ptr = i + 1; S.nodes++; return; }
}
if (!S.chosen.length) { S.done = true; S.found = false; return; }
S.rows.pop(); S.ptr = S.chosen.pop() + 1; S.backtracks++;
}
function rowsToMatrix(rows, n) { return rows.map(m => { const r = []; for (let j = 0; j < n; j++) r.push((m >> j) & 1 ? -1 : 1); return r; }); }
// Classify each multiple of 4 by the simplest classical construction that reaches it.
function isPrimePower(x) {
if (x < 2) return false;
let p = 2;
while (p * p <= x && x % p !== 0) p++;
if (x % p !== 0) p = x;
while (x % p === 0) x /= p;
return x === 1;
}
const FOUND_2026 = [668, 716, 892, 1132, 1244, 1388, 1436, 1676, 1772, 1916, 1948, 1964];
function classify(maxN) {
const out = {}, classical = new Set([1, 2]);
for (let n = 4; n <= maxN; n += 4) {
let c = null, a = 0, b = 0;
if ((n & (n - 1)) === 0) c = 'syl';
else if (isPrimePower(n - 1) && (n - 1) % 4 === 3) c = 'p1';
else if (isPrimePower(n / 2 - 1) && (n / 2 - 1) % 4 === 1) c = 'p2';
else for (const x of classical) if (x > 1 && n % x === 0 && classical.has(n / x)) { c = 'kron'; a = x; b = n / x; break; }
if (c) classical.add(n); else c = FOUND_2026.includes(n) ? 'y2026' : 'other';
out[n] = { c: c, a: a, b: b };
}
return out;
}
// Drawing helpers and theme colors.
const dark = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
const THEME = dark ? { plus: '#e4e9ef', minus: '#1b222b', mid: [40, 48, 58] } : { plus: '#1f2933', minus: '#ffffff', mid: [233, 237, 242] };
function drawMatrix(cv, M, n, pending) {
const cell = Math.max(2, Math.floor(240 / Math.max(n, 1)));
cv.width = n * cell; cv.height = n * cell;
const ctx = cv.getContext('2d');
ctx.fillStyle = THEME.mid.length ? 'rgb(' + THEME.mid.join(',') + ')' : '#ccc';
ctx.fillRect(0, 0, cv.width, cv.height);
for (let i = 0; i < M.length; i++) for (let j = 0; j < n; j++) {
ctx.fillStyle = M[i][j] === 1 ? THEME.plus : THEME.minus;
ctx.fillRect(j * cell, i * cell, cell, cell);
}
if (pending && M.length < n) { ctx.fillStyle = 'rgba(231,111,81,.35)'; ctx.fillRect(0, M.length * cell, cv.width, cell); }
}
function heat(v, n) {
const t = Math.max(-1, Math.min(1, v / n));
const to = t >= 0 ? [43, 108, 212] : [231, 111, 81], a = Math.abs(t);
return 'rgb(' + THEME.mid.map((m, k) => Math.round(m + (to[k] - m) * a)).join(',') + ')';
}
function drawGram(cv, G) {
const n = G.length, cell = Math.max(2, Math.floor(240 / n));
cv.width = n * cell; cv.height = n * cell;
const ctx = cv.getContext('2d');
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) { ctx.fillStyle = heat(G[i][j], n); ctx.fillRect(j * cell, i * cell, cell, cell); }
}
// Tab switching.
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x === t));
document.querySelectorAll('.panel').forEach(p => { p.hidden = p.id !== t.dataset.panel; });
}));
// Panel 1: build, check and break a matrix.
const PARAMS = { syl: [1, 2, 4, 8, 16, 32], p1: [3, 7, 11, 19, 23, 31, 43], p2: [5, 13, 17] };
const fam = document.getElementById('fam'), par = document.getElementById('par');
const cvH = document.getElementById('cvH'), cvG = document.getElementById('cvG');
const capH = document.getElementById('capH'), capG = document.getElementById('capG');
const buildStatus = document.getElementById('buildStatus');
let H = [[1]], flips = 0;
function fillParams() {
par.innerHTML = '';
for (const v of PARAMS[fam.value]) {
const o = document.createElement('option'); o.value = v;
const n = fam.value === 'syl' ? v : fam.value === 'p1' ? v + 1 : 2 * (v + 1);
o.textContent = fam.value === 'syl' ? i18nFmt("n = %n%", { n: n }) : i18nFmt("q = %q% → n = %n%", { q: v, n: n });
par.appendChild(o);
}
if (fam.value === 'syl') par.value = '8';
}
function setStatus(el, t, cls) { el.textContent = t; el.className = 'status' + (cls ? ' ' + cls : ''); }
function build() {
const v = Number(par.value);
H = fam.value === 'syl' ? sylvester(v) : fam.value === 'p1' ? paley1(v) : paley2(v);
flips = 0;
drawMatrix(cvH, H, H.length, false);
capH.textContent = i18nFmt("H of order %n% · black = +1, white = −1", { n: H.length });
const c = cvG.getContext('2d'); cvG.width = cvH.width; cvG.height = cvH.height; c.clearRect(0, 0, cvG.width, cvG.height);
capG.textContent = "HHᵀ appears here after you press Check.";
setStatus(buildStatus, i18nFmt("Built a %n%×%n% matrix of ±1 entries. Is every pair of rows orthogonal?", { n: H.length }), '');
}
function check() {
const G = gram(H), n = H.length, pairs = n * (n - 1) / 2, bad = badPairs(G);
drawGram(cvG, G);
capG.textContent = i18nFmt("HHᵀ: blue diagonal = %n%, gray = 0, orange = negative", { n: n });
if (bad === 0) setStatus(buildStatus, i18nFmt("✓ HHᵀ = %n%·I — all %pairs% pairs of rows are orthogonal. This is a Hadamard matrix.", { n: n, pairs: pairs }), 'ok');
else setStatus(buildStatus, i18nFmt("✗ %bad% of %pairs% row pairs are no longer orthogonal (after %flips% flip(s)). One wrong sign breaks it.", { bad: bad, pairs: pairs, flips: flips }), 'bad');
}
function flip() {
const n = H.length, i = Math.floor(Math.random() * n), j = Math.floor(Math.random() * n);
H[i][j] = -H[i][j]; flips++;
drawMatrix(cvH, H, n, false);
setStatus(buildStatus, i18nFmt("Flipped the entry in row %r%, column %c%. Check again.", { r: i + 1, c: j + 1 }), '');
}
fam.addEventListener('change', () => { fillParams(); build(); });
par.addEventListener('change', build);
document.getElementById('btnCheck').addEventListener('click', check);
document.getElementById('btnFlip').addEventListener('click', flip);
document.getElementById('btnRebuild').addEventListener('click', build);
fillParams(); build();
// Panel 2: animated backtracking search.
const cvS = document.getElementById('cvS'), searchStatus = document.getElementById('searchStatus');
const searchBtns = document.getElementById('searchBtns');
let S = null, timer = null;
function stopSearch() { if (timer) { clearTimeout(timer); timer = null; } }
function tick() {
const per = 1;
for (let k = 0; k < per && !S.done; k++) stepSearch(S);
drawMatrix(cvS, rowsToMatrix(S.rows, S.n), S.n, !S.done);
if (S.done) {
timer = null;
if (S.found) setStatus(searchStatus, i18nFmt("✓ Found a Hadamard matrix of order %n% after %nodes% placements and %bt% backtracks.", { n: S.n, nodes: S.nodes, bt: S.backtracks }), 'ok');
else setStatus(searchStatus, i18nFmt("✗ Search exhausted after %nodes% placements: no Hadamard matrix of order %n% exists.", { n: S.n, nodes: S.nodes, bt: S.backtracks }), 'bad');
return;
}
setStatus(searchStatus, i18nFmt("Rows placed: %k%/%n% · placements tried: %nodes% · backtracks: %bt%", { k: S.rows.length, n: S.n, nodes: S.nodes, bt: S.backtracks }), '');
timer = setTimeout(tick, 140);
}
[4, 6, 8, 12].forEach(n => {
const b = document.createElement('button'); b.type = 'button';
b.textContent = i18nFmt("Search n = %n%", { n: n });
b.addEventListener('click', () => { stopSearch(); S = makeSearch(n); tick(); });
searchBtns.appendChild(b);
});
document.getElementById('btnStop').addEventListener('click', () => { stopSearch(); setStatus(searchStatus, "Search stopped.", ''); });
drawMatrix(cvS, [], 4, false);
// Panel 3: map of orders up to 2000.
const CLS = classify(2000);
const NAMES = { syl: "Sylvester (power of 2)", p1: "Paley I", p2: "Paley II", kron: "Product of the above", other: "Other methods", y2026: "Open until Aug 2026" };
const legend = document.getElementById('legend'), orders = document.getElementById('orders'), info = document.getElementById('orderInfo');
const counts = {};
Object.values(CLS).forEach(o => { counts[o.c] = (counts[o.c] || 0) + 1; });
Object.keys(NAMES).forEach(k => {
const s = document.createElement('span');
const i = document.createElement('i'); i.className = k; s.appendChild(i);
s.appendChild(document.createTextNode(i18nFmt("%name% (%count%)", { name: NAMES[k], count: counts[k] || 0 })));
legend.appendChild(s);
});
function describe(n) {
const o = CLS[n];
if (n === 92) return "92: the first order beyond Sylvester, Paley and their products; found in 1962 by Baumert, Golomb and Hall with a computer search (Williamson type).";
if (n === 428) return "428: the smallest open case until Kharaghani and Tayfeh-Rezaie built it (announced 2004, published 2005). After that, 668 was the smallest open order.";
if (o.c === 'syl') return i18nFmt("%n% = 2^%k%: Sylvester’s doubling construction (1867).", { n: n, k: Math.round(Math.log2(n)) });
if (o.c === 'p1') return i18nFmt("%n% = %q% + 1 with %q% a prime power ≡ 3 (mod 4): Paley’s first construction (1933).", { n: n, q: n - 1 });
if (o.c === 'p2') return i18nFmt("%n% = 2(%q% + 1) with %q% a prime power ≡ 1 (mod 4): Paley’s second construction (1933).", { n: n, q: n / 2 - 1 });
if (o.c === 'kron') return i18nFmt("%n% = %a% × %b%: Kronecker product of two smaller Sylvester/Paley matrices.", { n: n, a: o.a, b: o.b });
if (o.c === 'y2026') return i18nFmt("%n%: no matrix was known for decades; a construction was announced in August 2026 by Alpöge, Voinov and Reynolds-Haertle, working with an AI model.", { n: n });
return i18nFmt("%n%: out of reach of Sylvester, Paley and their products; known through other methods (Williamson, Goethals–Seidel, Turyn, computer searches…).", { n: n });
}
let selected = null;
Object.keys(CLS).forEach(k => {
const n = Number(k), b = document.createElement('b');
b.className = CLS[n].c; b.title = String(n);
b.addEventListener('click', () => { if (selected) selected.classList.remove('sel'); selected = b; b.classList.add('sel'); info.textContent = describe(n); });
orders.appendChild(b);
});
Notice the asymmetry. Checking a candidate takes one matrix product. Finding one of order 668 took decades of searching.
Comments
Loading comments...