<p class="hint">A <b>3-photon boson sampling</b> experiment: photons enter ports 0, 1, 2 and exit across 3 output ports. Probabilities are computed from the <b>permanent</b> of the relevant submatrix. Randomize the network to see how drastically the distribution changes.</p>
<div class="matrix-area">
<div class="mat-label">Network matrix U (real part shown):</div>
<div id="matrix-display" class="matrix-grid"></div>
</div>
<div class="btns">
<button id="randomize" type="button">Randomize Network</button>
<button id="hadamard" type="button" class="ghost">Reset to Balanced</button>
</div>
<div class="chart-label">Output distribution (3 photons in 3 modes):</div>
<div id="chart" class="chart"></div>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
.matrix-area { margin-bottom: .6rem; }
.mat-label { font-size: .8rem; color: #666; margin-bottom: .3rem; }
.matrix-grid { display: grid; grid-template-columns: repeat(3, 54px); gap: 3px; width: max-content; }
.mat-cell { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 5px;
text-align: center; padding: 4px 2px; font: 600 12px ui-monospace, monospace;
color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.chart-label { font-size: .8rem; color: #666; margin-bottom: .3rem; }
.chart { display: flex; flex-direction: column; gap: 4px; }
.bar-row { display: flex; align-items: center; gap: 6px; }
.bar-key { font: 600 11px ui-monospace, monospace; color: #1d3557; width: 80px; flex-shrink: 0; text-align: right; }
.bar-track { flex: 1; background: #e8eef3; border-radius: 4px; height: 20px; overflow: hidden; }
.bar-fill { background: #1d3557; height: 100%; border-radius: 4px; transition: width .3s; }
.bar-pct { font: 600 11px ui-monospace, monospace; color: #555; width: 44px; flex-shrink: 0; }
.status { font-size: .85rem; color: #444; margin-top: .5rem; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Boson sampling simulation: 3 photons in 3 modes.
// Input state: one photon in each of modes 0, 1, 2.
// Output basis: all ways to place 3 photons across 3 modes (multisets of size 3).
// For output pattern s = [s0, s1, s2] (sum = 3), the probability is
// P(s) = |Perm(U_S)|^2 / (s0! * s1! * s2!)
// where U_S is the 3x3 submatrix with row i repeated s_i times.
// For this demo all inputs have one photon each, so the input factor is 1.
// Compute permanent of a square matrix (Ryser's formula for small n).
// Perm(A) = (-1)^n * sum_{S subset [n], S nonempty} (-1)^|S| * prod_i (sum_{j in S} A[i][j])
function permanent(M) {
const n = M.length;
let total = 0;
// iterate over all non-empty subsets of columns
for (let mask = 1; mask < (1 << n); mask++) {
const bits = [];
for (let i = 0; i < n; i++) if (mask & (1 << i)) bits.push(i);
const k = bits.length;
// row products: for each row, sum of M[row][col] for col in bits
let prod = 1;
for (let r = 0; r < n; r++) {
let rowSum = 0;
for (const c of bits) rowSum += M[r][c];
prod *= rowSum;
}
// inclusion-exclusion sign: (-1)^|subset|
const sign = (k % 2 === 0) ? 1 : -1;
total += sign * prod;
}
// outer factor: (-1)^n
return ((n % 2 === 0) ? 1 : -1) * total;
}
function factorial(n) { let f = 1; for (let i = 2; i <= n; i++) f *= i; return f; }
// Generate all multisets of size n from {0..modes-1}
function outputPatterns(modes, n) {
const results = [];
function gen(start, remaining, current) {
if (remaining === 0) { results.push([...current]); return; }
for (let i = start; i < modes; i++) {
current.push(i); gen(i, remaining - 1, current); current.pop();
}
}
gen(0, n, []);
return results;
}
// Build output occupation vector from pattern list
function patternToOccupation(pattern, modes) {
const occ = new Array(modes).fill(0);
for (const p of pattern) occ[p]++;
return occ;
}
// Build the submatrix for input modes inputOcc and output occupation outputOcc
// For input all-single-photon: select row i repeated inputOcc[i]=1 times -> just U itself
// For output occupation outputOcc: repeat column j outputOcc[j] times
function buildSubmatrix(U, inputOcc, outputOcc) {
const rows = [];
for (let i = 0; i < U.length; i++) {
for (let k = 0; k < inputOcc[i]; k++) rows.push(i);
}
const cols = [];
for (let j = 0; j < U.length; j++) {
for (let k = 0; k < outputOcc[j]; k++) cols.push(j);
}
return rows.map(r => cols.map(c => U[r][c]));
}
// Current network matrix (3x3 real unitary from Hadamard-like construction)
let U = [];
function balancedMatrix() {
// 3x3 DFT-like real unitary (from Hadamard construction)
const s = 1 / Math.sqrt(3);
return [
[s, s, s],
[s, -s * Math.cos(2 * Math.PI / 3) - s * Math.sin(2 * Math.PI / 3),
s * Math.sin(2 * Math.PI / 3) - s * Math.cos(2 * Math.PI / 3)],
[s, s * Math.cos(4 * Math.PI / 3) - s * Math.sin(4 * Math.PI / 3),
-s * Math.sin(4 * Math.PI / 3) - s * Math.cos(4 * Math.PI / 3)]
];
}
function randomUnitary3() {
// Random 3x3 real orthogonal matrix via Gram-Schmidt
function randVec() { return [Math.random()-0.5, Math.random()-0.5, Math.random()-0.5]; }
function dot(a, b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
function sub(a, b, s) { return [a[0]-s*b[0], a[1]-s*b[1], a[2]-s*b[2]]; }
function norm(a) { return Math.sqrt(dot(a, a)); }
function normalize(a) { const n = norm(a); return [a[0]/n, a[1]/n, a[2]/n]; }
let v0, v1, v2;
// Generate with retry to avoid near-zero norms
do { v0 = normalize(randVec()); } while (norm(randVec()) < 0.01);
let w1 = randVec(); w1 = sub(w1, v0, dot(w1, v0)); v1 = normalize(w1);
let w2 = randVec(); w2 = sub(w2, v0, dot(w2, v0)); w2 = sub(w2, v1, dot(w2, v1)); v2 = normalize(w2);
return [v0, v1, v2]; // rows
}
function computeDistribution(matrix) {
const modes = 3;
const n = 3; // photons
const inputOcc = [1, 1, 1]; // one photon per input mode
const patterns = outputPatterns(modes, n);
const probs = [];
let total = 0;
for (const pattern of patterns) {
const occ = patternToOccupation(pattern, modes);
const sub = buildSubmatrix(matrix, inputOcc, occ);
const perm = permanent(sub);
const denom = occ.reduce((acc, k) => acc * factorial(k), 1);
const prob = (perm * perm) / denom;
probs.push({ occ, prob });
total += prob;
}
// Normalize (should already be ~1 for unitary, but floating point)
for (const p of probs) p.prob /= total;
return probs;
}
function renderMatrix(matrix) {
const grid = document.getElementById('matrix-display');
grid.innerHTML = '';
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
const cell = document.createElement('div');
cell.className = 'mat-cell';
cell.textContent = matrix[r][c].toFixed(3);
grid.appendChild(cell);
}
}
}
function renderChart(dist) {
const chart = document.getElementById('chart');
chart.innerHTML = '';
// Sort by probability descending
const sorted = [...dist].sort((a, b) => b.prob - a.prob);
const maxProb = sorted[0].prob;
for (const { occ, prob } of sorted) {
const label = '(' + occ.join(',') + ')';
const pct = (prob * 100).toFixed(1);
const barWidth = maxProb > 0 ? (prob / maxProb * 100) : 0;
const row = document.createElement('div');
row.className = 'bar-row';
row.innerHTML =
'<div class="bar-key">' + label + '</div>' +
'<div class="bar-track"><div class="bar-fill" style="width:' + barWidth + '%"></div></div>' +
'<div class="bar-pct">' + pct + '%</div>';
chart.appendChild(row);
}
}
function update() {
renderMatrix(U);
const dist = computeDistribution(U);
renderChart(dist);
const permSum = dist.reduce((s, d) => s + d.prob, 0);
document.getElementById('status').textContent =
fmt('Total probability: %v%% (10 output patterns, computed: %length%)', { v: (permSum * 100).toFixed(1), length: dist.length });
// Log top outputs for node verification
const top = [...dist].sort((a, b) => b.prob - a.prob).slice(0, 3);
console.log('Top outputs:', top.map(d => '(' + d.occ.join(',') + ')=' + (d.prob*100).toFixed(2) + '%').join(' '));
}
document.getElementById('randomize').addEventListener('click', () => {
U = randomUnitary3();
update();
});
document.getElementById('hadamard').addEventListener('click', () => {
U = balancedMatrix();
update();
});
U = balancedMatrix();
update();
<p class="hint">A <b>3-photon boson sampling</b> experiment: photons enter ports 0, 1, 2 and exit across 3 output ports. Probabilities are computed from the <b>permanent</b> of the relevant submatrix. Randomize the network to see how drastically the distribution changes.</p>
<div class="matrix-area">
<div class="mat-label">Network matrix U (real part shown):</div>
<div id="matrix-display" class="matrix-grid"></div>
</div>
<div class="btns">
<button id="randomize" type="button">Randomize Network</button>
<button id="hadamard" type="button" class="ghost">Reset to Balanced</button>
</div>
<div class="chart-label">Output distribution (3 photons in 3 modes):</div>
<div id="chart" class="chart"></div>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
.matrix-area { margin-bottom: .6rem; }
.mat-label { font-size: .8rem; color: #666; margin-bottom: .3rem; }
.matrix-grid { display: grid; grid-template-columns: repeat(3, 54px); gap: 3px; width: max-content; }
.mat-cell { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 5px;
text-align: center; padding: 4px 2px; font: 600 12px ui-monospace, monospace;
color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.chart-label { font-size: .8rem; color: #666; margin-bottom: .3rem; }
.chart { display: flex; flex-direction: column; gap: 4px; }
.bar-row { display: flex; align-items: center; gap: 6px; }
.bar-key { font: 600 11px ui-monospace, monospace; color: #1d3557; width: 80px; flex-shrink: 0; text-align: right; }
.bar-track { flex: 1; background: #e8eef3; border-radius: 4px; height: 20px; overflow: hidden; }
.bar-fill { background: #1d3557; height: 100%; border-radius: 4px; transition: width .3s; }
.bar-pct { font: 600 11px ui-monospace, monospace; color: #555; width: 44px; flex-shrink: 0; }
.status { font-size: .85rem; color: #444; margin-top: .5rem; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Boson sampling simulation: 3 photons in 3 modes.
// Input state: one photon in each of modes 0, 1, 2.
// Output basis: all ways to place 3 photons across 3 modes (multisets of size 3).
// For output pattern s = [s0, s1, s2] (sum = 3), the probability is
// P(s) = |Perm(U_S)|^2 / (s0! * s1! * s2!)
// where U_S is the 3x3 submatrix with row i repeated s_i times.
// For this demo all inputs have one photon each, so the input factor is 1.
// Compute permanent of a square matrix (Ryser's formula for small n).
// Perm(A) = (-1)^n * sum_{S subset [n], S nonempty} (-1)^|S| * prod_i (sum_{j in S} A[i][j])
function permanent(M) {
const n = M.length;
let total = 0;
// iterate over all non-empty subsets of columns
for (let mask = 1; mask < (1 << n); mask++) {
const bits = [];
for (let i = 0; i < n; i++) if (mask & (1 << i)) bits.push(i);
const k = bits.length;
// row products: for each row, sum of M[row][col] for col in bits
let prod = 1;
for (let r = 0; r < n; r++) {
let rowSum = 0;
for (const c of bits) rowSum += M[r][c];
prod *= rowSum;
}
// inclusion-exclusion sign: (-1)^|subset|
const sign = (k % 2 === 0) ? 1 : -1;
total += sign * prod;
}
// outer factor: (-1)^n
return ((n % 2 === 0) ? 1 : -1) * total;
}
function factorial(n) { let f = 1; for (let i = 2; i <= n; i++) f *= i; return f; }
// Generate all multisets of size n from {0..modes-1}
function outputPatterns(modes, n) {
const results = [];
function gen(start, remaining, current) {
if (remaining === 0) { results.push([...current]); return; }
for (let i = start; i < modes; i++) {
current.push(i); gen(i, remaining - 1, current); current.pop();
}
}
gen(0, n, []);
return results;
}
// Build output occupation vector from pattern list
function patternToOccupation(pattern, modes) {
const occ = new Array(modes).fill(0);
for (const p of pattern) occ[p]++;
return occ;
}
// Build the submatrix for input modes inputOcc and output occupation outputOcc
// For input all-single-photon: select row i repeated inputOcc[i]=1 times -> just U itself
// For output occupation outputOcc: repeat column j outputOcc[j] times
function buildSubmatrix(U, inputOcc, outputOcc) {
const rows = [];
for (let i = 0; i < U.length; i++) {
for (let k = 0; k < inputOcc[i]; k++) rows.push(i);
}
const cols = [];
for (let j = 0; j < U.length; j++) {
for (let k = 0; k < outputOcc[j]; k++) cols.push(j);
}
return rows.map(r => cols.map(c => U[r][c]));
}
// Current network matrix (3x3 real unitary from Hadamard-like construction)
let U = [];
function balancedMatrix() {
// 3x3 DFT-like real unitary (from Hadamard construction)
const s = 1 / Math.sqrt(3);
return [
[s, s, s],
[s, -s * Math.cos(2 * Math.PI / 3) - s * Math.sin(2 * Math.PI / 3),
s * Math.sin(2 * Math.PI / 3) - s * Math.cos(2 * Math.PI / 3)],
[s, s * Math.cos(4 * Math.PI / 3) - s * Math.sin(4 * Math.PI / 3),
-s * Math.sin(4 * Math.PI / 3) - s * Math.cos(4 * Math.PI / 3)]
];
}
function randomUnitary3() {
// Random 3x3 real orthogonal matrix via Gram-Schmidt
function randVec() { return [Math.random()-0.5, Math.random()-0.5, Math.random()-0.5]; }
function dot(a, b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
function sub(a, b, s) { return [a[0]-s*b[0], a[1]-s*b[1], a[2]-s*b[2]]; }
function norm(a) { return Math.sqrt(dot(a, a)); }
function normalize(a) { const n = norm(a); return [a[0]/n, a[1]/n, a[2]/n]; }
let v0, v1, v2;
// Generate with retry to avoid near-zero norms
do { v0 = normalize(randVec()); } while (norm(randVec()) < 0.01);
let w1 = randVec(); w1 = sub(w1, v0, dot(w1, v0)); v1 = normalize(w1);
let w2 = randVec(); w2 = sub(w2, v0, dot(w2, v0)); w2 = sub(w2, v1, dot(w2, v1)); v2 = normalize(w2);
return [v0, v1, v2]; // rows
}
function computeDistribution(matrix) {
const modes = 3;
const n = 3; // photons
const inputOcc = [1, 1, 1]; // one photon per input mode
const patterns = outputPatterns(modes, n);
const probs = [];
let total = 0;
for (const pattern of patterns) {
const occ = patternToOccupation(pattern, modes);
const sub = buildSubmatrix(matrix, inputOcc, occ);
const perm = permanent(sub);
const denom = occ.reduce((acc, k) => acc * factorial(k), 1);
const prob = (perm * perm) / denom;
probs.push({ occ, prob });
total += prob;
}
// Normalize (should already be ~1 for unitary, but floating point)
for (const p of probs) p.prob /= total;
return probs;
}
function renderMatrix(matrix) {
const grid = document.getElementById('matrix-display');
grid.innerHTML = '';
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
const cell = document.createElement('div');
cell.className = 'mat-cell';
cell.textContent = matrix[r][c].toFixed(3);
grid.appendChild(cell);
}
}
}
function renderChart(dist) {
const chart = document.getElementById('chart');
chart.innerHTML = '';
// Sort by probability descending
const sorted = [...dist].sort((a, b) => b.prob - a.prob);
const maxProb = sorted[0].prob;
for (const { occ, prob } of sorted) {
const label = '(' + occ.join(',') + ')';
const pct = (prob * 100).toFixed(1);
const barWidth = maxProb > 0 ? (prob / maxProb * 100) : 0;
const row = document.createElement('div');
row.className = 'bar-row';
row.innerHTML =
'<div class="bar-key">' + label + '</div>' +
'<div class="bar-track"><div class="bar-fill" style="width:' + barWidth + '%"></div></div>' +
'<div class="bar-pct">' + pct + '%</div>';
chart.appendChild(row);
}
}
function update() {
renderMatrix(U);
const dist = computeDistribution(U);
renderChart(dist);
const permSum = dist.reduce((s, d) => s + d.prob, 0);
document.getElementById('status').textContent =
fmt('Total probability: %v%% (10 output patterns, computed: %length%)', { v: (permSum * 100).toFixed(1), length: dist.length });
// Log top outputs for node verification
const top = [...dist].sort((a, b) => b.prob - a.prob).slice(0, 3);
console.log('Top outputs:', top.map(d => '(' + d.occ.join(',') + ')=' + (d.prob*100).toFixed(2) + '%').join(' '));
}
document.getElementById('randomize').addEventListener('click', () => {
U = randomUnitary3();
update();
});
document.getElementById('hadamard').addEventListener('click', () => {
U = balancedMatrix();
update();
});
U = balancedMatrix();
update();
Notice how changing the matrix changes the output distribution dramatically. The permanent calculation here is trivial for 3 photons — but for 50 photons it would require summing over 50! ≈ 3 × 1064 permutations. That exponential blowup is exactly why classical computers cannot efficiently simulate large boson sampling experiments.
Comments
Loading comments...