// 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();
}
}