The energy of a two-parameter ansatz depends on two angles, θ₁ and θ₂. Drag the sliders to explore the energy landscape. The blue dot shows where you are; the dashed line marks the true ground-state energy. Press Run optimizer to watch gradient descent find the minimum automatically.
<p class="hint">Drag the sliders to set the two variational angles θ₁ and θ₂. The energy surface shows how the trial state's energy changes across parameter space. Press <b>Run optimizer</b> to watch gradient descent find the minimum automatically.</p>
<div class="controls">
<label>θ₁ = <span id="v1">0.00</span> rad
<input type="range" id="s1" min="0" max="6.283" step="0.02" value="0">
</label>
<label>θ₂ = <span id="v2">0.00</span> rad
<input type="range" id="s2" min="0" max="6.283" step="0.02" value="0">
</label>
</div>
<canvas id="canvas" width="340" height="220"></canvas>
<div class="info">
<span>Current energy: <b id="eNow">—</b></span>
<span>Ground state: <b id="eMin">—</b></span>
</div>
<div class="btns">
<button id="opt" type="button">Run optimizer</button>
<button id="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: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .6rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .5rem; }
input[type=range] { flex: 1; accent-color: #1d3557; }
canvas { display: block; border-radius: 8px; border: 1px solid #cdd9e3; background: #f4f7fa; width: 100%; max-width: 340px; }
.info { display: flex; gap: 1.5rem; font-size: .88rem; margin: .45rem 0; }
.info b { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
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: .9rem; font-weight: 600; min-height: 1.4em; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VQE demo: two-parameter ansatz for a simplified 2-qubit Hamiltonian.
// H = -cos(t1) - 0.5*cos(t2) - 0.3*cos(t1+t2)
// Exact ground-state energy = minimum of E(t1,t2) over [0,2pi)^2.
// The variational principle guarantees E(t1,t2) >= E_ground for all t1,t2.
const TWO_PI = 2 * Math.PI;
function energy(t1, t2) {
return -Math.cos(t1) - 0.5 * Math.cos(t2) - 0.3 * Math.cos(t1 + t2);
}
// Find true ground-state energy by grid search (for display reference only)
let eMin = Infinity;
let eMinT1 = 0, eMinT2 = 0;
const GRID = 200;
for (let i = 0; i <= GRID; i++) {
for (let j = 0; j <= GRID; j++) {
const t1 = (i / GRID) * TWO_PI;
const t2 = (j / GRID) * TWO_PI;
const e = energy(t1, t2);
if (e < eMin) { eMin = e; eMinT1 = t1; eMinT2 = t2; }
}
}
// eMin ≈ -1.8 (true minimum at t1≈0, t2≈0)
const eMax = 1.8; // for color scale
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Draw energy heatmap
function drawMap() {
const img = ctx.createImageData(W, H);
for (let px = 0; px < W; px++) {
for (let py = 0; py < H; py++) {
const t1 = (px / W) * TWO_PI;
const t2 = (py / H) * TWO_PI;
const e = energy(t1, t2);
// Map energy from [eMin,eMax] to [0,1]
const norm = Math.max(0, Math.min(1, (e - eMin) / (eMax - eMin)));
// Color: blue (low energy) to red (high energy)
const r = Math.round(norm * 210 + (1 - norm) * 29);
const g = Math.round(norm * 50 + (1 - norm) * 53);
const b = Math.round(norm * 50 + (1 - norm) * 87);
const idx = (py * W + px) * 4;
img.data[idx] = r; img.data[idx+1] = g; img.data[idx+2] = b; img.data[idx+3] = 255;
}
}
ctx.putImageData(img, 0, 0);
// Draw ground-state marker (star)
const gx = Math.round((eMinT1 / TWO_PI) * W);
const gy = Math.round((eMinT2 / TWO_PI) * H);
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(gx, gy, 6, 0, TWO_PI); ctx.stroke();
}
function drawDot(t1, t2) {
const px = Math.round((t1 / TWO_PI) * W);
const py = Math.round((t2 / TWO_PI) * H);
ctx.beginPath(); ctx.arc(px, py, 6, 0, TWO_PI);
ctx.fillStyle = '#60c2ff'; ctx.fill();
ctx.strokeStyle = '#0a2540'; ctx.lineWidth = 1.8; ctx.stroke();
}
function drawPath(path) {
if (path.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = 'rgba(255,255,100,0.7)'; ctx.lineWidth = 1.5;
ctx.moveTo(Math.round((path[0][0]/TWO_PI)*W), Math.round((path[0][1]/TWO_PI)*H));
for (let k = 1; k < path.length; k++) {
ctx.lineTo(Math.round((path[k][0]/TWO_PI)*W), Math.round((path[k][1]/TWO_PI)*H));
}
ctx.stroke();
}
let t1 = 0, t2 = 0;
const s1 = document.getElementById('s1');
const s2 = document.getElementById('s2');
const v1 = document.getElementById('v1');
const v2 = document.getElementById('v2');
const eNow = document.getElementById('eNow');
const eMinEl = document.getElementById('eMin');
const statusEl = document.getElementById('status');
eMinEl.textContent = eMin.toFixed(4);
function redraw(path) {
drawMap();
if (path) drawPath(path);
drawDot(t1, t2);
eNow.textContent = energy(t1, t2).toFixed(4);
}
s1.addEventListener('input', () => { t1 = parseFloat(s1.value); v1.textContent = t1.toFixed(2); statusEl.textContent = ''; redraw(null); });
s2.addEventListener('input', () => { t2 = parseFloat(s2.value); v2.textContent = t2.toFixed(2); statusEl.textContent = ''; redraw(null); });
// Gradient descent optimizer
document.getElementById('opt').addEventListener('click', () => {
statusEl.textContent = '';
const path = [[t1, t2]];
const LR = 0.12; const MAX_ITER = 300; const EPS = 1e-5; const DH = 0.001;
let steps = 0;
function step() {
const e0 = energy(t1, t2);
const g1 = (energy(t1 + DH, t2) - energy(t1 - DH, t2)) / (2 * DH);
const g2 = (energy(t1, t2 + DH) - energy(t1, t2 - DH)) / (2 * DH);
t1 = ((t1 - LR * g1) % TWO_PI + TWO_PI) % TWO_PI;
t2 = ((t2 - LR * g2) % TWO_PI + TWO_PI) % TWO_PI;
path.push([t1, t2]);
steps++;
s1.value = t1; v1.textContent = t1.toFixed(2);
s2.value = t2; v2.textContent = t2.toFixed(2);
const diff = Math.abs(energy(t1, t2) - e0);
if (diff < EPS || steps >= MAX_ITER) {
redraw(path);
const gap = Math.abs(energy(t1, t2) - eMin);
statusEl.textContent = fmt('Converged in %steps% steps. Gap to ground state: %gap%', { steps: steps, gap: gap.toFixed(4) });
console.assert(gap < 0.05, fmt('Optimizer failed to reach ground state: gap=%gap%', { gap: gap }));
} else {
redraw(path);
setTimeout(step, 20);
}
}
step();
});
document.getElementById('reset').addEventListener('click', () => {
t1 = 0; t2 = 0;
s1.value = 0; s2.value = 0;
v1.textContent = '0.00'; v2.textContent = '0.00';
statusEl.textContent = '';
redraw(null);
});
redraw(null);
<p class="hint">Drag the sliders to set the two variational angles θ₁ and θ₂. The energy surface shows how the trial state's energy changes across parameter space. Press <b>Run optimizer</b> to watch gradient descent find the minimum automatically.</p>
<div class="controls">
<label>θ₁ = <span id="v1">0.00</span> rad
<input type="range" id="s1" min="0" max="6.283" step="0.02" value="0">
</label>
<label>θ₂ = <span id="v2">0.00</span> rad
<input type="range" id="s2" min="0" max="6.283" step="0.02" value="0">
</label>
</div>
<canvas id="canvas" width="340" height="220"></canvas>
<div class="info">
<span>Current energy: <b id="eNow">—</b></span>
<span>Ground state: <b id="eMin">—</b></span>
</div>
<div class="btns">
<button id="opt" type="button">Run optimizer</button>
<button id="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: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .6rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .5rem; }
input[type=range] { flex: 1; accent-color: #1d3557; }
canvas { display: block; border-radius: 8px; border: 1px solid #cdd9e3; background: #f4f7fa; width: 100%; max-width: 340px; }
.info { display: flex; gap: 1.5rem; font-size: .88rem; margin: .45rem 0; }
.info b { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
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: .9rem; font-weight: 600; min-height: 1.4em; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VQE demo: two-parameter ansatz for a simplified 2-qubit Hamiltonian.
// H = -cos(t1) - 0.5*cos(t2) - 0.3*cos(t1+t2)
// Exact ground-state energy = minimum of E(t1,t2) over [0,2pi)^2.
// The variational principle guarantees E(t1,t2) >= E_ground for all t1,t2.
const TWO_PI = 2 * Math.PI;
function energy(t1, t2) {
return -Math.cos(t1) - 0.5 * Math.cos(t2) - 0.3 * Math.cos(t1 + t2);
}
// Find true ground-state energy by grid search (for display reference only)
let eMin = Infinity;
let eMinT1 = 0, eMinT2 = 0;
const GRID = 200;
for (let i = 0; i <= GRID; i++) {
for (let j = 0; j <= GRID; j++) {
const t1 = (i / GRID) * TWO_PI;
const t2 = (j / GRID) * TWO_PI;
const e = energy(t1, t2);
if (e < eMin) { eMin = e; eMinT1 = t1; eMinT2 = t2; }
}
}
// eMin ≈ -1.8 (true minimum at t1≈0, t2≈0)
const eMax = 1.8; // for color scale
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Draw energy heatmap
function drawMap() {
const img = ctx.createImageData(W, H);
for (let px = 0; px < W; px++) {
for (let py = 0; py < H; py++) {
const t1 = (px / W) * TWO_PI;
const t2 = (py / H) * TWO_PI;
const e = energy(t1, t2);
// Map energy from [eMin,eMax] to [0,1]
const norm = Math.max(0, Math.min(1, (e - eMin) / (eMax - eMin)));
// Color: blue (low energy) to red (high energy)
const r = Math.round(norm * 210 + (1 - norm) * 29);
const g = Math.round(norm * 50 + (1 - norm) * 53);
const b = Math.round(norm * 50 + (1 - norm) * 87);
const idx = (py * W + px) * 4;
img.data[idx] = r; img.data[idx+1] = g; img.data[idx+2] = b; img.data[idx+3] = 255;
}
}
ctx.putImageData(img, 0, 0);
// Draw ground-state marker (star)
const gx = Math.round((eMinT1 / TWO_PI) * W);
const gy = Math.round((eMinT2 / TWO_PI) * H);
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(gx, gy, 6, 0, TWO_PI); ctx.stroke();
}
function drawDot(t1, t2) {
const px = Math.round((t1 / TWO_PI) * W);
const py = Math.round((t2 / TWO_PI) * H);
ctx.beginPath(); ctx.arc(px, py, 6, 0, TWO_PI);
ctx.fillStyle = '#60c2ff'; ctx.fill();
ctx.strokeStyle = '#0a2540'; ctx.lineWidth = 1.8; ctx.stroke();
}
function drawPath(path) {
if (path.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = 'rgba(255,255,100,0.7)'; ctx.lineWidth = 1.5;
ctx.moveTo(Math.round((path[0][0]/TWO_PI)*W), Math.round((path[0][1]/TWO_PI)*H));
for (let k = 1; k < path.length; k++) {
ctx.lineTo(Math.round((path[k][0]/TWO_PI)*W), Math.round((path[k][1]/TWO_PI)*H));
}
ctx.stroke();
}
let t1 = 0, t2 = 0;
const s1 = document.getElementById('s1');
const s2 = document.getElementById('s2');
const v1 = document.getElementById('v1');
const v2 = document.getElementById('v2');
const eNow = document.getElementById('eNow');
const eMinEl = document.getElementById('eMin');
const statusEl = document.getElementById('status');
eMinEl.textContent = eMin.toFixed(4);
function redraw(path) {
drawMap();
if (path) drawPath(path);
drawDot(t1, t2);
eNow.textContent = energy(t1, t2).toFixed(4);
}
s1.addEventListener('input', () => { t1 = parseFloat(s1.value); v1.textContent = t1.toFixed(2); statusEl.textContent = ''; redraw(null); });
s2.addEventListener('input', () => { t2 = parseFloat(s2.value); v2.textContent = t2.toFixed(2); statusEl.textContent = ''; redraw(null); });
// Gradient descent optimizer
document.getElementById('opt').addEventListener('click', () => {
statusEl.textContent = '';
const path = [[t1, t2]];
const LR = 0.12; const MAX_ITER = 300; const EPS = 1e-5; const DH = 0.001;
let steps = 0;
function step() {
const e0 = energy(t1, t2);
const g1 = (energy(t1 + DH, t2) - energy(t1 - DH, t2)) / (2 * DH);
const g2 = (energy(t1, t2 + DH) - energy(t1, t2 - DH)) / (2 * DH);
t1 = ((t1 - LR * g1) % TWO_PI + TWO_PI) % TWO_PI;
t2 = ((t2 - LR * g2) % TWO_PI + TWO_PI) % TWO_PI;
path.push([t1, t2]);
steps++;
s1.value = t1; v1.textContent = t1.toFixed(2);
s2.value = t2; v2.textContent = t2.toFixed(2);
const diff = Math.abs(energy(t1, t2) - e0);
if (diff < EPS || steps >= MAX_ITER) {
redraw(path);
const gap = Math.abs(energy(t1, t2) - eMin);
statusEl.textContent = fmt('Converged in %steps% steps. Gap to ground state: %gap%', { steps: steps, gap: gap.toFixed(4) });
console.assert(gap < 0.05, fmt('Optimizer failed to reach ground state: gap=%gap%', { gap: gap }));
} else {
redraw(path);
setTimeout(step, 20);
}
}
step();
});
document.getElementById('reset').addEventListener('click', () => {
t1 = 0; t2 = 0;
s1.value = 0; s2.value = 0;
v1.textContent = '0.00'; v2.textContent = '0.00';
statusEl.textContent = '';
redraw(null);
});
redraw(null);
Notice how the energy surface has a clear global minimum — but also ridges and flat regions. The optimizer follows the gradient downhill; if it starts badly it can converge slowly. On real quantum hardware, noise makes the energy estimate noisy too, which is the central challenge of NISQ-era VQE.
Comments
Loading comments...