<!-- Interactive demo: compare basic composition vs RDP composition of the Gaussian mechanism -->
<div class="controls">
<label>
<span class="label-text">Noise multiplier σ</span>
<input type="range" id="sigma" min="0.5" max="4" step="0.1" value="1.2">
<span class="val" id="sigma-val">1.2</span>
</label>
<label>
<span class="label-text">Sampling fraction q</span>
<input type="range" id="q" min="0.001" max="0.1" step="0.001" value="0.01">
<span class="val" id="q-val">0.010</span>
</label>
<label>
<span class="label-text">Training steps T</span>
<input type="range" id="steps" min="100" max="10000" step="100" value="1000">
<span class="val" id="steps-val">1000</span>
</label>
<label>
<span class="label-text">Delta δ</span>
<input type="range" id="delta-exp" min="-8" max="-4" step="1" value="-5">
<span class="val" id="delta-val">10<sup>-5</sup></span>
</label>
</div>
<div class="chart-wrap">
<canvas id="chart" width="460" height="200"></canvas>
<div class="y-label">epsilon (privacy loss)</div>
</div>
<div class="results" id="results">
<div class="result-box basic">
<div class="result-label">Basic composition epsilon</div>
<div class="result-val" id="basic-val">—</div>
</div>
<div class="result-box rdp">
<div class="result-label">RDP composition epsilon</div>
<div class="result-val" id="rdp-val">—</div>
</div>
<div class="result-box ratio">
<div class="result-label">Ratio (basic / RDP)</div>
<div class="result-val" id="ratio-val">—</div>
</div>
</div>
<p class="note">Both lines describe identical training — only the accounting differs. RDP tracks the actual Rényi divergence at each order alpha and picks the tightest conversion to (epsilon, delta)-DP.</p>
/* Style reset and layout */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .5rem .6rem; font-size: .9rem; }
.controls { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem .8rem; margin-bottom: .7rem; }
label { display: flex; flex-direction: column; gap: .15rem; }
.label-text { font-size: .78rem; font-weight: 600; color: #555; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
.val { font-size: .8rem; font-weight: 700; color: #1d3557; }
.chart-wrap { position: relative; margin-bottom: .6rem; }
canvas { display: block; width: 100%; border: 1px solid #dde; border-radius: 6px; background: #fafbfc; }
.y-label { position: absolute; left: -1px; top: 50%; transform: rotate(-90deg) translateX(50%); font-size: .65rem; color: #888; white-space: nowrap; transform-origin: left center; }
.results { display: flex; gap: .5rem; margin-bottom: .5rem; }
.result-box { flex: 1; border-radius: 8px; padding: .4rem .5rem; text-align: center; }
.result-box.basic { background: #fde8e8; border: 1px solid #f5b8b8; }
.result-box.rdp { background: #e8f4e8; border: 1px solid #a8d5a8; }
.result-box.ratio { background: #e8eef6; border: 1px solid #b0c4de; }
.result-label { font-size: .7rem; font-weight: 600; color: #555; margin-bottom: .15rem; }
.result-val { font-size: 1.05rem; font-weight: 700; }
.result-box.basic .result-val { color: #c92f3c; }
.result-box.rdp .result-val { color: #0a7d33; }
.result-box.ratio .result-val { color: #1d3557; }
.note { font-size: .78rem; color: #666; line-height: 1.4; margin-top: .2rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Rényi divergence of the subsampled Gaussian mechanism at order alpha. */
/* For small q, D_alpha(M(D) || M(D')) ~ alpha*q^2 / (2*sigma^2). */
function rdpGaussSubsampled(alpha, sigma, q) {
/* Tight approximation valid for small q (subsampling amplification). */
if (alpha < 2) {
return (alpha * q * q) / (2 * sigma * sigma);
}
return (alpha * q * q) / (2 * sigma * sigma);
}
/* Convert an (alpha, rdpEps)-RDP guarantee to (epsilon, delta)-DP. */
function rdpToDp(rdpEps, alpha, delta) {
return rdpEps + Math.log(1 / delta) / (alpha - 1);
}
/* Basic (naive) composition: epsilon grows linearly with T steps. */
function basicEps(sigma, q, T) {
/* Per-step epsilon for the Gaussian mechanism (no subsampling benefit). */
const epsPerStep = q / (sigma * sigma);
return epsPerStep * T;
}
/* RDP composition: sum Renyi divergences, then convert once at the end. */
function rdpEps(sigma, q, T, delta) {
/* Scan integer orders alpha from 2 to 64; pick the tightest final epsilon. */
const alphas = [];
for (let a = 2; a <= 64; a++) alphas.push(a);
let best = Infinity;
for (const alpha of alphas) {
const totalRdp = rdpGaussSubsampled(alpha, sigma, q) * T;
const eps = rdpToDp(totalRdp, alpha, delta);
if (eps < best) best = eps;
}
return best;
}
/* Canvas chart setup. */
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = 460, H = 200;
function getParams() {
const sigma = parseFloat(document.getElementById('sigma').value);
const q = parseFloat(document.getElementById('q').value);
const T = parseInt(document.getElementById('steps').value, 10);
const dExp = parseInt(document.getElementById('delta-exp').value, 10);
return { sigma, q, T, delta: Math.pow(10, dExp) };
}
function drawChart(sigma, q, T, delta) {
ctx.clearRect(0, 0, W, H);
const PAD = { l: 40, r: 10, t: 12, b: 30 };
const cW = W - PAD.l - PAD.r;
const cH = H - PAD.t - PAD.b;
/* Compute epsilon at 40 evenly spaced checkpoints from 1 to T. */
const POINTS = 40;
const basicSeries = [], rdpSeries = [];
for (let i = 1; i <= POINTS; i++) {
const t = Math.round((i / POINTS) * T);
basicSeries.push({ t, eps: basicEps(sigma, q, t) });
rdpSeries.push({ t, eps: rdpEps(sigma, q, t, delta) });
}
const allEps = [...basicSeries.map(p => p.eps), ...rdpSeries.map(p => p.eps)];
const maxEps = Math.max(...allEps) * 1.05 || 1;
const xScale = (t) => PAD.l + (t / T) * cW;
const yScale = (e) => PAD.t + cH - (e / maxEps) * cH;
/* Draw axis lines. */
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t);
ctx.lineTo(PAD.l, PAD.t + cH);
ctx.lineTo(PAD.l + cW, PAD.t + cH);
ctx.stroke();
/* Draw axis tick labels. */
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
ctx.fillText('0', PAD.l, H - 4);
ctx.fillText(T.toString(), PAD.l + cW, H - 4);
ctx.fillText('T', PAD.l + cW / 2, H - 4);
ctx.textAlign = 'right';
ctx.fillText(maxEps.toFixed(1), PAD.l - 3, PAD.t + 8);
ctx.fillText('0', PAD.l - 3, PAD.t + cH);
/* Draw the basic composition curve in red. */
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2;
ctx.beginPath();
basicSeries.forEach((p, i) => {
const x = xScale(p.t), y = yScale(p.eps);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
/* Draw the RDP composition curve in green. */
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 2;
ctx.beginPath();
rdpSeries.forEach((p, i) => {
const x = xScale(p.t), y = yScale(p.eps);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
/* Draw the chart legend. */
ctx.font = '10px system-ui';
ctx.textAlign = 'left';
ctx.fillStyle = '#c92f3c';
ctx.fillRect(PAD.l + 6, PAD.t + 4, 18, 3);
ctx.fillText('Basic', PAD.l + 28, PAD.t + 8);
ctx.fillStyle = '#0a7d33';
ctx.fillRect(PAD.l + 6, PAD.t + 16, 18, 3);
ctx.fillText('RDP', PAD.l + 28, PAD.t + 20);
}
function update() {
const { sigma, q, T, delta } = getParams();
document.getElementById('sigma-val').textContent = sigma.toFixed(1);
document.getElementById('q-val').textContent = q.toFixed(3);
document.getElementById('steps-val').textContent = T;
const dExp = parseInt(document.getElementById('delta-exp').value, 10);
document.getElementById('delta-val').innerHTML = '10<sup>' + dExp + '</sup>';
const bEps = basicEps(sigma, q, T);
const rEps = rdpEps(sigma, q, T, delta);
const ratio = bEps / rEps;
document.getElementById('basic-val').textContent = bEps.toFixed(2);
document.getElementById('rdp-val').textContent = rEps.toFixed(2);
document.getElementById('ratio-val').textContent = i18nFmt('%r%x tighter', { r: ratio.toFixed(1) });
drawChart(sigma, q, T, delta);
}
['sigma', 'q', 'steps', 'delta-exp'].forEach(id =>
document.getElementById(id).addEventListener('input', update)
);
update();
<!-- Interactive demo: compare basic composition vs RDP composition of the Gaussian mechanism -->
<div class="controls">
<label>
<span class="label-text">Noise multiplier σ</span>
<input type="range" id="sigma" min="0.5" max="4" step="0.1" value="1.2">
<span class="val" id="sigma-val">1.2</span>
</label>
<label>
<span class="label-text">Sampling fraction q</span>
<input type="range" id="q" min="0.001" max="0.1" step="0.001" value="0.01">
<span class="val" id="q-val">0.010</span>
</label>
<label>
<span class="label-text">Training steps T</span>
<input type="range" id="steps" min="100" max="10000" step="100" value="1000">
<span class="val" id="steps-val">1000</span>
</label>
<label>
<span class="label-text">Delta δ</span>
<input type="range" id="delta-exp" min="-8" max="-4" step="1" value="-5">
<span class="val" id="delta-val">10<sup>-5</sup></span>
</label>
</div>
<div class="chart-wrap">
<canvas id="chart" width="460" height="200"></canvas>
<div class="y-label">epsilon (privacy loss)</div>
</div>
<div class="results" id="results">
<div class="result-box basic">
<div class="result-label">Basic composition epsilon</div>
<div class="result-val" id="basic-val">—</div>
</div>
<div class="result-box rdp">
<div class="result-label">RDP composition epsilon</div>
<div class="result-val" id="rdp-val">—</div>
</div>
<div class="result-box ratio">
<div class="result-label">Ratio (basic / RDP)</div>
<div class="result-val" id="ratio-val">—</div>
</div>
</div>
<p class="note">Both lines describe identical training — only the accounting differs. RDP tracks the actual Rényi divergence at each order alpha and picks the tightest conversion to (epsilon, delta)-DP.</p>
/* Style reset and layout */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .5rem .6rem; font-size: .9rem; }
.controls { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem .8rem; margin-bottom: .7rem; }
label { display: flex; flex-direction: column; gap: .15rem; }
.label-text { font-size: .78rem; font-weight: 600; color: #555; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
.val { font-size: .8rem; font-weight: 700; color: #1d3557; }
.chart-wrap { position: relative; margin-bottom: .6rem; }
canvas { display: block; width: 100%; border: 1px solid #dde; border-radius: 6px; background: #fafbfc; }
.y-label { position: absolute; left: -1px; top: 50%; transform: rotate(-90deg) translateX(50%); font-size: .65rem; color: #888; white-space: nowrap; transform-origin: left center; }
.results { display: flex; gap: .5rem; margin-bottom: .5rem; }
.result-box { flex: 1; border-radius: 8px; padding: .4rem .5rem; text-align: center; }
.result-box.basic { background: #fde8e8; border: 1px solid #f5b8b8; }
.result-box.rdp { background: #e8f4e8; border: 1px solid #a8d5a8; }
.result-box.ratio { background: #e8eef6; border: 1px solid #b0c4de; }
.result-label { font-size: .7rem; font-weight: 600; color: #555; margin-bottom: .15rem; }
.result-val { font-size: 1.05rem; font-weight: 700; }
.result-box.basic .result-val { color: #c92f3c; }
.result-box.rdp .result-val { color: #0a7d33; }
.result-box.ratio .result-val { color: #1d3557; }
.note { font-size: .78rem; color: #666; line-height: 1.4; margin-top: .2rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Rényi divergence of the subsampled Gaussian mechanism at order alpha. */
/* For small q, D_alpha(M(D) || M(D')) ~ alpha*q^2 / (2*sigma^2). */
function rdpGaussSubsampled(alpha, sigma, q) {
/* Tight approximation valid for small q (subsampling amplification). */
if (alpha < 2) {
return (alpha * q * q) / (2 * sigma * sigma);
}
return (alpha * q * q) / (2 * sigma * sigma);
}
/* Convert an (alpha, rdpEps)-RDP guarantee to (epsilon, delta)-DP. */
function rdpToDp(rdpEps, alpha, delta) {
return rdpEps + Math.log(1 / delta) / (alpha - 1);
}
/* Basic (naive) composition: epsilon grows linearly with T steps. */
function basicEps(sigma, q, T) {
/* Per-step epsilon for the Gaussian mechanism (no subsampling benefit). */
const epsPerStep = q / (sigma * sigma);
return epsPerStep * T;
}
/* RDP composition: sum Renyi divergences, then convert once at the end. */
function rdpEps(sigma, q, T, delta) {
/* Scan integer orders alpha from 2 to 64; pick the tightest final epsilon. */
const alphas = [];
for (let a = 2; a <= 64; a++) alphas.push(a);
let best = Infinity;
for (const alpha of alphas) {
const totalRdp = rdpGaussSubsampled(alpha, sigma, q) * T;
const eps = rdpToDp(totalRdp, alpha, delta);
if (eps < best) best = eps;
}
return best;
}
/* Canvas chart setup. */
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = 460, H = 200;
function getParams() {
const sigma = parseFloat(document.getElementById('sigma').value);
const q = parseFloat(document.getElementById('q').value);
const T = parseInt(document.getElementById('steps').value, 10);
const dExp = parseInt(document.getElementById('delta-exp').value, 10);
return { sigma, q, T, delta: Math.pow(10, dExp) };
}
function drawChart(sigma, q, T, delta) {
ctx.clearRect(0, 0, W, H);
const PAD = { l: 40, r: 10, t: 12, b: 30 };
const cW = W - PAD.l - PAD.r;
const cH = H - PAD.t - PAD.b;
/* Compute epsilon at 40 evenly spaced checkpoints from 1 to T. */
const POINTS = 40;
const basicSeries = [], rdpSeries = [];
for (let i = 1; i <= POINTS; i++) {
const t = Math.round((i / POINTS) * T);
basicSeries.push({ t, eps: basicEps(sigma, q, t) });
rdpSeries.push({ t, eps: rdpEps(sigma, q, t, delta) });
}
const allEps = [...basicSeries.map(p => p.eps), ...rdpSeries.map(p => p.eps)];
const maxEps = Math.max(...allEps) * 1.05 || 1;
const xScale = (t) => PAD.l + (t / T) * cW;
const yScale = (e) => PAD.t + cH - (e / maxEps) * cH;
/* Draw axis lines. */
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t);
ctx.lineTo(PAD.l, PAD.t + cH);
ctx.lineTo(PAD.l + cW, PAD.t + cH);
ctx.stroke();
/* Draw axis tick labels. */
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
ctx.fillText('0', PAD.l, H - 4);
ctx.fillText(T.toString(), PAD.l + cW, H - 4);
ctx.fillText('T', PAD.l + cW / 2, H - 4);
ctx.textAlign = 'right';
ctx.fillText(maxEps.toFixed(1), PAD.l - 3, PAD.t + 8);
ctx.fillText('0', PAD.l - 3, PAD.t + cH);
/* Draw the basic composition curve in red. */
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2;
ctx.beginPath();
basicSeries.forEach((p, i) => {
const x = xScale(p.t), y = yScale(p.eps);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
/* Draw the RDP composition curve in green. */
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 2;
ctx.beginPath();
rdpSeries.forEach((p, i) => {
const x = xScale(p.t), y = yScale(p.eps);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
/* Draw the chart legend. */
ctx.font = '10px system-ui';
ctx.textAlign = 'left';
ctx.fillStyle = '#c92f3c';
ctx.fillRect(PAD.l + 6, PAD.t + 4, 18, 3);
ctx.fillText('Basic', PAD.l + 28, PAD.t + 8);
ctx.fillStyle = '#0a7d33';
ctx.fillRect(PAD.l + 6, PAD.t + 16, 18, 3);
ctx.fillText('RDP', PAD.l + 28, PAD.t + 20);
}
function update() {
const { sigma, q, T, delta } = getParams();
document.getElementById('sigma-val').textContent = sigma.toFixed(1);
document.getElementById('q-val').textContent = q.toFixed(3);
document.getElementById('steps-val').textContent = T;
const dExp = parseInt(document.getElementById('delta-exp').value, 10);
document.getElementById('delta-val').innerHTML = '10<sup>' + dExp + '</sup>';
const bEps = basicEps(sigma, q, T);
const rEps = rdpEps(sigma, q, T, delta);
const ratio = bEps / rEps;
document.getElementById('basic-val').textContent = bEps.toFixed(2);
document.getElementById('rdp-val').textContent = rEps.toFixed(2);
document.getElementById('ratio-val').textContent = i18nFmt('%r%x tighter', { r: ratio.toFixed(1) });
drawChart(sigma, q, T, delta);
}
['sigma', 'q', 'steps', 'delta-exp'].forEach(id =>
document.getElementById(id).addEventListener('input', update)
);
update();
Comments
Loading comments...