// Cyclic Voltammetry Parameter Extraction & Physical Model Fitting // High-Performance JAX Auto-Diff Engine Client // Global State let expPotential = []; let expCurrent = []; let latestResults = null; let stagedFileContent = null; let stagedFileName = "No file selected"; let detectedColumns = []; // Global Modal Handler window.toggleModal = function(modalId, show) { const modal = document.getElementById(modalId); if (modal) { if (show) { modal.classList.remove('hidden'); } else { modal.classList.add('hidden'); } } }; // Global Advanced Settings Toggle window.toggleAdvanced = function() { const advToggle = document.getElementById('advanced-toggle'); const advContent = document.getElementById('advanced-content'); const toggleIcon = document.getElementById('toggle-icon'); if (advContent) { const isHidden = advContent.classList.toggle('hidden'); if (advToggle) advToggle.setAttribute('aria-expanded', !isHidden); if (toggleIcon) { toggleIcon.style.transform = isHidden ? 'rotate(0deg)' : 'rotate(180deg)'; } } }; // Delimiter Detection function detectDelimiter(line) { const commas = (line.match(/,/g) || []).length; const tabs = (line.match(/\t/g) || []).length; const semicolons = (line.match(/;/g) || []).length; if (tabs > commas && tabs > semicolons) return '\t'; if (semicolons > commas && semicolons > tabs) return ';'; return ','; } // Generalized 2-Column CSV Analysis & Dropdown Populator function analyzeCSVAndPopulateColumns(content) { const lines = content.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//')); if (lines.length === 0) return; const delimiter = detectDelimiter(lines[0]); const firstLineFields = lines[0].split(delimiter).map(s => s.trim()); const secondLineFields = lines.length > 1 ? lines[1].split(delimiter).map(s => s.trim()) : []; let hasHeader = false; if (firstLineFields.some(f => isNaN(parseFloat(f)) && f.length > 0)) { hasHeader = true; } const colCount = hasHeader ? firstLineFields.length : (secondLineFields.length || firstLineFields.length); detectedColumns = []; const colCounts = new Array(colCount).fill(0); const startRow = hasHeader ? 1 : 0; for (let i = startRow; i < lines.length; i++) { const tokens = lines[i].split(delimiter); for (let c = 0; c < colCount; c++) { if (c < tokens.length) { const s = tokens[c].trim(); if (s !== "" && !isNaN(parseFloat(s))) { colCounts[c]++; } } } } let defaultPot = 0; let defaultCur = colCount > 1 ? 1 : 0; for (let c = 0; c < colCount; c++) { let rawHeader = hasHeader && firstLineFields[c] ? firstLineFields[c] : `Column ${c}`; let cleanHeader = rawHeader.replace(/["']/g, ''); let ptsStr = ` (${colCounts[c].toLocaleString()} pts)`; let displayName = `${cleanHeader}${ptsStr}`; detectedColumns.push({ index: c, name: displayName, rawName: cleanHeader, count: colCounts[c] }); // Smart column auto-detection based on header text const lower = cleanHeader.toLowerCase(); if (lower.includes('potential') || lower.includes('volt') || lower === 'v' || lower.includes('(v)')) { defaultPot = c; } else if (lower.includes('current') || lower.includes('curr') || lower === 'i' || lower.includes('(a)') || lower.includes('amp')) { defaultCur = c; } } const potSelect = document.getElementById('pot_col'); const curSelect = document.getElementById('cur_col'); const metaBar = document.getElementById('column-meta-bar'); const metaText = document.getElementById('detected-columns-text'); if (potSelect && curSelect) { potSelect.innerHTML = ''; curSelect.innerHTML = ''; detectedColumns.forEach(col => { const optP = document.createElement('option'); optP.value = col.index; optP.textContent = `[Col ${col.index}] ${col.name}`; potSelect.appendChild(optP); const optC = document.createElement('option'); optC.value = col.index; optC.textContent = `[Col ${col.index}] ${col.name}`; curSelect.appendChild(optC); }); potSelect.value = defaultPot; curSelect.value = defaultCur; if (metaBar && metaText) { metaBar.classList.add('visible'); metaText.innerHTML = `Loaded ${colCount} column${colCount > 1 ? 's' : ''}${lines.length - startRow} rows`; } window.updateLivePreviewFromColumns(); } } // Global Live Preview & Baseline Plotter window.updateLivePreviewFromColumns = function() { if (!stagedFileContent) return; const potSelect = document.getElementById('pot_col'); const curSelect = document.getElementById('cur_col'); if (!potSelect || !curSelect) return; const potCol = parseInt(potSelect.value, 10); const curCol = parseInt(curSelect.value, 10); const lines = stagedFileContent.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//')); if (lines.length === 0) return; const delimiter = detectDelimiter(lines[0]); const firstTokens = lines[0].split(delimiter).map(t => t.trim()); let startIndex = 0; if (firstTokens.length > Math.max(potCol, curCol)) { if (isNaN(parseFloat(firstTokens[potCol])) || isNaN(parseFloat(firstTokens[curCol]))) { startIndex = 1; } } const previewPot = []; const previewCur = []; for (let i = startIndex; i < lines.length; i++) { const tokens = lines[i].split(delimiter); if (tokens.length > Math.max(potCol, curCol)) { const vStr = tokens[potCol].trim(); const cStr = tokens[curCol].trim(); if (vStr !== "" && cStr !== "") { const v = parseFloat(vStr); const c = parseFloat(cStr); if (!isNaN(v) && !isNaN(c)) { previewPot.push(v); previewCur.push(c); } } } } if (previewPot.length > 0) { expPotential = previewPot; expCurrent = previewCur; const vMin = Math.min(...previewPot); const vMax = Math.max(...previewPot); const iMin = Math.min(...previewCur); const iMax = Math.max(...previewCur); const vMinInput = document.getElementById('v_min'); const vMaxInput = document.getElementById('v_max'); if (vMinInput && vMaxInput) { vMinInput.value = vMin.toFixed(3); vMaxInput.value = vMax.toFixed(3); } const vRangeSpan = document.getElementById('stat-v-range'); const iRangeSpan = document.getElementById('stat-i-range'); const ptsSpan = document.getElementById('stat-points-count'); const statsBox = document.getElementById('col-stats-preview'); if (vRangeSpan) vRangeSpan.innerText = `${vMin.toFixed(3)} V to ${vMax.toFixed(3)} V`; if (iRangeSpan) iRangeSpan.innerText = `${iMin.toExponential(2)} A to ${iMax.toExponential(2)} A`; if (ptsSpan) ptsSpan.innerText = `${previewPot.length.toLocaleString()}`; if (statsBox) statsBox.classList.add('visible'); const statusDetails = document.getElementById('status-details'); if (statusDetails) { statusDetails.innerHTML = `Loaded ${stagedFileName} • Potential (Col ${potCol}) & Current (Col ${curCol}) • ${previewPot.length.toLocaleString()} points ready for optimization.`; } renderInitialExpPlot(previewPot, previewCur); } }; function setLoadedFile(content, name) { stagedFileContent = content; stagedFileName = name; const fileNameDisplay = document.getElementById('file-name-display'); if (fileNameDisplay) { fileNameDisplay.innerText = name; fileNameDisplay.classList.add('has-file'); } analyzeCSVAndPopulateColumns(content); } // Global File Input Handlers window.handleCSVFileChange = function(input) { if (!input || !input.files || input.files.length === 0) return; const file = input.files[0]; const reader = new FileReader(); reader.onload = function(ev) { setLoadedFile(ev.target.result, file.name); }; reader.readAsText(file); }; window.handleCSVDrop = function(event) { if (!event || !event.dataTransfer || !event.dataTransfer.files || event.dataTransfer.files.length === 0) return; const file = event.dataTransfer.files[0]; const reader = new FileReader(); reader.onload = function(ev) { setLoadedFile(ev.target.result, file.name); }; reader.readAsText(file); }; // Helpers for Gradio Element Discovery and Value Setting function findGradioElement(selector) { let el = document.querySelector(selector); if (el) return el; const grApp = document.querySelector('gradio-app'); if (grApp && grApp.shadowRoot) { return grApp.shadowRoot.querySelector(selector); } return null; } function setGradioInputValue(containerSelector, val) { const container = findGradioElement(containerSelector); if (!container) return false; const input = container.querySelector('textarea, input') || container; try { const proto = Object.getPrototypeOf(input); const desc = Object.getOwnPropertyDescriptor(proto, 'value'); if (desc && desc.set) { desc.set.call(input, val); } else { input.value = val; } } catch (e) { input.value = val; } input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); input.dispatchEvent(new Event('change', { bubbles: true, composed: true })); return true; } // Global Form Submit Handler window.handleFormSubmit = async function(e) { if (e && e.preventDefault) e.preventDefault(); if (!stagedFileContent) { alert('Please select and upload a cyclic voltammetry CSV data file first.'); return false; } const cvForm = document.getElementById('cv-form'); const formData = new FormData(cvForm); const config = {}; formData.forEach((value, key) => { config[key] = value; }); config.pot_col = parseInt(document.getElementById('pot_col').value, 10); config.cur_col = parseInt(document.getElementById('cur_col').value, 10); startOptimizationUI(); executeZeroGPUSolver(stagedFileContent, config); return false; }; // Execution via Native ZeroGPU Pipeline & Direct HTTP API async function executeZeroGPUSolver(fileContent, config) { const stageEl = document.getElementById('status-stage'); const detailsEl = document.getElementById('status-details'); if (stageEl) stageEl.innerText = '⚡ Optimizing Physical Model Parameters...'; if (detailsEl) detailsEl.innerText = 'Executing multi-stage non-linear L-BFGS-B optimization on JAX auto-diff engine...'; // 1. Native Gradio ZeroGPU Queue Trigger const fileSet = setGradioInputValue('#gr_input_file', fileContent); const configSet = setGradioInputValue('#gr_input_config', JSON.stringify(config)); const grBtn = findGradioElement('#gr_trigger_btn button') || findGradioElement('#gr_trigger_btn'); if (fileSet && configSet && grBtn) { const startTime = Date.now(); setGradioInputValue('#gr_output_json', ''); const pollInterval = setInterval(() => { const outContainer = findGradioElement('#gr_output_json'); const outEl = outContainer ? (outContainer.querySelector('textarea, input') || outContainer) : null; const textVal = (outEl ? outEl.value : "") || (outContainer ? outContainer.innerText : ""); if (textVal && textVal.trim().startsWith('{') && textVal.trim().endsWith('}')) { clearInterval(pollInterval); try { const data = JSON.parse(textVal.trim()); handleSolverMessage(data); } catch (e) { handleSolverError(`Failed to parse output JSON: ${e.message}`); } return; } const elapsedSec = Math.floor((Date.now() - startTime) / 1000); if (stageEl) stageEl.innerText = `⚡ Non-Linear Parameter Extraction (${elapsedSec}s)...`; if (detailsEl) detailsEl.innerText = `Solving 1D diffusion PDE and optimizing Fermi-Dirac DOS sub-bands...`; if (Date.now() - startTime > 180000) { clearInterval(pollInterval); handleSolverError("Optimization calculation timed out (3 min)."); } }, 500); grBtn.click(); return; } // 2. Direct HTTP POST fallback const endpoints = [ window.location.origin + "/api/solve", window.location.origin + "/solve", "http://127.0.0.1:8000/api/solve" ]; for (const endpoint of endpoints) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 180000); const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_content: fileContent, config: config }), signal: controller.signal }); clearTimeout(timeoutId); if (res.ok) { const data = await res.json(); handleSolverMessage(data); return; } } catch (err) { console.warn(`HTTP solve attempt on ${endpoint} failed:`, err); } } handleSolverError("Could not communicate with solver engine. Please verify the space is running."); } function handleSolverMessage(data) { const stageEl = document.getElementById('status-stage'); const detailsEl = document.getElementById('status-details'); if (data.type === 'done') { if (stageEl) stageEl.innerText = '✓ Physical Model Parameters Successfully Extracted'; if (detailsEl) detailsEl.innerText = `Optimization converged in ${data.total_iterations || 100} iterations. Model fit overlay and diagnostic spectra rendered below.`; stopOptimizationUI(); latestResults = data; displayExtractedResults(data); // Update primary plot with simulation overlay if (data.plots && data.plots.sim_current && window.Plotly) { updateLivePlotProgress({ potential: data.plots.exp_potential, current: data.plots.sim_current }); } } else if (data.type === 'error') { handleSolverError(data.message || 'An error occurred during calculation.'); } } function handleSolverError(msg) { const stageEl = document.getElementById('status-stage'); const detailsEl = document.getElementById('status-details'); if (stageEl) stageEl.innerText = '❌ Calculation Notice'; if (detailsEl) detailsEl.innerText = msg; stopOptimizationUI(); alert(`Solver Message: ${msg}`); } function startOptimizationUI() { const spinner = document.getElementById('status-spinner'); const submitBtn = document.getElementById('submit-btn'); if (spinner) spinner.classList.remove('hidden'); if (submitBtn) { submitBtn.disabled = true; submitBtn.innerText = 'Extracting Parameters...'; } } function stopOptimizationUI() { const spinner = document.getElementById('status-spinner'); const submitBtn = document.getElementById('submit-btn'); if (spinner) spinner.classList.add('hidden'); if (submitBtn) { submitBtn.disabled = false; submitBtn.innerText = 'Execute Optimization'; } } // Scientific Academic Plotly Layout Configuration const layoutConfig = { paper_bgcolor: 'transparent', plot_bgcolor: 'transparent', font: { family: 'Inter, -apple-system, sans-serif', color: '#94a3b8', size: 12 }, margin: { l: 75, r: 35, t: 40, b: 55 }, xaxis: { gridcolor: 'rgba(255, 255, 255, 0.07)', zerolinecolor: 'rgba(255, 255, 255, 0.15)', tickfont: { color: '#94a3b8', size: 11 }, titlefont: { color: '#f1f5f9', size: 13 } }, yaxis: { gridcolor: 'rgba(255, 255, 255, 0.07)', zerolinecolor: 'rgba(255, 255, 255, 0.15)', tickfont: { color: '#94a3b8', size: 11 }, titlefont: { color: '#f1f5f9', size: 13 }, tickformat: '.2e' } }; function renderInitialExpPlot(pot, cur) { if (!window.Plotly) return; const traceExp = { x: pot, y: cur, mode: 'lines', type: 'scatter', name: 'Experimental Voltammogram', line: { color: '#38bdf8', width: 2.2 } }; const layout = Object.assign({}, layoutConfig, { title: { text: `Cyclic Voltammogram (${stagedFileName})`, font: { color: '#ffffff', size: 14 } }, xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Applied Potential V (V vs. Ref)' }), yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Current I (A)' }), showlegend: true, legend: { x: 0.02, y: 0.98, bgcolor: 'rgba(15, 23, 42, 0.8)', font: { color: '#f1f5f9' } } }); Plotly.react('live-chart', [traceExp], layout, { responsive: true, displaylogo: false }); } function updateLivePlotProgress(currentFit) { if (!window.Plotly) return; const traceExp = { x: expPotential, y: expCurrent, mode: 'lines', type: 'scatter', name: 'Experimental Data', line: { color: '#38bdf8', width: 2.2 } }; const traceSim = { x: currentFit.potential || expPotential, y: currentFit.current, mode: 'lines', type: 'scatter', name: 'Fitted Physical Model', line: { color: '#f43f5e', width: 2.6 } }; const layout = Object.assign({}, layoutConfig, { title: { text: 'Experimental vs. Fitted Cyclic Voltammogram Overlay', font: { color: '#ffffff', size: 14 } }, xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Applied Potential V (V vs. Ref)' }), yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Current I (A)' }), showlegend: true, legend: { x: 0.02, y: 0.98, bgcolor: 'rgba(15, 23, 42, 0.8)', font: { color: '#f1f5f9' } } }); Plotly.react('live-chart', [traceExp, traceSim], layout, { responsive: true, displaylogo: false }); } function displayExtractedResults(results) { const resultsPanel = document.getElementById('results-panel'); if (resultsPanel) resultsPanel.classList.remove('hidden'); const paramsDiv = document.getElementById('params-output'); if (paramsDiv) { paramsDiv.innerHTML = ''; const params = results.params || {}; const cards = [ { label: 'Diffusivity Constant (D₀)', value: `${(params.D0 || 0).toExponential(3)} cm²/s` }, { label: 'Thermodynamic Potential (V_c)', value: `${(params.Vc || 0).toFixed(4)} V` }, { label: 'Asymmetry Factor Left (β_L)', value: `${(params.beta_L || 0).toFixed(4)} V⁻²` }, { label: 'Asymmetry Factor Right (β_R)', value: `${(params.beta_R || 0).toFixed(4)} V⁻²` }, { label: 'Baseline DC Offset (I_offset)', value: `${(params.I_offset || 0).toExponential(3)} A` }, { label: 'Objective Loss (L_final)', value: results.final_loss ? results.final_loss.toExponential(4) : 'Converged' } ]; cards.forEach(c => { const card = document.createElement('div'); card.className = 'stat-card'; card.innerHTML = ` ${c.label} ${c.value} `; paramsDiv.appendChild(card); }); } if (results.plots) { renderSecondaryPlots(results.plots); } } function renderSecondaryPlots(plots) { if (!window.Plotly) return; // DOS Plot const dosTraces = []; if (plots.dos_peaks && plots.dos_peaks.length > 0) { plots.dos_peaks.forEach((peak, i) => { dosTraces.push({ x: plots.v_plot, y: peak, mode: 'lines', type: 'scatter', name: `Sub-band ${i+1}`, line: { width: 1, dash: 'dot', color: 'rgba(56, 189, 248, 0.35)' }, showlegend: false }); }); } dosTraces.push({ x: plots.v_plot, y: plots.dos_total, mode: 'lines', type: 'scatter', name: 'Total DOS(V)', line: { color: '#10b981', width: 2.5 } }); const dosLayout = Object.assign({}, layoutConfig, { title: { text: 'Extracted Density of States DOS(V)', font: { color: '#ffffff', size: 14 } }, xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Potential V (V vs. Ref)', autorange: true }), yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'DOS (a.u.)', autorange: true, tickformat: '.2e' }), showlegend: false }); Plotly.react('dos-chart', dosTraces, dosLayout, { responsive: true, displaylogo: false }); // Diffusivity D(V) Plot const traceDiff = { x: plots.v_plot, y: plots.d_of_v, mode: 'lines', type: 'scatter', name: 'D(V)', line: { color: '#38bdf8', width: 2.5 } }; const diffLayout = Object.assign({}, layoutConfig, { title: { text: 'Voltage-Dependent Diffusivity Profile D(V)', font: { color: '#ffffff', size: 14 } }, xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Potential V (V vs. Ref)', autorange: true }), yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Diffusivity D (cm²/s)', type: 'log', autorange: true, tickformat: '.1e' }), showlegend: false }); Plotly.react('diffusivity-chart', [traceDiff], diffLayout, { responsive: true, displaylogo: false }); } // Global Export Functions window.exportResultsJson = function() { if (!latestResults) return; const jsonStr = JSON.stringify(latestResults, null, 2); downloadFile(jsonStr, 'cv_extracted_parameters.json', 'application/json'); }; window.exportResultsCsv = function() { if (!latestResults || !latestResults.plots) return; const p = latestResults.plots; const rows = ["Index,Potential_V,Exp_Current_A,Sim_Current_A,V_Plot,D_of_V,DOS_Total"]; const maxLen = Math.max(p.exp_potential.length, p.v_plot.length); for (let i = 0; i < maxLen; i++) { const pot = i < p.exp_potential.length ? p.exp_potential[i] : ""; const expCur = i < p.exp_current.length ? p.exp_current[i] : ""; const simCur = i < p.sim_current.length ? p.sim_current[i] : ""; const vp = i < p.v_plot.length ? p.v_plot[i] : ""; const dv = i < p.d_of_v.length ? p.d_of_v[i] : ""; const dos = i < p.dos_total.length ? p.dos_total[i] : ""; rows.push(`${i},${pot},${expCur},${simCur},${vp},${dv},${dos}`); } downloadFile(rows.join("\n"), 'cv_extracted_curves.csv', 'text/csv'); }; function downloadFile(content, fileName, contentType) { const a = document.createElement("a"); const file = new Blob([content], { type: contentType }); a.href = URL.createObjectURL(file); a.download = fileName; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(a.href); }, 100); } // Master Initialization Function window.__initCVApp = function() { if (window.Plotly && expPotential.length > 0) { Plotly.Plots.resize('live-chart'); } }; // Window resize observer to keep Plotly charts perfectly proportioned window.addEventListener('resize', () => { if (window.Plotly) { const chartIds = ['live-chart', 'dos-chart', 'diffusivity-chart']; chartIds.forEach(id => { const el = document.getElementById(id); if (el && el.data) { Plotly.Plots.resize(id); } }); } }); // Run initialization immediately and on DOM load if (typeof document !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', window.__initCVApp); } else { window.__initCVApp(); } }