// ======= Analytics & Identity Bootstrap ======= const ANALYTICS_ENDPOINT = '/api/track'; const COOKIE_NAME = 'vid'; // Generate a stable session id (per browser) const sessionId = (() => { const key = 'dpsgd_session_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; })(); // Minimal user context (non-PII by default). Call identify({ id, role, org, plan }) if you have a login. let userContext = { vid: null, 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(); 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(), sessionId, eventType, path: location.pathname, payload, user: { id: userContext.id, role: userContext.role, org: userContext.org, plan: userContext.plan }, vid: userContext.vid }; 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.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')); } 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() { const presets = { 'high-privacy': { clippingNorm: 1.0, noiseMultiplier: 1.5, batchSize: 256, learningRate: 0.005, epochs: 30 }, 'balanced': { clippingNorm: 1.0, noiseMultiplier: 1.0, batchSize: 128, learningRate: 0.01, epochs: 30 }, 'high-utility': { clippingNorm: 1.5, noiseMultiplier: 0.5, batchSize: 64, learningRate: 0.02, 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'); if (!trainButton || this.isTraining) return; this.isTraining = true; trainButton.textContent = 'Stop Training'; trainButton.classList.add('running'); trainingStatus.style.display = 'flex'; // Reset charts this.resetCharts(); try { console.log('Starting training with parameters:', this.getParameters()); // Debug log const response = await fetch('/api/train', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(this.getParameters()) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Unknown error occurred'); } console.log('Received training data:', data); // Debug log // Update charts and results this.updateCharts(data); this.updateResults(data); } catch (error) { console.error('Training error:', error); // Show error message to user const errorMessage = document.createElement('div'); errorMessage.className = 'error-message'; errorMessage.textContent = error.message || 'An error occurred during training'; document.querySelector('.lab-main').insertBefore(errorMessage, document.querySelector('.lab-main').firstChild); // Remove error message after 5 seconds setTimeout(() => { errorMessage.remove(); }, 5000); } finally { this.stopTraining(); } } stopTraining() { this.isTraining = false; const trainButton = document.getElementById('train-button'); if (trainButton) { trainButton.textContent = 'Run Training'; trainButton.classList.remove('running'); } document.getElementById('training-status').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() { // Set optimal parameters based on actual MNIST DP-SGD training results // These values achieve ~95% accuracy with reasonable privacy budget (ε≈15) document.getElementById('clipping-norm').value = '2.0'; // Balanced clipping norm document.getElementById('noise-multiplier').value = '1.0'; // Moderate noise for good privacy document.getElementById('batch-size').value = '256'; // Large batches for DP-SGD stability document.getElementById('learning-rate').value = '0.05'; // Balanced learning rate document.getElementById('epochs').value = '30'; // Sufficient epochs for convergence // Update displays updateClippingNormDisplay(); updateNoiseMultiplierDisplay(); updateBatchSizeDisplay(); updateLearningRateDisplay(); updateEpochsDisplay(); }