// ======= Analytics & Identity Bootstrap =======
const ANALYTICS_ENDPOINT = '/api/track';
const COOKIE_NAME = 'vid';
// Generate a stable visitor id (persists across sessions)
const visitorId = (() => {
const key = 'dp_sgd_visitor_id';
let id = localStorage.getItem(key);
if (!id) {
id = crypto.randomUUID?.() || (String(Date.now()) + Math.random().toString(16).slice(2));
localStorage.setItem(key, id);
}
return id;
})();
// Generate a stable session id (per browser tab/session)
const sessionId = (() => {
const key = 'dp_sgd_session_id';
let id = sessionStorage.getItem(key);
if (!id) {
id = crypto.randomUUID?.() || (String(Date.now()) + Math.random().toString(16).slice(2));
sessionStorage.setItem(key, id);
}
return id;
})();
// Expose globally for compatibility with other scripts
window.__visitor_id = visitorId;
window.__session_id = sessionId;
// Minimal user context (non-PII by default). Call identify({ id, role, org, plan }) if you have a login.
let userContext = { vid: visitorId, id: null, role: null, org: null, plan: null };
async function initIdentity() {
try {
const r = await fetch('/api/whoami', { credentials: 'same-origin' });
const info = await r.json();
if (info && info.vid) userContext.vid = info.vid;
} catch {}
}
initIdentity();
// Track page view after DOM is ready to ensure track() is defined
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
track('page_view', { path: location.pathname, title: document.title });
});
} else {
track('page_view', { path: location.pathname, title: document.title });
}
function identify(user) {
userContext = { ...userContext, ...{
id: user?.id ?? null,
role: user?.role ?? null,
org: user?.org ?? null,
plan: user?.plan ?? null,
}};
track('identify', { user: { id: userContext.id, role: userContext.role, org: userContext.org, plan: userContext.plan } });
}
// Fire-and-forget tracker
function track(eventType, payload = {}) {
const body = {
t: Date.now(),
session_id: sessionId, // Use snake_case to match API
sessionId: sessionId, // Keep camelCase for backward compatibility
event: eventType, // New field name
eventType: eventType, // Keep for backward compatibility
path: location.pathname,
payload,
user: { id: userContext.id, role: userContext.role, org: userContext.org, plan: userContext.plan },
visitor_id: userContext.vid, // Use snake_case to match API
vid: userContext.vid // Keep for backward compatibility
};
const data = new Blob([JSON.stringify(body)], { type: 'application/json' });
if (!(navigator.sendBeacon && navigator.sendBeacon(ANALYTICS_ENDPOINT, data))) {
fetch(ANALYTICS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), credentials: 'same-origin' }).catch(()=>{});
}
}
// Global click listener (optional; captures generic UI clicks)
document.addEventListener('click', (e) => {
const target = e.target?.closest?.('[data-track], button, a, .nav-link, .tab');
if (!target) return;
const name = target.getAttribute('data-track') || target.id || target.textContent?.trim()?.slice(0, 60);
if (!name) return;
track('ui_click', { name });
}, { capture: true });
// ======= End Analytics Bootstrap =======
class DPSGDExplorer {
constructor() {
this.trainingChart = null;
this.privacyChart = null;
this.gradientChart = null;
this.isTraining = false;
this.currentView = 'epochs'; // 'epochs' or 'iterations'
this.epochsData = [];
this.iterationsData = [];
this.abortController = null; // For canceling training requests
this.eventSource = null; // For SSE streaming
this.initializeUI();
}
initializeUI() {
// Initialize parameter controls
this.initializeSliders();
this.initializePresets();
this.initializeTabs();
this.initializeCharts();
// Add event listeners
document.getElementById('train-button')?.addEventListener('click', () => this.toggleTraining());
document.getElementById('train-button')?.addEventListener('click', () => { try { track('train_toggle', (this.getParameters?.()||{})); } catch (e) {} });
// Add view toggle listeners
document.getElementById('view-epochs')?.addEventListener('click', () => this.switchView('epochs'));
document.getElementById('view-iterations')?.addEventListener('click', () => this.switchView('iterations'));
document.getElementById('dataset-select')?.addEventListener('change', (e) => {
track('dataset_change', { dataset: e.target.value });
});
document.getElementById('model-select')?.addEventListener('change', (e) => {
track('model_change', { model_architecture: e.target.value });
});
document.getElementById('preset-high-privacy')?.addEventListener('click', () => {
track('preset_apply', { preset: 'high-privacy' });
});
document.getElementById('preset-balanced')?.addEventListener('click', () => {
track('preset_apply', { preset: 'balanced' });
});
document.getElementById('preset-high-utility')?.addEventListener('click', () => {
track('preset_apply', { preset: 'high-utility' });
});
}
initializeSliders() {
// Parameter sliders
const sliders = {
'clipping-norm': document.getElementById('clipping-norm'),
'noise-multiplier': document.getElementById('noise-multiplier'),
'batch-size': document.getElementById('batch-size'),
'learning-rate': document.getElementById('learning-rate'),
'epochs': document.getElementById('epochs')
};
// Add event listeners to sliders
for (const [id, slider] of Object.entries(sliders)) {
if (slider) {
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById(`${id}-value`).textContent = value.toFixed(1);
// Update privacy budget
this.updatePrivacyBudget();
try { track('param_change', { param: id, value }); } catch (e) {};
// Update gradient visualization when clipping norm changes
if (id === 'clipping-norm') {
this.updateGradientVisualization(value);
}
});
}
}
// Add event listener for the visual clipping norm slider
const visualSlider = document.getElementById('clipping-norm-visual');
if (visualSlider) {
visualSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('clipping-norm-visual-value').textContent = value.toFixed(1);
this.updateGradientVisualization(value);
});
}
}
initializePresets() {
// Presets based on research (Optax/TF Privacy benchmarks)
// With proper noise scaling: noise_stddev = C * σ / batch_size
const presets = {
'high-privacy': {
// Strong privacy (ε≈1-3), ~95% accuracy achievable
// Based on: noise=1.3, clip=1.5, LR=0.25, 15 epochs → ~95%
clippingNorm: 1.5,
noiseMultiplier: 1.3,
batchSize: 256,
learningRate: 0.25,
epochs: 30
},
'balanced': {
// Moderate privacy (ε≈3-5), ~96% accuracy
// Based on: noise=1.1, clip=1.0, LR=0.15, 60 epochs → ~96.6%
clippingNorm: 1.0,
noiseMultiplier: 1.1,
batchSize: 256,
learningRate: 0.15,
epochs: 30
},
'high-utility': {
// Lower privacy (ε≈8+), ~97% accuracy
// Based on: noise=0.7, clip=1.5, LR=0.25, 45 epochs → ~97%
clippingNorm: 1.5,
noiseMultiplier: 0.7,
batchSize: 256,
learningRate: 0.25,
epochs: 30
}
};
// Add event listeners to preset buttons
for (const [preset, values] of Object.entries(presets)) {
document.getElementById(`preset-${preset}`)?.addEventListener('click', () => {
this.applyPreset(values);
});
}
}
initializeTabs() {
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => { try { track('tab_click', { tab: tab.dataset?.tab || tab.id || 'unknown' }); } catch (e) {} });
tab.addEventListener('click', () => {
const tabsContainer = tab.closest('.tabs');
tabsContainer.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const tabName = tab.getAttribute('data-tab');
const panel = tab.closest('.panel');
panel.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
panel.querySelector(`#${tabName}-tab`)?.classList.add('active');
});
});
}
initializeCharts() {
const trainingCtx = document.getElementById('training-chart')?.getContext('2d');
const privacyCtx = document.getElementById('privacy-chart')?.getContext('2d');
const gradientCtx = document.getElementById('gradient-chart')?.getContext('2d');
if (trainingCtx) {
this.trainingChart = new Chart(trainingCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Accuracy',
borderColor: '#4caf50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
data: [],
yAxisID: 'y',
borderWidth: 3,
pointRadius: 4,
pointHoverRadius: 6,
tension: 0.1
},
{
label: 'Loss',
borderColor: '#f44336',
backgroundColor: 'rgba(244, 67, 54, 0.1)',
data: [],
yAxisID: 'y1',
borderWidth: 3,
pointRadius: 4,
pointHoverRadius: 6,
tension: 0.1,
borderDash: [5, 5] // Dashed line to differentiate from accuracy
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
padding: 20,
font: {
size: 12,
weight: 'bold'
}
}
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#ddd',
borderWidth: 1
}
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Accuracy (%)',
color: '#4caf50',
font: {
size: 14,
weight: 'bold'
}
},
min: 0,
max: 100,
ticks: {
color: '#4caf50',
font: {
weight: 'bold'
},
callback: function(value) {
return value + '%';
}
},
grid: {
color: 'rgba(76, 175, 80, 0.2)'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'Loss',
color: '#f44336',
font: {
size: 14,
weight: 'bold'
}
},
min: 0,
max: 3, // More reasonable max for loss
ticks: {
color: '#f44336',
font: {
weight: 'bold'
},
callback: function(value) {
return value.toFixed(1);
}
},
grid: {
drawOnChartArea: false, // Don't overlay grid lines
color: 'rgba(244, 67, 54, 0.2)'
},
},
x: {
title: {
display: true,
text: 'Training Progress',
font: {
size: 12,
weight: 'bold'
}
},
ticks: {
font: {
size: 11
}
}
}
}
}
});
}
if (privacyCtx) {
this.privacyChart = new Chart(privacyCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Privacy Budget (ε)',
borderColor: '#3f51b5',
data: []
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Privacy Budget (ε)'
}
}
}
}
});
}
if (gradientCtx) {
this.gradientChart = new Chart(gradientCtx, {
type: 'scatter',
data: {
datasets: [
{
label: 'Before Clipping',
borderColor: '#2196f3',
backgroundColor: 'rgba(33, 150, 243, 0.1)',
data: [],
showLine: true
},
{
label: 'After Clipping',
borderColor: '#f44336',
backgroundColor: 'rgba(244, 67, 54, 0.1)',
data: [],
showLine: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
position: 'bottom',
title: {
display: true,
text: 'Gradient Norm'
},
min: 0
},
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'Density'
},
min: 0
}
},
plugins: {
annotation: {
annotations: {
line1: {
type: 'line',
xMin: 1,
xMax: 1,
borderColor: '#f44336',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: 'Clipping Threshold',
display: true,
position: 'top'
}
}
}
}
}
}
});
}
}
async updatePrivacyBudget() {
const params = this.getParameters();
try {
const response = await fetch('/api/privacy-budget', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params)
});
const data = await response.json();
// Update UI
const budgetValue = document.getElementById('budget-value');
const budgetFill = document.getElementById('budget-fill');
if (budgetValue && budgetFill) {
budgetValue.textContent = data.epsilon.toFixed(2);
budgetFill.style.width = `${Math.min(data.epsilon / 10 * 100, 100)}%`;
try { track('privacy_budget_update', { epsilon: data.epsilon }); } catch (e) {};
// Update class for coloring
budgetFill.classList.remove('low', 'medium', 'high');
if (data.epsilon <= 1) {
budgetFill.classList.add('low');
} else if (data.epsilon <= 5) {
budgetFill.classList.add('medium');
} else {
budgetFill.classList.add('high');
}
}
} catch (error) {
console.error('Error calculating privacy budget:', error);
}
}
async toggleTraining() {
if (this.isTraining) {
this.stopTraining();
} else {
await this.startTraining();
}
}
async startTraining() {
const trainButton = document.getElementById('train-button');
const trainingStatus = document.getElementById('training-status');
const trainingStatusText = document.getElementById('training-status-text');
const currentEpochEl = document.getElementById('current-epoch');
const totalEpochsEl = document.getElementById('total-epochs');
if (!trainButton || this.isTraining) return;
this.isTraining = true;
this.epochsData = []; // Reset epoch data for streaming
trainButton.textContent = 'Stop Training';
trainButton.classList.add('running');
trainingStatus.style.display = 'flex';
// Show initialization status
if (trainingStatusText) {
trainingStatusText.textContent = 'Initializing model...';
trainingStatusText.style.color = '#ff9800'; // Orange for initializing
}
if (currentEpochEl) currentEpochEl.textContent = '0';
if (totalEpochsEl) totalEpochsEl.textContent = this.getParameters().epochs;
// Reset charts
this.resetCharts();
console.log('Starting streaming training with parameters:', this.getParameters());
// === Analytics: training started ===
try {
track('train_start', {
...this.getParameters(),
view: this.currentView
});
} catch (e) {}
// Use fetch with POST to initiate SSE stream (EventSource only supports GET)
try {
const response = await fetch('/api/train-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.getParameters())
});
if (!response.ok) {
throw new Error('Failed to start training');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
console.log('[Stream] Read chunk - done:', done, 'value size:', value?.length, 'isTraining:', this.isTraining);
if (done || !this.isTraining) {
console.log('[Stream] Stream ended or training stopped');
break;
}
const chunk = decoder.decode(value, { stream: true });
console.log('[Stream] Decoded chunk:', chunk.substring(0, 200));
buffer += chunk;
// Process complete SSE messages
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
console.log('[Stream] Processing', lines.length, 'lines, buffer remaining:', buffer.length);
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
console.log('[Stream] Parsed SSE data type:', data.type);
this.handleStreamingData(data);
} catch (parseError) {
console.warn('[Stream] Failed to parse SSE data:', parseError, 'line:', line);
}
}
}
}
} catch (error) {
if (!this.isTraining) {
console.log('Training was stopped');
return;
}
console.error('Training error:', error);
// === Analytics: training failed ===
try {
track('train_error', {
message: error.message || 'unknown',
params: this.getParameters()
});
} catch (e) {}
// Show error message to user
const errorMessage = document.createElement('div');
errorMessage.className = 'error-message';
errorMessage.textContent = error.message || 'An error occurred during training';
const labMain = document.querySelector('.lab-main');
if (labMain) {
labMain.insertBefore(errorMessage, labMain.firstChild);
setTimeout(() => errorMessage.remove(), 5000);
}
} finally {
try {
track('train_end', { ended_at: Date.now() });
} catch (e) {}
this.stopTraining();
}
}
handleStreamingData(data) {
const trainingStatusText = document.getElementById('training-status-text');
const currentEpochEl = document.getElementById('current-epoch');
const totalEpochsEl = document.getElementById('total-epochs');
const chartInfo = document.getElementById('chart-info');
console.log('[SSE] Received:', data.type, data);
switch (data.type) {
case 'status':
// Update status message
console.log('[SSE] Status update:', data.message);
if (trainingStatusText) {
trainingStatusText.textContent = data.message;
trainingStatusText.style.color = data.message.includes('Initializing') ? '#ff9800' : '#4caf50';
}
if (currentEpochEl) currentEpochEl.textContent = data.epoch;
if (totalEpochsEl) totalEpochsEl.textContent = data.total_epochs;
break;
case 'progress':
// Update progress - add new epoch data to chart
console.log('[SSE] Progress update - Epoch:', data.epoch, 'Accuracy:', data.epoch_data?.accuracy);
if (trainingStatusText) {
trainingStatusText.textContent = `Training epoch ${data.epoch}...`;
trainingStatusText.style.color = '#4caf50';
}
if (currentEpochEl) currentEpochEl.textContent = data.epoch;
if (totalEpochsEl) totalEpochsEl.textContent = data.total_epochs;
// Add epoch data to our collection
this.epochsData.push(data.epoch_data);
// Update chart with new data point
console.log('[SSE] Updating chart with epoch data, chart exists:', !!this.trainingChart);
this.updateChartRealtime(data.epoch_data);
if (chartInfo) {
chartInfo.textContent = `Showing ${this.epochsData.length} data points (epochs)`;
}
break;
case 'complete':
// Training complete - update all final results
console.log('Training complete:', data);
// Store complete data
this.epochsData = data.epochs_data || this.epochsData;
this.iterationsData = data.iterations_data || [];
// Update final results
this.updateResults(data);
// === Analytics: training succeeded ===
try {
track('train_success', {
trainer_type: data.trainer_type,
dataset: data.dataset,
model_architecture: data.model_architecture,
final_metrics: data.final_metrics,
privacy_budget: data.privacy_budget,
epochs: this.epochsData.length
});
} catch (e) {}
break;
case 'error':
console.error('Training error from server:', data.message);
const errorMessage = document.createElement('div');
errorMessage.className = 'error-message';
errorMessage.textContent = data.message || 'An error occurred during training';
const labMain = document.querySelector('.lab-main');
if (labMain) {
labMain.insertBefore(errorMessage, labMain.firstChild);
setTimeout(() => errorMessage.remove(), 5000);
}
break;
}
}
updateChartRealtime(epochData) {
console.log('[Chart] updateChartRealtime called, chart exists:', !!this.trainingChart, 'epochData:', epochData);
if (!this.trainingChart) {
console.error('[Chart] Training chart not initialized!');
return;
}
// Add new data point to chart
const label = `Epoch ${epochData.epoch}`;
this.trainingChart.data.labels.push(label);
this.trainingChart.data.datasets[0].data.push(epochData.accuracy);
this.trainingChart.data.datasets[1].data.push(epochData.loss);
console.log('[Chart] Updated data - labels:', this.trainingChart.data.labels.length,
'accuracies:', this.trainingChart.data.datasets[0].data,
'losses:', this.trainingChart.data.datasets[1].data);
// Auto-adjust loss scale
const losses = this.trainingChart.data.datasets[1].data;
const maxLoss = Math.max(...losses);
const minLoss = Math.min(...losses);
this.trainingChart.options.scales.y1.max = Math.max(maxLoss * 1.1, 3);
this.trainingChart.options.scales.y1.min = Math.max(0, minLoss * 0.9);
// Update chart with animation
this.trainingChart.update('none'); // 'none' for faster updates during streaming
console.log('[Chart] Chart updated');
}
stopTraining() {
// Mark as not training - this will cause the stream reader to stop
this.isTraining = false;
// Abort any pending training request
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
// Close any active event source
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
const trainButton = document.getElementById('train-button');
if (trainButton) {
trainButton.textContent = 'Run Training';
trainButton.classList.remove('running');
}
const trainingStatus = document.getElementById('training-status');
if (trainingStatus) {
trainingStatus.style.display = 'none';
}
}
resetCharts() {
if (this.trainingChart) {
this.trainingChart.data.labels = [];
this.trainingChart.data.datasets[0].data = [];
this.trainingChart.data.datasets[1].data = [];
this.trainingChart.update();
}
if (this.privacyChart) {
this.privacyChart.data.labels = [];
this.privacyChart.data.datasets[0].data = [];
this.privacyChart.update();
}
if (this.gradientChart) {
this.gradientChart.data.datasets[0].data = [];
this.gradientChart.data.datasets[1].data = [];
this.gradientChart.update();
}
}
switchView(view) {
this.currentView = view;
try { track('view_switch', { view }); } catch (e) {};
// Update button states
document.querySelectorAll('.view-toggle').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById(`view-${view}`).classList.add('active');
// Update chart with current data
if (view === 'epochs' && this.epochsData.length > 0) {
this.updateChartsWithData(this.epochsData, 'epochs');
} else if (view === 'iterations' && this.iterationsData.length > 0) {
this.updateChartsWithData(this.iterationsData, 'iterations');
}
}
updateCharts(data) {
if (!this.trainingChart || !data) return;
console.log('Updating charts with data:', data); // Debug log
// Store data for view switching
if (data.epochs_data) {
this.epochsData = data.epochs_data;
}
if (data.iterations_data) {
this.iterationsData = data.iterations_data;
}
// Use current view to determine which data to display
if (this.currentView === 'epochs' && this.epochsData.length > 0) {
this.updateChartsWithData(this.epochsData, 'epochs');
} else if (this.currentView === 'iterations' && this.iterationsData.length > 0) {
this.updateChartsWithData(this.iterationsData, 'iterations');
} else if (this.epochsData.length > 0) {
// Fallback to epochs if iterations not available
this.updateChartsWithData(this.epochsData, 'epochs');
}
}
updateChartsWithData(chartData, dataType) {
if (!this.trainingChart || !chartData) return;
// Update training metrics chart
const labels = chartData.map(d =>
dataType === 'epochs' ? `Epoch ${d.epoch}` : `Iter ${d.iteration}`
);
const accuracies = chartData.map(d => d.accuracy);
const losses = chartData.map(d => d.loss);
console.log(`${dataType} - Accuracies:`, accuracies);
console.log(`${dataType} - Losses:`, losses);
this.trainingChart.data.labels = labels;
this.trainingChart.data.datasets[0].data = accuracies;
this.trainingChart.data.datasets[1].data = losses;
// Auto-adjust loss scale based on actual data
const maxLoss = Math.max(...losses);
const minLoss = Math.min(...losses);
this.trainingChart.options.scales.y1.max = Math.max(maxLoss * 1.1, 3);
this.trainingChart.options.scales.y1.min = Math.max(0, minLoss * 0.9);
// Update chart info
const chartInfo = document.getElementById('chart-info');
if (chartInfo) {
chartInfo.textContent = `Showing ${chartData.length} data points (${dataType})`;
}
this.trainingChart.update();
// Update current epoch display
const currentEpoch = document.getElementById('current-epoch');
const totalEpochs = document.getElementById('total-epochs');
if (currentEpoch && totalEpochs && dataType === 'epochs') {
currentEpoch.textContent = chartData.length;
totalEpochs.textContent = this.getParameters().epochs;
}
// Update privacy budget chart (only for epochs view)
if (this.privacyChart && dataType === 'epochs') {
const privacyBudgets = chartData.map((_, i) =>
this.calculateEpochPrivacy(i + 1)
);
this.privacyChart.data.labels = labels;
this.privacyChart.data.datasets[0].data = privacyBudgets;
this.privacyChart.update();
}
// Update gradient visualization
if (this.gradientChart) {
const clippingNorm = this.getParameters().clipping_norm;
// Generate gradient data if not provided in chartData
let gradientData;
if (chartData[chartData.length - 1]?.gradient_info) {
gradientData = chartData[chartData.length - 1].gradient_info;
} else {
// Generate synthetic gradient data
const beforeClipping = [];
const afterClipping = [];
// Generate log-normal distributed gradients
const mu = Math.log(clippingNorm) - 0.5;
const sigma = 0.8;
for (let i = 0; i < 100; i++) {
const u1 = Math.random();
const u2 = Math.random();
const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
const norm = Math.exp(mu + sigma * z);
const density = Math.exp(-(Math.pow(Math.log(norm) - mu, 2) / (2 * sigma * sigma))) /
(norm * sigma * Math.sqrt(2 * Math.PI));
const y = 0.2 + 0.8 * (density / 0.8) + 0.1 * (Math.random() - 0.5);
beforeClipping.push({ x: norm, y: y });
afterClipping.push({ x: Math.min(norm, clippingNorm), y: y });
}
gradientData = {
before_clipping: beforeClipping.sort((a, b) => a.x - b.x),
after_clipping: afterClipping.sort((a, b) => a.x - b.x)
};
}
// Update gradient chart
this.gradientChart.data.datasets[0].data = gradientData.before_clipping;
this.gradientChart.data.datasets[1].data = gradientData.after_clipping;
// Update clipping threshold line
this.gradientChart.options.plugins.annotation.annotations.line1 = {
type: 'line',
xMin: clippingNorm,
xMax: clippingNorm,
borderColor: '#f44336',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: `Clipping Threshold (C=${clippingNorm.toFixed(1)})`,
display: true,
position: 'top'
}
};
// Update x-axis scale based on clipping norm
this.gradientChart.options.scales.x.max = Math.max(clippingNorm * 2.5, 5);
this.gradientChart.update('active');
}
}
updateResults(data) {
// Hide no-results message and show results content
document.getElementById('no-results').style.display = 'none';
document.getElementById('results-content').style.display = 'block';
// Update metrics
document.getElementById('accuracy-value').textContent =
data.final_metrics.accuracy.toFixed(1) + '%';
document.getElementById('loss-value').textContent =
data.final_metrics.loss.toFixed(3);
document.getElementById('training-time-value').textContent =
data.final_metrics.training_time.toFixed(1) + 's';
// Update privacy budget display (make it dynamic)
const privacyBudgetElement = document.getElementById('privacy-budget-value');
if (privacyBudgetElement) {
privacyBudgetElement.textContent = `ε=${data.privacy_budget.toFixed(1)}`;
}
// Update privacy-utility trade-off explanation dynamically
const tradeoffElement = document.getElementById('tradeoff-explanation');
if (tradeoffElement) {
const accuracy = data.final_metrics.accuracy.toFixed(1);
const epsilon = data.privacy_budget.toFixed(1);
// Generate realistic trade-off assessment
let tradeoffAssessment;
if (data.final_metrics.accuracy >= 85) {
tradeoffAssessment = "This is an excellent trade-off for most applications.";
} else if (data.final_metrics.accuracy >= 75) {
tradeoffAssessment = "This is a good trade-off for most applications.";
} else if (data.final_metrics.accuracy >= 65) {
tradeoffAssessment = "This trade-off may be acceptable for privacy-critical applications.";
} else if (data.final_metrics.accuracy >= 50) {
tradeoffAssessment = "Low utility - consider reducing noise or increasing clipping norm.";
} else {
tradeoffAssessment = "Very poor utility - privacy parameters need significant adjustment.";
}
tradeoffElement.textContent =
`This model achieved ${accuracy}% accuracy with a privacy budget of ε=${epsilon}. ${tradeoffAssessment}`;
}
// Update recommendations
const recommendationList = document.querySelector('.recommendation-list');
recommendationList.innerHTML = '';
data.recommendations.forEach(rec => {
const item = document.createElement('li');
item.className = 'recommendation-item';
item.innerHTML = `
${rec.icon}
${rec.text}
`;
recommendationList.appendChild(item);
});
}
getParameters() {
return {
clipping_norm: parseFloat(document.getElementById('clipping-norm').value),
noise_multiplier: parseFloat(document.getElementById('noise-multiplier').value),
batch_size: parseInt(document.getElementById('batch-size').value),
learning_rate: parseFloat(document.getElementById('learning-rate').value),
epochs: parseInt(document.getElementById('epochs').value),
dataset: document.getElementById('dataset-select').value,
model_architecture: document.getElementById('model-select').value
};
}
applyPreset(values) {
document.getElementById('clipping-norm').value = values.clippingNorm;
document.getElementById('noise-multiplier').value = values.noiseMultiplier;
document.getElementById('batch-size').value = values.batchSize;
document.getElementById('learning-rate').value = values.learningRate;
document.getElementById('epochs').value = values.epochs;
// Update displayed values
document.getElementById('clipping-norm-value').textContent = values.clippingNorm;
document.getElementById('noise-multiplier-value').textContent = values.noiseMultiplier;
document.getElementById('batch-size-value').textContent = values.batchSize;
document.getElementById('learning-rate-value').textContent = values.learningRate;
document.getElementById('epochs-value').textContent = values.epochs;
this.updatePrivacyBudget();
}
calculateEpochPrivacy(epoch) {
const params = this.getParameters();
// Get dataset size based on selection
let datasetSize;
switch(params.dataset) {
case 'cifar10':
datasetSize = 50000; // CIFAR-10 training set size
break;
case 'fashion-mnist':
datasetSize = 60000; // Fashion-MNIST training set size
break;
case 'mnist':
default:
datasetSize = 60000; // MNIST training set size
break;
}
const samplingRate = params.batch_size / datasetSize;
const steps = epoch * (1 / samplingRate);
const delta = 1e-5;
const c = Math.sqrt(2 * Math.log(1.25 / delta));
return Math.min((c * samplingRate * Math.sqrt(steps)) / params.noise_multiplier, 10);
}
updateGradientVisualization(clippingNorm) {
if (!this.gradientChart) return;
// Generate random gradient norms following a log-normal distribution
const numPoints = 100;
const beforeClipping = [];
const afterClipping = [];
// Parameters for log-normal distribution
const mu = Math.log(clippingNorm) - 0.5;
const sigma = 0.8;
// Generate gradient norms
for (let i = 0; i < numPoints; i++) {
// Generate log-normal distributed gradient norms
const u1 = Math.random();
const u2 = Math.random();
const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
const norm = Math.exp(mu + sigma * z);
// Calculate density using kernel density estimation
const density = Math.exp(-(Math.pow(Math.log(norm) - mu, 2) / (2 * sigma * sigma))) / (norm * sigma * Math.sqrt(2 * Math.PI));
// Normalize density and add some randomness
const y = 0.2 + 0.8 * (density / 0.8) + 0.1 * (Math.random() - 0.5);
beforeClipping.push({ x: norm, y: y });
afterClipping.push({ x: Math.min(norm, clippingNorm), y: y });
}
// Sort points by x-value for smoother lines
beforeClipping.sort((a, b) => a.x - b.x);
afterClipping.sort((a, b) => a.x - b.x);
// Update chart data
this.gradientChart.data.datasets[0].data = beforeClipping;
this.gradientChart.data.datasets[1].data = afterClipping;
// Update clipping threshold line
this.gradientChart.options.plugins.annotation.annotations.line1 = {
type: 'line',
xMin: clippingNorm,
xMax: clippingNorm,
borderColor: '#f44336',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: `Clipping Threshold (C=${clippingNorm.toFixed(1)})`,
display: true,
position: 'top'
}
};
// Update x-axis scale based on clipping norm
this.gradientChart.options.scales.x.max = Math.max(clippingNorm * 2.5, 5);
// Update the chart with animation
this.gradientChart.update('active');
}
updateGradientVisualizationWithData(beforeClipping, afterClipping, clippingNorm) {
if (!this.gradientChart) return;
// Update chart data with real training data
this.gradientChart.data.datasets[0].data = beforeClipping;
this.gradientChart.data.datasets[1].data = afterClipping;
// Update clipping threshold line
this.gradientChart.options.plugins.annotation.annotations.line1 = {
type: 'line',
xMin: clippingNorm,
xMax: clippingNorm,
borderColor: '#f44336',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: `Clipping Threshold (C=${clippingNorm.toFixed(1)})`,
display: true,
position: 'top'
}
};
// Update x-axis scale based on clipping norm
this.gradientChart.options.scales.x.max = Math.max(clippingNorm * 2.5, 5);
// Update the chart with animation
this.gradientChart.update('active');
}
}
// Initialize the application when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.dpsgdExplorer = new DPSGDExplorer();
});
function setOptimalParameters() {
// Research-validated optimal parameters for DP-SGD on MNIST
// Based on Optax/TF Privacy: achieves ~96-97% accuracy with reasonable privacy
document.getElementById('clipping-norm').value = '1.0'; // Standard clipping norm
document.getElementById('noise-multiplier').value = '1.1'; // Moderate noise (ε≈3-5)
document.getElementById('batch-size').value = '256'; // Large batches for stability
document.getElementById('learning-rate').value = '0.15'; // Higher LR works well for DP-SGD
document.getElementById('epochs').value = '30'; // Sufficient for convergence
// Update displays
updateClippingNormDisplay();
updateNoiseMultiplierDisplay();
updateBatchSizeDisplay();
updateLearningRateDisplay();
updateEpochsDisplay();
}