Below is a stream of 12 queries, each with a true value between 0 and 100. Set the threshold and the privacy budget ε, then run SVT. The algorithm adds Laplace noise to the threshold and to each query value, reports Above for queries that cross the noisy threshold, and stops once a configurable number of above-threshold answers have been reported.
<!-- SVT demo — stream of queries, report only above-threshold ones -->
<div class="controls">
<label>Threshold T: <input type="range" id="threshold" min="10" max="90" value="50" step="1"> <span id="threshold-val">50</span></label>
<label>Privacy budget ε (×0.2): <input type="range" id="epsilon" min="1" max="10" value="5" step="1"> <span id="epsilon-val">1.0</span></label>
<label>Max above answers k: <input type="range" id="kmax" min="1" max="6" value="3" step="1"> <span id="kmax-val">3</span></label>
</div>
<button id="run" type="button">Run SVT</button>
<div id="results"></div>
<div id="status-bar"></div>
/* Sparse Vector Technique demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .7rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .5rem; }
input[type=range] { flex: 1; max-width: 180px; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-bottom: .7rem; }
#results { display: flex; flex-direction: column; gap: 4px; }
.query-row { display: flex; align-items: center; gap: 6px; font-size: .85rem; }
.q-label { width: 80px; color: #555; }
.bar-wrap { flex: 1; background: #e8eef3; border-radius: 4px; height: 18px; position: relative; }
.bar-fill { height: 100%; border-radius: 4px; transition: width .3s; }
.bar-fill.above { background: #0a7d33; }
.bar-fill.below { background: #c9ccd1; }
.bar-fill.stopped { background: #aaa; }
.q-badge { font: 600 12px system-ui; padding: 1px 7px; border-radius: 10px; white-space: nowrap; }
.badge-above { background: #d4edda; color: #0a7d33; }
.badge-below { background: #e8eef3; color: #555; }
.badge-stopped { background: #eee; color: #888; }
.threshold-line { position: absolute; top: 0; bottom: 0; width: 2px; background: #e63946; }
#status-bar { margin-top: .6rem; font-size: .9rem; font-weight: 600; min-height: 1.4em; }
#status-bar.ok { color: #0a7d33; }
#status-bar.warn { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Draw a sample from Laplace(b) using the inverse CDF method.
function laplaceNoise(b) {
const u = Math.random() - 0.5;
return -b * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
}
// Fixed true query values (0-100) and their display labels.
const TRUE_VALUES = [22, 38, 55, 61, 47, 70, 33, 82, 41, 66, 29, 74];
const QUERY_LABELS = ['Q1','Q2','Q3','Q4','Q5','Q6','Q7','Q8','Q9','Q10','Q11','Q12'];
const threshInput = document.getElementById('threshold');
const epsilonInput = document.getElementById('epsilon');
const kmaxInput = document.getElementById('kmax');
const threshVal = document.getElementById('threshold-val');
const epsilonValEl = document.getElementById('epsilon-val');
const kmaxValEl = document.getElementById('kmax-val');
const resultsEl = document.getElementById('results');
const statusEl = document.getElementById('status-bar');
threshInput.addEventListener('input', () => { threshVal.textContent = threshInput.value; });
epsilonInput.addEventListener('input', () => {
epsilonValEl.textContent = (epsilonInput.value / 5).toFixed(1);
});
kmaxInput.addEventListener('input', () => { kmaxValEl.textContent = kmaxInput.value; });
// Run SVT — add Laplace noise to threshold and each query, report outcomes.
function runSVT() {
const T = Number(threshInput.value);
const eps = Number(epsilonInput.value) / 5;
const k = Number(kmaxInput.value);
// Noise scales follow the SVT proof — threshold noise Lap(2/eps), query noise Lap(4k/eps).
const threshNoise = laplaceNoise(2 / eps);
const noisyThresh = T + threshNoise;
const queryNoiseScale = (4 * k) / eps;
let aboveCount = 0;
const rows = [];
for (let i = 0; i < TRUE_VALUES.length; i++) {
const trueVal = TRUE_VALUES[i];
const label = QUERY_LABELS[i];
if (aboveCount >= k) {
// Budget exhausted — remaining queries are not answered.
rows.push({ label, trueVal, outcome: 'stopped' });
continue;
}
const noisyVal = trueVal + laplaceNoise(queryNoiseScale);
const isAbove = noisyVal >= noisyThresh;
if (isAbove) {
aboveCount++;
rows.push({ label, trueVal, outcome: 'above' });
} else {
rows.push({ label, trueVal, outcome: 'below' });
}
}
// Render results as labeled bars with threshold line and outcome badge.
resultsEl.innerHTML = '';
for (const row of rows) {
const div = document.createElement('div');
div.className = 'query-row';
const qlabel = document.createElement('span');
qlabel.className = 'q-label';
qlabel.textContent = row.label;
const wrap = document.createElement('div');
wrap.className = 'bar-wrap';
const fill = document.createElement('div');
fill.className = 'bar-fill ' + row.outcome;
fill.style.width = row.trueVal + '%';
const tline = document.createElement('div');
tline.className = 'threshold-line';
tline.style.left = T + '%';
tline.title = 'Threshold T';
wrap.appendChild(fill);
wrap.appendChild(tline);
const badge = document.createElement('span');
badge.className = 'q-badge';
if (row.outcome === 'above') {
badge.className += ' badge-above';
badge.textContent = 'Above ↑';
} else if (row.outcome === 'below') {
badge.className += ' badge-below';
badge.textContent = 'Below ↓';
} else {
badge.className += ' badge-stopped';
badge.textContent = 'Stopped';
}
div.appendChild(qlabel);
div.appendChild(wrap);
div.appendChild(badge);
resultsEl.appendChild(div);
}
// Show summary: how many above-threshold answers were issued.
const stopped = rows.filter(r => r.outcome === 'stopped').length;
if (aboveCount >= k) {
statusEl.textContent = i18nFmt('Budget exhausted after %k% above-threshold answer(s). %stopped% query/queries not answered.', { k: k, stopped: stopped });
statusEl.className = 'warn';
} else {
statusEl.textContent = i18nFmt('Stream complete. %above% of %k% above-threshold answer(s) used.', { above: aboveCount, k: k });
statusEl.className = 'ok';
}
}
document.getElementById('run').addEventListener('click', runSVT);
// Run on load so the demo is ready immediately.
runSVT();
<!-- SVT demo — stream of queries, report only above-threshold ones -->
<div class="controls">
<label>Threshold T: <input type="range" id="threshold" min="10" max="90" value="50" step="1"> <span id="threshold-val">50</span></label>
<label>Privacy budget ε (×0.2): <input type="range" id="epsilon" min="1" max="10" value="5" step="1"> <span id="epsilon-val">1.0</span></label>
<label>Max above answers k: <input type="range" id="kmax" min="1" max="6" value="3" step="1"> <span id="kmax-val">3</span></label>
</div>
<button id="run" type="button">Run SVT</button>
<div id="results"></div>
<div id="status-bar"></div>
/* Sparse Vector Technique demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .7rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .5rem; }
input[type=range] { flex: 1; max-width: 180px; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-bottom: .7rem; }
#results { display: flex; flex-direction: column; gap: 4px; }
.query-row { display: flex; align-items: center; gap: 6px; font-size: .85rem; }
.q-label { width: 80px; color: #555; }
.bar-wrap { flex: 1; background: #e8eef3; border-radius: 4px; height: 18px; position: relative; }
.bar-fill { height: 100%; border-radius: 4px; transition: width .3s; }
.bar-fill.above { background: #0a7d33; }
.bar-fill.below { background: #c9ccd1; }
.bar-fill.stopped { background: #aaa; }
.q-badge { font: 600 12px system-ui; padding: 1px 7px; border-radius: 10px; white-space: nowrap; }
.badge-above { background: #d4edda; color: #0a7d33; }
.badge-below { background: #e8eef3; color: #555; }
.badge-stopped { background: #eee; color: #888; }
.threshold-line { position: absolute; top: 0; bottom: 0; width: 2px; background: #e63946; }
#status-bar { margin-top: .6rem; font-size: .9rem; font-weight: 600; min-height: 1.4em; }
#status-bar.ok { color: #0a7d33; }
#status-bar.warn { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Draw a sample from Laplace(b) using the inverse CDF method.
function laplaceNoise(b) {
const u = Math.random() - 0.5;
return -b * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
}
// Fixed true query values (0-100) and their display labels.
const TRUE_VALUES = [22, 38, 55, 61, 47, 70, 33, 82, 41, 66, 29, 74];
const QUERY_LABELS = ['Q1','Q2','Q3','Q4','Q5','Q6','Q7','Q8','Q9','Q10','Q11','Q12'];
const threshInput = document.getElementById('threshold');
const epsilonInput = document.getElementById('epsilon');
const kmaxInput = document.getElementById('kmax');
const threshVal = document.getElementById('threshold-val');
const epsilonValEl = document.getElementById('epsilon-val');
const kmaxValEl = document.getElementById('kmax-val');
const resultsEl = document.getElementById('results');
const statusEl = document.getElementById('status-bar');
threshInput.addEventListener('input', () => { threshVal.textContent = threshInput.value; });
epsilonInput.addEventListener('input', () => {
epsilonValEl.textContent = (epsilonInput.value / 5).toFixed(1);
});
kmaxInput.addEventListener('input', () => { kmaxValEl.textContent = kmaxInput.value; });
// Run SVT — add Laplace noise to threshold and each query, report outcomes.
function runSVT() {
const T = Number(threshInput.value);
const eps = Number(epsilonInput.value) / 5;
const k = Number(kmaxInput.value);
// Noise scales follow the SVT proof — threshold noise Lap(2/eps), query noise Lap(4k/eps).
const threshNoise = laplaceNoise(2 / eps);
const noisyThresh = T + threshNoise;
const queryNoiseScale = (4 * k) / eps;
let aboveCount = 0;
const rows = [];
for (let i = 0; i < TRUE_VALUES.length; i++) {
const trueVal = TRUE_VALUES[i];
const label = QUERY_LABELS[i];
if (aboveCount >= k) {
// Budget exhausted — remaining queries are not answered.
rows.push({ label, trueVal, outcome: 'stopped' });
continue;
}
const noisyVal = trueVal + laplaceNoise(queryNoiseScale);
const isAbove = noisyVal >= noisyThresh;
if (isAbove) {
aboveCount++;
rows.push({ label, trueVal, outcome: 'above' });
} else {
rows.push({ label, trueVal, outcome: 'below' });
}
}
// Render results as labeled bars with threshold line and outcome badge.
resultsEl.innerHTML = '';
for (const row of rows) {
const div = document.createElement('div');
div.className = 'query-row';
const qlabel = document.createElement('span');
qlabel.className = 'q-label';
qlabel.textContent = row.label;
const wrap = document.createElement('div');
wrap.className = 'bar-wrap';
const fill = document.createElement('div');
fill.className = 'bar-fill ' + row.outcome;
fill.style.width = row.trueVal + '%';
const tline = document.createElement('div');
tline.className = 'threshold-line';
tline.style.left = T + '%';
tline.title = 'Threshold T';
wrap.appendChild(fill);
wrap.appendChild(tline);
const badge = document.createElement('span');
badge.className = 'q-badge';
if (row.outcome === 'above') {
badge.className += ' badge-above';
badge.textContent = 'Above ↑';
} else if (row.outcome === 'below') {
badge.className += ' badge-below';
badge.textContent = 'Below ↓';
} else {
badge.className += ' badge-stopped';
badge.textContent = 'Stopped';
}
div.appendChild(qlabel);
div.appendChild(wrap);
div.appendChild(badge);
resultsEl.appendChild(div);
}
// Show summary: how many above-threshold answers were issued.
const stopped = rows.filter(r => r.outcome === 'stopped').length;
if (aboveCount >= k) {
statusEl.textContent = i18nFmt('Budget exhausted after %k% above-threshold answer(s). %stopped% query/queries not answered.', { k: k, stopped: stopped });
statusEl.className = 'warn';
} else {
statusEl.textContent = i18nFmt('Stream complete. %above% of %k% above-threshold answer(s) used.', { above: aboveCount, k: k });
statusEl.className = 'ok';
}
}
document.getElementById('run').addEventListener('click', runSVT);
// Run on load so the demo is ready immediately.
runSVT();
Notice the key asymmetry: below-threshold queries consume almost no privacy budget — only the noisy threshold comparison matters. Above-threshold queries each cost a share of ε. Run the demo several times: the random noise means different queries cross the threshold each time, and the budget runs out at different points. Raise ε for more accurate answers (less noise) at higher privacy cost; lower it for stronger privacy at the price of more randomness.
Comments
Loading comments...