<p class="hint">A <b>2-local Hamiltonian</b> on 4 qubits: each edge has an interaction
energy between two qubits. Click qubits to toggle |0⟩ / |1⟩ and watch the energy
change. Can you reach the <b>minimum energy</b> (ground state)?</p>
<div id="canvas-wrap"><canvas id="canvas" width="340" height="220"></canvas></div>
<div class="energy-row">
<span>Total energy: <b id="energy-val">—</b></span>
<span id="energy-label" class="label"></span>
</div>
<div class="btns">
<button id="btn-find" type="button">Find ground state</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
#canvas-wrap { display: flex; justify-content: center; margin: .4rem 0 .2rem; }
canvas { border-radius: 10px; background: #f4f7fa; border: 1px solid #dde3ea; cursor: pointer; }
.energy-row { display: flex; align-items: center; gap: .8rem; font-size: 1rem; margin: .4rem 0; }
.energy-row b { font-size: 1.15rem; }
.label { font-size: .82rem; font-weight: 600; padding: .15rem .55rem; border-radius: 12px; }
.label.ground { background: #d1fae5; color: #065f46; }
.label.high { background: #fee2e2; color: #991b1b; }
.label.mid { background: #fef3c7; color: #92400e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .2rem; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2-Local Hamiltonian demo on 4 qubits.
// Each term H_{ij}(s_i, s_j) penalizes certain qubit configurations.
// We use an Ising-like model: H = sum_{(i,j)} J_{ij} * (1 - 2*s_i) * (1 - 2*s_j)
// where s_i in {0,1} and J_{ij} is the coupling strength.
// Negative J -> ferromagnetic (same spin preferred), Positive J -> antiferromagnetic.
// Total energy = sum of all edge terms.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// 4 qubits positioned at corners of a square, centered
const qpos = [
{ x: 90, y: 70 }, // qubit 0
{ x: 250, y: 70 }, // qubit 1
{ x: 250, y: 160 }, // qubit 2
{ x: 90, y: 160 }, // qubit 3
];
// Edges: [i, j, J_ij, label]
// Negative = ferromagnetic (same-spin = low energy), Positive = antiferromagnetic
const edges = [
[0, 1, -1.0, '-1'], // top
[1, 2, 2.0, '+2'], // right
[2, 3, -1.5, '-1.5'], // bottom
[0, 3, 1.5, '+1.5'], // left
[0, 2, 1.0, '+1'], // diagonal
];
// Qubit states: 0 = |0⟩, 1 = |1⟩
const state = [0, 0, 0, 0];
const R = 24; // qubit radius
function spin(s) { return 1 - 2 * s; } // 0->+1, 1->-1
function totalEnergy(st) {
let E = 0;
for (const [i, j, J] of edges) E += J * spin(st[i]) * spin(st[j]);
return E;
}
function groundState() {
let best = Infinity, bestMask = 0;
for (let mask = 0; mask < 16; mask++) {
const st = [mask >> 3 & 1, mask >> 2 & 1, mask >> 1 & 1, mask & 1];
const E = totalEnergy(st);
if (E < best) { best = E; bestMask = mask; }
}
return bestMask;
}
function edgeEnergy(i, j, J) {
return J * spin(state[i]) * spin(state[j]);
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw edges
for (const [i, j, J, lbl] of edges) {
const pi = qpos[i], pj = qpos[j];
const ee = edgeEnergy(i, j, J);
// Color edge: negative contribution = green tint, positive = red tint
const t = Math.min(1, Math.abs(ee) / 3);
if (ee < 0) {
ctx.strokeStyle = `rgba(16,185,129,${0.3 + 0.5 * t})`;
} else if (ee > 0) {
ctx.strokeStyle = `rgba(239,68,68,${0.3 + 0.5 * t})`;
} else {
ctx.strokeStyle = '#c0cdd8';
}
ctx.lineWidth = 2.5 + 2 * t;
ctx.beginPath(); ctx.moveTo(pi.x, pi.y); ctx.lineTo(pj.x, pj.y); ctx.stroke();
// Edge label: coupling constant and contribution
const mx = (pi.x + pj.x) / 2, my = (pi.y + pj.y) / 2;
const dx = pj.x - pi.x, dy = pj.y - pi.y;
const len = Math.sqrt(dx * dx + dy * dy);
// Offset label perpendicular to edge
const ox = -dy / len * 14, oy = dx / len * 14;
ctx.font = '600 11px system-ui, sans-serif';
ctx.fillStyle = '#555';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('J=' + lbl, mx + ox, my + oy);
}
// Draw qubits
for (let i = 0; i < 4; i++) {
const p = qpos[i];
const s = state[i];
ctx.beginPath();
ctx.arc(p.x, p.y, R, 0, 2 * Math.PI);
ctx.fillStyle = s === 0 ? '#dbeafe' : '#fde68a';
ctx.fill();
ctx.strokeStyle = s === 0 ? '#2563eb' : '#d97706';
ctx.lineWidth = 2.2;
ctx.stroke();
ctx.font = '700 15px system-ui, monospace';
ctx.fillStyle = s === 0 ? '#1e40af' : '#92400e';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(s === 0 ? '|0⟩' : '|1⟩', p.x, p.y);
// Qubit index
ctx.font = '500 11px system-ui, sans-serif';
ctx.fillStyle = '#888';
ctx.fillText('q' + i, p.x, p.y + R + 12);
}
updateEnergy();
}
function updateEnergy() {
const E = totalEnergy(state);
const minE = totalEnergy([0,1,1,0]); // we'll compute properly
document.getElementById('energy-val').textContent = E.toFixed(2);
// Compute true minimum for comparison
let best = Infinity;
for (let mask = 0; mask < 16; mask++) {
const st = [mask >> 3 & 1, mask >> 2 & 1, mask >> 1 & 1, mask & 1];
const e = totalEnergy(st); if (e < best) best = e;
}
const lbl = document.getElementById('energy-label');
const status = document.getElementById('status');
if (Math.abs(E - best) < 0.001) {
lbl.textContent = 'Ground state!'; lbl.className = 'label ground';
status.textContent = fmt('✓ You found the ground state energy (%best%)!', { best: best.toFixed(2) });
} else if (E > best + 4) {
lbl.textContent = 'High energy'; lbl.className = 'label high';
status.textContent = fmt('Energy %E%. Ground state is %best%. Keep exploring.', { E: E.toFixed(2), best: best.toFixed(2) });
} else {
lbl.textContent = 'Not minimal'; lbl.className = 'label mid';
status.textContent = fmt('Energy %E%. Ground state is %best%. Can you do better?', { E: E.toFixed(2), best: best.toFixed(2) });
}
}
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (W / rect.width);
const my = (e.clientY - rect.top) * (H / rect.height);
for (let i = 0; i < 4; i++) {
const p = qpos[i];
if ((mx - p.x) ** 2 + (my - p.y) ** 2 < R * R) {
state[i] ^= 1;
draw();
return;
}
}
});
document.getElementById('btn-find').addEventListener('click', function() {
const mask = groundState();
for (let i = 0; i < 4; i++) state[i] = (mask >> (3 - i)) & 1;
draw();
const E = totalEnergy(state);
document.getElementById('status').textContent =
fmt('✓ Ground state found by exhaustive search over all 16 configurations. Energy = %E%.', { E: E.toFixed(2) });
console.log('Ground state:', state.map((s, i) => 'q' + i + '=' + s).join(', '), '| Energy:', E);
});
document.getElementById('btn-reset').addEventListener('click', function() {
state.fill(0); draw();
document.getElementById('status').textContent = '';
});
draw();
<p class="hint">A <b>2-local Hamiltonian</b> on 4 qubits: each edge has an interaction
energy between two qubits. Click qubits to toggle |0⟩ / |1⟩ and watch the energy
change. Can you reach the <b>minimum energy</b> (ground state)?</p>
<div id="canvas-wrap"><canvas id="canvas" width="340" height="220"></canvas></div>
<div class="energy-row">
<span>Total energy: <b id="energy-val">—</b></span>
<span id="energy-label" class="label"></span>
</div>
<div class="btns">
<button id="btn-find" type="button">Find ground state</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
#canvas-wrap { display: flex; justify-content: center; margin: .4rem 0 .2rem; }
canvas { border-radius: 10px; background: #f4f7fa; border: 1px solid #dde3ea; cursor: pointer; }
.energy-row { display: flex; align-items: center; gap: .8rem; font-size: 1rem; margin: .4rem 0; }
.energy-row b { font-size: 1.15rem; }
.label { font-size: .82rem; font-weight: 600; padding: .15rem .55rem; border-radius: 12px; }
.label.ground { background: #d1fae5; color: #065f46; }
.label.high { background: #fee2e2; color: #991b1b; }
.label.mid { background: #fef3c7; color: #92400e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .2rem; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2-Local Hamiltonian demo on 4 qubits.
// Each term H_{ij}(s_i, s_j) penalizes certain qubit configurations.
// We use an Ising-like model: H = sum_{(i,j)} J_{ij} * (1 - 2*s_i) * (1 - 2*s_j)
// where s_i in {0,1} and J_{ij} is the coupling strength.
// Negative J -> ferromagnetic (same spin preferred), Positive J -> antiferromagnetic.
// Total energy = sum of all edge terms.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// 4 qubits positioned at corners of a square, centered
const qpos = [
{ x: 90, y: 70 }, // qubit 0
{ x: 250, y: 70 }, // qubit 1
{ x: 250, y: 160 }, // qubit 2
{ x: 90, y: 160 }, // qubit 3
];
// Edges: [i, j, J_ij, label]
// Negative = ferromagnetic (same-spin = low energy), Positive = antiferromagnetic
const edges = [
[0, 1, -1.0, '-1'], // top
[1, 2, 2.0, '+2'], // right
[2, 3, -1.5, '-1.5'], // bottom
[0, 3, 1.5, '+1.5'], // left
[0, 2, 1.0, '+1'], // diagonal
];
// Qubit states: 0 = |0⟩, 1 = |1⟩
const state = [0, 0, 0, 0];
const R = 24; // qubit radius
function spin(s) { return 1 - 2 * s; } // 0->+1, 1->-1
function totalEnergy(st) {
let E = 0;
for (const [i, j, J] of edges) E += J * spin(st[i]) * spin(st[j]);
return E;
}
function groundState() {
let best = Infinity, bestMask = 0;
for (let mask = 0; mask < 16; mask++) {
const st = [mask >> 3 & 1, mask >> 2 & 1, mask >> 1 & 1, mask & 1];
const E = totalEnergy(st);
if (E < best) { best = E; bestMask = mask; }
}
return bestMask;
}
function edgeEnergy(i, j, J) {
return J * spin(state[i]) * spin(state[j]);
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw edges
for (const [i, j, J, lbl] of edges) {
const pi = qpos[i], pj = qpos[j];
const ee = edgeEnergy(i, j, J);
// Color edge: negative contribution = green tint, positive = red tint
const t = Math.min(1, Math.abs(ee) / 3);
if (ee < 0) {
ctx.strokeStyle = `rgba(16,185,129,${0.3 + 0.5 * t})`;
} else if (ee > 0) {
ctx.strokeStyle = `rgba(239,68,68,${0.3 + 0.5 * t})`;
} else {
ctx.strokeStyle = '#c0cdd8';
}
ctx.lineWidth = 2.5 + 2 * t;
ctx.beginPath(); ctx.moveTo(pi.x, pi.y); ctx.lineTo(pj.x, pj.y); ctx.stroke();
// Edge label: coupling constant and contribution
const mx = (pi.x + pj.x) / 2, my = (pi.y + pj.y) / 2;
const dx = pj.x - pi.x, dy = pj.y - pi.y;
const len = Math.sqrt(dx * dx + dy * dy);
// Offset label perpendicular to edge
const ox = -dy / len * 14, oy = dx / len * 14;
ctx.font = '600 11px system-ui, sans-serif';
ctx.fillStyle = '#555';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('J=' + lbl, mx + ox, my + oy);
}
// Draw qubits
for (let i = 0; i < 4; i++) {
const p = qpos[i];
const s = state[i];
ctx.beginPath();
ctx.arc(p.x, p.y, R, 0, 2 * Math.PI);
ctx.fillStyle = s === 0 ? '#dbeafe' : '#fde68a';
ctx.fill();
ctx.strokeStyle = s === 0 ? '#2563eb' : '#d97706';
ctx.lineWidth = 2.2;
ctx.stroke();
ctx.font = '700 15px system-ui, monospace';
ctx.fillStyle = s === 0 ? '#1e40af' : '#92400e';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(s === 0 ? '|0⟩' : '|1⟩', p.x, p.y);
// Qubit index
ctx.font = '500 11px system-ui, sans-serif';
ctx.fillStyle = '#888';
ctx.fillText('q' + i, p.x, p.y + R + 12);
}
updateEnergy();
}
function updateEnergy() {
const E = totalEnergy(state);
const minE = totalEnergy([0,1,1,0]); // we'll compute properly
document.getElementById('energy-val').textContent = E.toFixed(2);
// Compute true minimum for comparison
let best = Infinity;
for (let mask = 0; mask < 16; mask++) {
const st = [mask >> 3 & 1, mask >> 2 & 1, mask >> 1 & 1, mask & 1];
const e = totalEnergy(st); if (e < best) best = e;
}
const lbl = document.getElementById('energy-label');
const status = document.getElementById('status');
if (Math.abs(E - best) < 0.001) {
lbl.textContent = 'Ground state!'; lbl.className = 'label ground';
status.textContent = fmt('✓ You found the ground state energy (%best%)!', { best: best.toFixed(2) });
} else if (E > best + 4) {
lbl.textContent = 'High energy'; lbl.className = 'label high';
status.textContent = fmt('Energy %E%. Ground state is %best%. Keep exploring.', { E: E.toFixed(2), best: best.toFixed(2) });
} else {
lbl.textContent = 'Not minimal'; lbl.className = 'label mid';
status.textContent = fmt('Energy %E%. Ground state is %best%. Can you do better?', { E: E.toFixed(2), best: best.toFixed(2) });
}
}
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (W / rect.width);
const my = (e.clientY - rect.top) * (H / rect.height);
for (let i = 0; i < 4; i++) {
const p = qpos[i];
if ((mx - p.x) ** 2 + (my - p.y) ** 2 < R * R) {
state[i] ^= 1;
draw();
return;
}
}
});
document.getElementById('btn-find').addEventListener('click', function() {
const mask = groundState();
for (let i = 0; i < 4; i++) state[i] = (mask >> (3 - i)) & 1;
draw();
const E = totalEnergy(state);
document.getElementById('status').textContent =
fmt('✓ Ground state found by exhaustive search over all 16 configurations. Energy = %E%.', { E: E.toFixed(2) });
console.log('Ground state:', state.map((s, i) => 'q' + i + '=' + s).join(', '), '| Energy:', E);
});
document.getElementById('btn-reset').addEventListener('click', function() {
state.fill(0); draw();
document.getElementById('status').textContent = '';
});
draw();
Comments
Loading comments...