Spaces:
Running on Zero
Running on Zero
Update app.js
Browse files
app.js
CHANGED
|
@@ -1,53 +1,13 @@
|
|
| 1 |
-
//
|
| 2 |
-
//
|
| 3 |
|
| 4 |
// Global State
|
| 5 |
let expPotential = [];
|
| 6 |
let expCurrent = [];
|
| 7 |
let latestResults = null;
|
| 8 |
let stagedFileContent = null;
|
| 9 |
-
let stagedFileName = "No file
|
| 10 |
let detectedColumns = [];
|
| 11 |
-
let isBackendAvailable = true;
|
| 12 |
-
|
| 13 |
-
// Default endpoints
|
| 14 |
-
const DEFAULT_LOCAL_URL = "http://127.0.0.1:8000";
|
| 15 |
-
const DEFAULT_HF_SPACE_URL = "https://rodrigo1421-cv-curve-fitting.hf.space";
|
| 16 |
-
|
| 17 |
-
function getStoredBackendUrl() {
|
| 18 |
-
return localStorage.getItem('cv_backend_url') || "";
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
function resolveEndpoints(rawUrl) {
|
| 22 |
-
let url = (rawUrl || "").trim().replace(/\/+$/, "");
|
| 23 |
-
if (!url) {
|
| 24 |
-
if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
|
| 25 |
-
url = window.location.port ? `${window.location.protocol}//${window.location.host}` : DEFAULT_LOCAL_URL;
|
| 26 |
-
} else if (window.location.hostname.endsWith(".hf.space") || window.location.hostname.includes("huggingface.co")) {
|
| 27 |
-
url = `${window.location.protocol}//${window.location.host}`;
|
| 28 |
-
} else {
|
| 29 |
-
url = DEFAULT_HF_SPACE_URL;
|
| 30 |
-
}
|
| 31 |
-
}
|
| 32 |
-
return {
|
| 33 |
-
rawUrl: url,
|
| 34 |
-
httpHealth: url + "/health",
|
| 35 |
-
httpSolve: url + "/api/solve"
|
| 36 |
-
};
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
let currentEndpoints = resolveEndpoints(getStoredBackendUrl());
|
| 40 |
-
|
| 41 |
-
// Probing Python Backend Engine
|
| 42 |
-
async function probePythonBackend() {
|
| 43 |
-
const dot = document.getElementById('engine-dot');
|
| 44 |
-
const label = document.getElementById('engine-label');
|
| 45 |
-
const msg = document.getElementById('engine-status-msg');
|
| 46 |
-
|
| 47 |
-
if (dot) dot.className = 'status-dot online';
|
| 48 |
-
if (label) label.innerText = 'ZeroGPU A100 Active (100% Free)';
|
| 49 |
-
if (msg) msg.innerHTML = `<span style="color: #10b981; font-weight: 500;">✓ Connected to NVIDIA A100 (ZeroGPU)</span> • JAX Auto-Diff Engine Ready ($0.00 / Free)`;
|
| 50 |
-
}
|
| 51 |
|
| 52 |
// Global Modal Handler
|
| 53 |
window.toggleModal = function(modalId, show) {
|
|
@@ -75,31 +35,6 @@ window.toggleAdvanced = function() {
|
|
| 75 |
}
|
| 76 |
};
|
| 77 |
|
| 78 |
-
// Global Backend URL Configuration
|
| 79 |
-
window.saveBackendUrl = function() {
|
| 80 |
-
const urlInput = document.getElementById('backend-url-input');
|
| 81 |
-
if (urlInput) {
|
| 82 |
-
const val = urlInput.value.trim();
|
| 83 |
-
localStorage.setItem('cv_backend_url', val);
|
| 84 |
-
currentEndpoints = resolveEndpoints(val);
|
| 85 |
-
probePythonBackend();
|
| 86 |
-
}
|
| 87 |
-
};
|
| 88 |
-
|
| 89 |
-
window.setBackendPreset = function(presetType) {
|
| 90 |
-
const urlInput = document.getElementById('backend-url-input');
|
| 91 |
-
if (!urlInput) return;
|
| 92 |
-
|
| 93 |
-
if (presetType === 'origin') {
|
| 94 |
-
urlInput.value = window.location.origin;
|
| 95 |
-
} else if (presetType === 'hf') {
|
| 96 |
-
urlInput.value = DEFAULT_HF_SPACE_URL;
|
| 97 |
-
} else if (presetType === 'local') {
|
| 98 |
-
urlInput.value = DEFAULT_LOCAL_URL;
|
| 99 |
-
}
|
| 100 |
-
window.saveBackendUrl();
|
| 101 |
-
};
|
| 102 |
-
|
| 103 |
// Delimiter Detection
|
| 104 |
function detectDelimiter(line) {
|
| 105 |
const commas = (line.match(/,/g) || []).length;
|
|
@@ -110,7 +45,7 @@ function detectDelimiter(line) {
|
|
| 110 |
return ',';
|
| 111 |
}
|
| 112 |
|
| 113 |
-
//
|
| 114 |
function analyzeCSVAndPopulateColumns(content) {
|
| 115 |
const lines = content.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//'));
|
| 116 |
if (lines.length === 0) return;
|
|
@@ -141,28 +76,29 @@ function analyzeCSVAndPopulateColumns(content) {
|
|
| 141 |
}
|
| 142 |
}
|
| 143 |
|
|
|
|
|
|
|
|
|
|
| 144 |
for (let c = 0; c < colCount; c++) {
|
| 145 |
let rawHeader = hasHeader && firstLineFields[c] ? firstLineFields[c] : `Column ${c}`;
|
| 146 |
-
let
|
| 147 |
-
let isAdjusted = rawHeader.toLowerCase().includes('adjusted');
|
| 148 |
-
let isCurrent = rawHeader.toLowerCase().includes('current') || rawHeader.toLowerCase().includes('(a)');
|
| 149 |
-
let isPotential = rawHeader.toLowerCase().includes('potential') || rawHeader.toLowerCase().includes('(v)');
|
| 150 |
-
|
| 151 |
-
let typeStr = isPotential ? "Potential (V)" : (isCurrent ? "Current (A)" : rawHeader);
|
| 152 |
-
let adjStr = isAdjusted ? " [Adjusted]" : " [Raw]";
|
| 153 |
-
let cycleStr = colCount >= 8 ? `Cycle ${cycleNum}` : "";
|
| 154 |
let ptsStr = ` (${colCounts[c].toLocaleString()} pts)`;
|
| 155 |
-
|
| 156 |
-
let displayName = `${cycleStr ? cycleStr + ' ' : ''}${typeStr}${adjStr}${ptsStr}`;
|
| 157 |
-
if (!hasHeader) displayName = `Column ${c}${ptsStr}`;
|
| 158 |
|
| 159 |
detectedColumns.push({
|
| 160 |
index: c,
|
| 161 |
name: displayName,
|
| 162 |
-
rawName:
|
| 163 |
-
count: colCounts[c]
|
| 164 |
-
isAdjusted: isAdjusted
|
| 165 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
const potSelect = document.getElementById('pot_col');
|
|
@@ -186,59 +122,18 @@ function analyzeCSVAndPopulateColumns(content) {
|
|
| 186 |
curSelect.appendChild(optC);
|
| 187 |
});
|
| 188 |
|
| 189 |
-
let defaultPot = 0;
|
| 190 |
-
let defaultCur = colCount > 1 ? 1 : 0;
|
| 191 |
-
|
| 192 |
-
if (colCount >= 10) {
|
| 193 |
-
defaultPot = 8;
|
| 194 |
-
defaultCur = 9;
|
| 195 |
-
} else if (colCount >= 4) {
|
| 196 |
-
defaultPot = 0;
|
| 197 |
-
defaultCur = 1;
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
potSelect.value = defaultPot;
|
| 201 |
curSelect.value = defaultCur;
|
| 202 |
|
| 203 |
if (metaBar && metaText) {
|
| 204 |
metaBar.classList.add('visible');
|
| 205 |
-
metaText.innerHTML = `
|
| 206 |
}
|
| 207 |
|
| 208 |
-
updateCycleButtonsActiveState(defaultPot, defaultCur);
|
| 209 |
window.updateLivePreviewFromColumns();
|
| 210 |
}
|
| 211 |
}
|
| 212 |
|
| 213 |
-
function updateCycleButtonsActiveState(pot, cur) {
|
| 214 |
-
const buttons = document.querySelectorAll('.preset-pill-btn');
|
| 215 |
-
buttons.forEach(btn => {
|
| 216 |
-
const bPot = parseInt(btn.getAttribute('data-pot'), 10);
|
| 217 |
-
const bCur = parseInt(btn.getAttribute('data-cur'), 10);
|
| 218 |
-
if (bPot === pot && bCur === cur) {
|
| 219 |
-
btn.classList.add('active');
|
| 220 |
-
} else {
|
| 221 |
-
btn.classList.remove('active');
|
| 222 |
-
}
|
| 223 |
-
});
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
// Global Cycle Preset Click Handler
|
| 227 |
-
window.applyCyclePreset = function(pot, cur, skip, btn) {
|
| 228 |
-
const potSelect = document.getElementById('pot_col');
|
| 229 |
-
const curSelect = document.getElementById('cur_col');
|
| 230 |
-
const skipInput = document.getElementById('skip_factor');
|
| 231 |
-
if (skip && skipInput) {
|
| 232 |
-
skipInput.value = skip;
|
| 233 |
-
}
|
| 234 |
-
if (potSelect && curSelect) {
|
| 235 |
-
potSelect.value = pot;
|
| 236 |
-
curSelect.value = cur;
|
| 237 |
-
updateCycleButtonsActiveState(pot, cur);
|
| 238 |
-
window.updateLivePreviewFromColumns();
|
| 239 |
-
}
|
| 240 |
-
};
|
| 241 |
-
|
| 242 |
// Global Live Preview & Baseline Plotter
|
| 243 |
window.updateLivePreviewFromColumns = function() {
|
| 244 |
if (!stagedFileContent) return;
|
|
@@ -249,7 +144,6 @@ window.updateLivePreviewFromColumns = function() {
|
|
| 249 |
|
| 250 |
const potCol = parseInt(potSelect.value, 10);
|
| 251 |
const curCol = parseInt(curSelect.value, 10);
|
| 252 |
-
updateCycleButtonsActiveState(potCol, curCol);
|
| 253 |
|
| 254 |
const lines = stagedFileContent.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//'));
|
| 255 |
if (lines.length === 0) return;
|
|
@@ -294,8 +188,8 @@ window.updateLivePreviewFromColumns = function() {
|
|
| 294 |
const vMinInput = document.getElementById('v_min');
|
| 295 |
const vMaxInput = document.getElementById('v_max');
|
| 296 |
if (vMinInput && vMaxInput) {
|
| 297 |
-
vMinInput.value = vMin.toFixed(
|
| 298 |
-
vMaxInput.value = vMax.toFixed(
|
| 299 |
}
|
| 300 |
|
| 301 |
const vRangeSpan = document.getElementById('stat-v-range');
|
|
@@ -303,14 +197,14 @@ window.updateLivePreviewFromColumns = function() {
|
|
| 303 |
const ptsSpan = document.getElementById('stat-points-count');
|
| 304 |
const statsBox = document.getElementById('col-stats-preview');
|
| 305 |
|
| 306 |
-
if (vRangeSpan) vRangeSpan.innerText = `${vMin.toFixed(
|
| 307 |
-
if (iRangeSpan) iRangeSpan.innerText = `${iMin.toExponential(2)}A to ${iMax.toExponential(2)}A`;
|
| 308 |
if (ptsSpan) ptsSpan.innerText = `${previewPot.length.toLocaleString()}`;
|
| 309 |
if (statsBox) statsBox.classList.add('visible');
|
| 310 |
|
| 311 |
const statusDetails = document.getElementById('status-details');
|
| 312 |
if (statusDetails) {
|
| 313 |
-
statusDetails.innerHTML = `Loaded <strong>${stagedFileName}</strong> • Col ${potCol}
|
| 314 |
}
|
| 315 |
|
| 316 |
renderInitialExpPlot(previewPot, previewCur);
|
|
@@ -388,7 +282,7 @@ window.handleFormSubmit = async function(e) {
|
|
| 388 |
if (e && e.preventDefault) e.preventDefault();
|
| 389 |
|
| 390 |
if (!stagedFileContent) {
|
| 391 |
-
alert('Please select and upload a CSV data file first.');
|
| 392 |
return false;
|
| 393 |
}
|
| 394 |
|
|
@@ -411,8 +305,8 @@ window.handleFormSubmit = async function(e) {
|
|
| 411 |
async function executeZeroGPUSolver(fileContent, config) {
|
| 412 |
const stageEl = document.getElementById('status-stage');
|
| 413 |
const detailsEl = document.getElementById('status-details');
|
| 414 |
-
if (stageEl) stageEl.innerText = '⚡
|
| 415 |
-
if (detailsEl) detailsEl.innerText = '
|
| 416 |
|
| 417 |
// 1. Native Gradio ZeroGPU Queue Trigger
|
| 418 |
const fileSet = setGradioInputValue('#gr_input_file', fileContent);
|
|
@@ -421,7 +315,6 @@ async function executeZeroGPUSolver(fileContent, config) {
|
|
| 421 |
|
| 422 |
if (fileSet && configSet && grBtn) {
|
| 423 |
const startTime = Date.now();
|
| 424 |
-
// Clear previous output
|
| 425 |
setGradioInputValue('#gr_output_json', '');
|
| 426 |
|
| 427 |
const pollInterval = setInterval(() => {
|
|
@@ -441,24 +334,24 @@ async function executeZeroGPUSolver(fileContent, config) {
|
|
| 441 |
}
|
| 442 |
|
| 443 |
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
| 444 |
-
if (stageEl) stageEl.innerText = `⚡
|
| 445 |
-
if (detailsEl) detailsEl.innerText = `
|
| 446 |
|
| 447 |
if (Date.now() - startTime > 180000) {
|
| 448 |
clearInterval(pollInterval);
|
| 449 |
handleSolverError("Optimization calculation timed out (3 min).");
|
| 450 |
}
|
| 451 |
-
},
|
| 452 |
|
| 453 |
grBtn.click();
|
| 454 |
return;
|
| 455 |
}
|
| 456 |
|
| 457 |
-
// 2. HTTP POST fallback
|
| 458 |
const endpoints = [
|
| 459 |
window.location.origin + "/api/solve",
|
| 460 |
window.location.origin + "/solve",
|
| 461 |
-
|
| 462 |
];
|
| 463 |
|
| 464 |
for (const endpoint of endpoints) {
|
|
@@ -486,7 +379,7 @@ async function executeZeroGPUSolver(fileContent, config) {
|
|
| 486 |
}
|
| 487 |
}
|
| 488 |
|
| 489 |
-
handleSolverError("Could not
|
| 490 |
}
|
| 491 |
|
| 492 |
function handleSolverMessage(data) {
|
|
@@ -494,14 +387,14 @@ function handleSolverMessage(data) {
|
|
| 494 |
const detailsEl = document.getElementById('status-details');
|
| 495 |
|
| 496 |
if (data.type === 'done') {
|
| 497 |
-
if (stageEl) stageEl.innerText = '✓
|
| 498 |
-
if (detailsEl) detailsEl.innerText = `
|
| 499 |
|
| 500 |
stopOptimizationUI();
|
| 501 |
latestResults = data;
|
| 502 |
displayExtractedResults(data);
|
| 503 |
|
| 504 |
-
// Update
|
| 505 |
if (data.plots && data.plots.sim_current && window.Plotly) {
|
| 506 |
updateLivePlotProgress({
|
| 507 |
potential: data.plots.exp_potential,
|
|
@@ -516,10 +409,10 @@ function handleSolverMessage(data) {
|
|
| 516 |
function handleSolverError(msg) {
|
| 517 |
const stageEl = document.getElementById('status-stage');
|
| 518 |
const detailsEl = document.getElementById('status-details');
|
| 519 |
-
if (stageEl) stageEl.innerText = '❌
|
| 520 |
if (detailsEl) detailsEl.innerText = msg;
|
| 521 |
stopOptimizationUI();
|
| 522 |
-
alert(`Solver: ${msg}`);
|
| 523 |
}
|
| 524 |
|
| 525 |
function startOptimizationUI() {
|
|
@@ -528,7 +421,7 @@ function startOptimizationUI() {
|
|
| 528 |
if (spinner) spinner.classList.remove('hidden');
|
| 529 |
if (submitBtn) {
|
| 530 |
submitBtn.disabled = true;
|
| 531 |
-
submitBtn.innerText = '
|
| 532 |
}
|
| 533 |
}
|
| 534 |
|
|
@@ -542,21 +435,24 @@ function stopOptimizationUI() {
|
|
| 542 |
}
|
| 543 |
}
|
| 544 |
|
| 545 |
-
// Plotly
|
| 546 |
const layoutConfig = {
|
| 547 |
paper_bgcolor: 'transparent',
|
| 548 |
plot_bgcolor: 'transparent',
|
| 549 |
-
font: { family: '
|
| 550 |
-
margin: { l:
|
| 551 |
xaxis: {
|
| 552 |
-
gridcolor: 'rgba(255, 255, 255, 0.
|
| 553 |
-
zerolinecolor: 'rgba(255, 255, 255, 0.
|
| 554 |
-
tickfont: { color: '#94a3b8' }
|
|
|
|
| 555 |
},
|
| 556 |
yaxis: {
|
| 557 |
-
gridcolor: 'rgba(255, 255, 255, 0.
|
| 558 |
-
zerolinecolor: 'rgba(255, 255, 255, 0.
|
| 559 |
-
tickfont: { color: '#94a3b8' }
|
|
|
|
|
|
|
| 560 |
}
|
| 561 |
};
|
| 562 |
|
|
@@ -567,19 +463,19 @@ function renderInitialExpPlot(pot, cur) {
|
|
| 567 |
y: cur,
|
| 568 |
mode: 'lines',
|
| 569 |
type: 'scatter',
|
| 570 |
-
name: 'Experimental
|
| 571 |
-
line: { color: '#38bdf8', width: 2 }
|
| 572 |
};
|
| 573 |
|
| 574 |
const layout = Object.assign({}, layoutConfig, {
|
| 575 |
-
title: { text: `Cyclic Voltammogram
|
| 576 |
-
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Applied Potential (V)' }),
|
| 577 |
-
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Current (A)' }),
|
| 578 |
showlegend: true,
|
| 579 |
-
legend: { x: 0.02, y: 0.98, bgcolor: 'rgba(15, 23, 42, 0.
|
| 580 |
});
|
| 581 |
|
| 582 |
-
Plotly.react('live-chart', [traceExp], layout, { responsive: true });
|
| 583 |
}
|
| 584 |
|
| 585 |
function updateLivePlotProgress(currentFit) {
|
|
@@ -590,7 +486,7 @@ function updateLivePlotProgress(currentFit) {
|
|
| 590 |
mode: 'lines',
|
| 591 |
type: 'scatter',
|
| 592 |
name: 'Experimental Data',
|
| 593 |
-
line: { color: '#38bdf8', width: 2 }
|
| 594 |
};
|
| 595 |
|
| 596 |
const traceSim = {
|
|
@@ -598,19 +494,19 @@ function updateLivePlotProgress(currentFit) {
|
|
| 598 |
y: currentFit.current,
|
| 599 |
mode: 'lines',
|
| 600 |
type: 'scatter',
|
| 601 |
-
name: '
|
| 602 |
-
line: { color: '#f43f5e', width: 2.
|
| 603 |
};
|
| 604 |
|
| 605 |
const layout = Object.assign({}, layoutConfig, {
|
| 606 |
-
title: { text: '
|
| 607 |
-
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Potential (V)' }),
|
| 608 |
-
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Current (A)' }),
|
| 609 |
showlegend: true,
|
| 610 |
-
legend: { x: 0.02, y: 0.98, bgcolor: 'rgba(15, 23, 42, 0.
|
| 611 |
});
|
| 612 |
|
| 613 |
-
Plotly.react('live-chart', [traceExp, traceSim], layout, { responsive: true });
|
| 614 |
}
|
| 615 |
|
| 616 |
function displayExtractedResults(results) {
|
|
@@ -623,12 +519,12 @@ function displayExtractedResults(results) {
|
|
| 623 |
const params = results.params || {};
|
| 624 |
|
| 625 |
const cards = [
|
| 626 |
-
{ label: '
|
| 627 |
-
{ label: '
|
| 628 |
-
{ label: 'Asymmetry Left (β_L)', value: `${(params.beta_L || 0).toFixed(4)} V⁻²` },
|
| 629 |
-
{ label: 'Asymmetry Right (β_R)', value: `${(params.beta_R || 0).toFixed(4)} V⁻²` },
|
| 630 |
-
{ label: 'DC
|
| 631 |
-
{ label: '
|
| 632 |
];
|
| 633 |
|
| 634 |
cards.forEach(c => {
|
|
@@ -660,7 +556,8 @@ function renderSecondaryPlots(plots) {
|
|
| 660 |
mode: 'lines',
|
| 661 |
type: 'scatter',
|
| 662 |
name: `Sub-band ${i+1}`,
|
| 663 |
-
line: { width: 1, dash: 'dot' }
|
|
|
|
| 664 |
});
|
| 665 |
});
|
| 666 |
}
|
|
@@ -675,13 +572,13 @@ function renderSecondaryPlots(plots) {
|
|
| 675 |
});
|
| 676 |
|
| 677 |
const dosLayout = Object.assign({}, layoutConfig, {
|
| 678 |
-
title: { text: 'Extracted Density of States DOS(V)', font: { size: 14 } },
|
| 679 |
-
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Potential (V)', autorange: true }),
|
| 680 |
-
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'DOS (a.u.)', autorange: true }),
|
| 681 |
showlegend: false
|
| 682 |
});
|
| 683 |
|
| 684 |
-
Plotly.react('dos-chart', dosTraces, dosLayout, { responsive: true });
|
| 685 |
|
| 686 |
// Diffusivity D(V) Plot
|
| 687 |
const traceDiff = {
|
|
@@ -694,20 +591,20 @@ function renderSecondaryPlots(plots) {
|
|
| 694 |
};
|
| 695 |
|
| 696 |
const diffLayout = Object.assign({}, layoutConfig, {
|
| 697 |
-
title: { text: 'Voltage-Dependent Diffusivity D(V)', font: { size: 14 } },
|
| 698 |
-
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Potential (V)', autorange: true }),
|
| 699 |
-
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Diffusivity (cm²/s)', type: 'log', autorange: true }),
|
| 700 |
showlegend: false
|
| 701 |
});
|
| 702 |
|
| 703 |
-
Plotly.react('diffusivity-chart', [traceDiff], diffLayout, { responsive: true });
|
| 704 |
}
|
| 705 |
|
| 706 |
// Global Export Functions
|
| 707 |
window.exportResultsJson = function() {
|
| 708 |
if (!latestResults) return;
|
| 709 |
const jsonStr = JSON.stringify(latestResults, null, 2);
|
| 710 |
-
downloadFile(jsonStr, '
|
| 711 |
};
|
| 712 |
|
| 713 |
window.exportResultsCsv = function() {
|
|
@@ -726,7 +623,7 @@ window.exportResultsCsv = function() {
|
|
| 726 |
rows.push(`${i},${pot},${expCur},${simCur},${vp},${dv},${dos}`);
|
| 727 |
}
|
| 728 |
|
| 729 |
-
downloadFile(rows.join("\n"), '
|
| 730 |
};
|
| 731 |
|
| 732 |
function downloadFile(content, fileName, contentType) {
|
|
@@ -744,13 +641,24 @@ function downloadFile(content, fileName, contentType) {
|
|
| 744 |
|
| 745 |
// Master Initialization Function
|
| 746 |
window.__initCVApp = function() {
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
if (urlInput && !urlInput.value) {
|
| 750 |
-
urlInput.value = getStoredBackendUrl() || currentEndpoints.rawUrl;
|
| 751 |
}
|
| 752 |
};
|
| 753 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
// Run initialization immediately and on DOM load
|
| 755 |
if (typeof document !== 'undefined') {
|
| 756 |
if (document.readyState === 'loading') {
|
|
|
|
| 1 |
+
// Cyclic Voltammetry Parameter Extraction & Physical Model Fitting
|
| 2 |
+
// High-Performance JAX Auto-Diff Engine Client
|
| 3 |
|
| 4 |
// Global State
|
| 5 |
let expPotential = [];
|
| 6 |
let expCurrent = [];
|
| 7 |
let latestResults = null;
|
| 8 |
let stagedFileContent = null;
|
| 9 |
+
let stagedFileName = "No file selected";
|
| 10 |
let detectedColumns = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
// Global Modal Handler
|
| 13 |
window.toggleModal = function(modalId, show) {
|
|
|
|
| 35 |
}
|
| 36 |
};
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
// Delimiter Detection
|
| 39 |
function detectDelimiter(line) {
|
| 40 |
const commas = (line.match(/,/g) || []).length;
|
|
|
|
| 45 |
return ',';
|
| 46 |
}
|
| 47 |
|
| 48 |
+
// Generalized 2-Column CSV Analysis & Dropdown Populator
|
| 49 |
function analyzeCSVAndPopulateColumns(content) {
|
| 50 |
const lines = content.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//'));
|
| 51 |
if (lines.length === 0) return;
|
|
|
|
| 76 |
}
|
| 77 |
}
|
| 78 |
|
| 79 |
+
let defaultPot = 0;
|
| 80 |
+
let defaultCur = colCount > 1 ? 1 : 0;
|
| 81 |
+
|
| 82 |
for (let c = 0; c < colCount; c++) {
|
| 83 |
let rawHeader = hasHeader && firstLineFields[c] ? firstLineFields[c] : `Column ${c}`;
|
| 84 |
+
let cleanHeader = rawHeader.replace(/["']/g, '');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
let ptsStr = ` (${colCounts[c].toLocaleString()} pts)`;
|
| 86 |
+
let displayName = `${cleanHeader}${ptsStr}`;
|
|
|
|
|
|
|
| 87 |
|
| 88 |
detectedColumns.push({
|
| 89 |
index: c,
|
| 90 |
name: displayName,
|
| 91 |
+
rawName: cleanHeader,
|
| 92 |
+
count: colCounts[c]
|
|
|
|
| 93 |
});
|
| 94 |
+
|
| 95 |
+
// Smart column auto-detection based on header text
|
| 96 |
+
const lower = cleanHeader.toLowerCase();
|
| 97 |
+
if (lower.includes('potential') || lower.includes('volt') || lower === 'v' || lower.includes('(v)')) {
|
| 98 |
+
defaultPot = c;
|
| 99 |
+
} else if (lower.includes('current') || lower.includes('curr') || lower === 'i' || lower.includes('(a)') || lower.includes('amp')) {
|
| 100 |
+
defaultCur = c;
|
| 101 |
+
}
|
| 102 |
}
|
| 103 |
|
| 104 |
const potSelect = document.getElementById('pot_col');
|
|
|
|
| 122 |
curSelect.appendChild(optC);
|
| 123 |
});
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
potSelect.value = defaultPot;
|
| 126 |
curSelect.value = defaultCur;
|
| 127 |
|
| 128 |
if (metaBar && metaText) {
|
| 129 |
metaBar.classList.add('visible');
|
| 130 |
+
metaText.innerHTML = `Loaded <strong>${colCount} column${colCount > 1 ? 's' : ''}</strong> • <strong>${lines.length - startRow} rows</strong>`;
|
| 131 |
}
|
| 132 |
|
|
|
|
| 133 |
window.updateLivePreviewFromColumns();
|
| 134 |
}
|
| 135 |
}
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
// Global Live Preview & Baseline Plotter
|
| 138 |
window.updateLivePreviewFromColumns = function() {
|
| 139 |
if (!stagedFileContent) return;
|
|
|
|
| 144 |
|
| 145 |
const potCol = parseInt(potSelect.value, 10);
|
| 146 |
const curCol = parseInt(curSelect.value, 10);
|
|
|
|
| 147 |
|
| 148 |
const lines = stagedFileContent.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//'));
|
| 149 |
if (lines.length === 0) return;
|
|
|
|
| 188 |
const vMinInput = document.getElementById('v_min');
|
| 189 |
const vMaxInput = document.getElementById('v_max');
|
| 190 |
if (vMinInput && vMaxInput) {
|
| 191 |
+
vMinInput.value = vMin.toFixed(3);
|
| 192 |
+
vMaxInput.value = vMax.toFixed(3);
|
| 193 |
}
|
| 194 |
|
| 195 |
const vRangeSpan = document.getElementById('stat-v-range');
|
|
|
|
| 197 |
const ptsSpan = document.getElementById('stat-points-count');
|
| 198 |
const statsBox = document.getElementById('col-stats-preview');
|
| 199 |
|
| 200 |
+
if (vRangeSpan) vRangeSpan.innerText = `${vMin.toFixed(3)} V to ${vMax.toFixed(3)} V`;
|
| 201 |
+
if (iRangeSpan) iRangeSpan.innerText = `${iMin.toExponential(2)} A to ${iMax.toExponential(2)} A`;
|
| 202 |
if (ptsSpan) ptsSpan.innerText = `${previewPot.length.toLocaleString()}`;
|
| 203 |
if (statsBox) statsBox.classList.add('visible');
|
| 204 |
|
| 205 |
const statusDetails = document.getElementById('status-details');
|
| 206 |
if (statusDetails) {
|
| 207 |
+
statusDetails.innerHTML = `Loaded <strong>${stagedFileName}</strong> • Potential (Col ${potCol}) & Current (Col ${curCol}) • ${previewPot.length.toLocaleString()} points ready for optimization.`;
|
| 208 |
}
|
| 209 |
|
| 210 |
renderInitialExpPlot(previewPot, previewCur);
|
|
|
|
| 282 |
if (e && e.preventDefault) e.preventDefault();
|
| 283 |
|
| 284 |
if (!stagedFileContent) {
|
| 285 |
+
alert('Please select and upload a cyclic voltammetry CSV data file first.');
|
| 286 |
return false;
|
| 287 |
}
|
| 288 |
|
|
|
|
| 305 |
async function executeZeroGPUSolver(fileContent, config) {
|
| 306 |
const stageEl = document.getElementById('status-stage');
|
| 307 |
const detailsEl = document.getElementById('status-details');
|
| 308 |
+
if (stageEl) stageEl.innerText = '⚡ Optimizing Physical Model Parameters...';
|
| 309 |
+
if (detailsEl) detailsEl.innerText = 'Executing multi-stage non-linear L-BFGS-B optimization on JAX auto-diff engine...';
|
| 310 |
|
| 311 |
// 1. Native Gradio ZeroGPU Queue Trigger
|
| 312 |
const fileSet = setGradioInputValue('#gr_input_file', fileContent);
|
|
|
|
| 315 |
|
| 316 |
if (fileSet && configSet && grBtn) {
|
| 317 |
const startTime = Date.now();
|
|
|
|
| 318 |
setGradioInputValue('#gr_output_json', '');
|
| 319 |
|
| 320 |
const pollInterval = setInterval(() => {
|
|
|
|
| 334 |
}
|
| 335 |
|
| 336 |
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
| 337 |
+
if (stageEl) stageEl.innerText = `⚡ Non-Linear Parameter Extraction (${elapsedSec}s)...`;
|
| 338 |
+
if (detailsEl) detailsEl.innerText = `Solving 1D diffusion PDE and optimizing Fermi-Dirac DOS sub-bands...`;
|
| 339 |
|
| 340 |
if (Date.now() - startTime > 180000) {
|
| 341 |
clearInterval(pollInterval);
|
| 342 |
handleSolverError("Optimization calculation timed out (3 min).");
|
| 343 |
}
|
| 344 |
+
}, 500);
|
| 345 |
|
| 346 |
grBtn.click();
|
| 347 |
return;
|
| 348 |
}
|
| 349 |
|
| 350 |
+
// 2. Direct HTTP POST fallback
|
| 351 |
const endpoints = [
|
| 352 |
window.location.origin + "/api/solve",
|
| 353 |
window.location.origin + "/solve",
|
| 354 |
+
"http://127.0.0.1:8000/api/solve"
|
| 355 |
];
|
| 356 |
|
| 357 |
for (const endpoint of endpoints) {
|
|
|
|
| 379 |
}
|
| 380 |
}
|
| 381 |
|
| 382 |
+
handleSolverError("Could not communicate with solver engine. Please verify the space is running.");
|
| 383 |
}
|
| 384 |
|
| 385 |
function handleSolverMessage(data) {
|
|
|
|
| 387 |
const detailsEl = document.getElementById('status-details');
|
| 388 |
|
| 389 |
if (data.type === 'done') {
|
| 390 |
+
if (stageEl) stageEl.innerText = '✓ Physical Model Parameters Successfully Extracted';
|
| 391 |
+
if (detailsEl) detailsEl.innerText = `Optimization converged in ${data.total_iterations || 100} iterations. Model fit overlay and diagnostic spectra rendered below.`;
|
| 392 |
|
| 393 |
stopOptimizationUI();
|
| 394 |
latestResults = data;
|
| 395 |
displayExtractedResults(data);
|
| 396 |
|
| 397 |
+
// Update primary plot with simulation overlay
|
| 398 |
if (data.plots && data.plots.sim_current && window.Plotly) {
|
| 399 |
updateLivePlotProgress({
|
| 400 |
potential: data.plots.exp_potential,
|
|
|
|
| 409 |
function handleSolverError(msg) {
|
| 410 |
const stageEl = document.getElementById('status-stage');
|
| 411 |
const detailsEl = document.getElementById('status-details');
|
| 412 |
+
if (stageEl) stageEl.innerText = '❌ Calculation Notice';
|
| 413 |
if (detailsEl) detailsEl.innerText = msg;
|
| 414 |
stopOptimizationUI();
|
| 415 |
+
alert(`Solver Message: ${msg}`);
|
| 416 |
}
|
| 417 |
|
| 418 |
function startOptimizationUI() {
|
|
|
|
| 421 |
if (spinner) spinner.classList.remove('hidden');
|
| 422 |
if (submitBtn) {
|
| 423 |
submitBtn.disabled = true;
|
| 424 |
+
submitBtn.innerText = 'Extracting Parameters...';
|
| 425 |
}
|
| 426 |
}
|
| 427 |
|
|
|
|
| 435 |
}
|
| 436 |
}
|
| 437 |
|
| 438 |
+
// Scientific Academic Plotly Layout Configuration
|
| 439 |
const layoutConfig = {
|
| 440 |
paper_bgcolor: 'transparent',
|
| 441 |
plot_bgcolor: 'transparent',
|
| 442 |
+
font: { family: 'Inter, -apple-system, sans-serif', color: '#94a3b8', size: 12 },
|
| 443 |
+
margin: { l: 75, r: 35, t: 40, b: 55 },
|
| 444 |
xaxis: {
|
| 445 |
+
gridcolor: 'rgba(255, 255, 255, 0.07)',
|
| 446 |
+
zerolinecolor: 'rgba(255, 255, 255, 0.15)',
|
| 447 |
+
tickfont: { color: '#94a3b8', size: 11 },
|
| 448 |
+
titlefont: { color: '#f1f5f9', size: 13 }
|
| 449 |
},
|
| 450 |
yaxis: {
|
| 451 |
+
gridcolor: 'rgba(255, 255, 255, 0.07)',
|
| 452 |
+
zerolinecolor: 'rgba(255, 255, 255, 0.15)',
|
| 453 |
+
tickfont: { color: '#94a3b8', size: 11 },
|
| 454 |
+
titlefont: { color: '#f1f5f9', size: 13 },
|
| 455 |
+
tickformat: '.2e'
|
| 456 |
}
|
| 457 |
};
|
| 458 |
|
|
|
|
| 463 |
y: cur,
|
| 464 |
mode: 'lines',
|
| 465 |
type: 'scatter',
|
| 466 |
+
name: 'Experimental Voltammogram',
|
| 467 |
+
line: { color: '#38bdf8', width: 2.2 }
|
| 468 |
};
|
| 469 |
|
| 470 |
const layout = Object.assign({}, layoutConfig, {
|
| 471 |
+
title: { text: `Cyclic Voltammogram (${stagedFileName})`, font: { color: '#ffffff', size: 14 } },
|
| 472 |
+
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Applied Potential <i>V</i> (V vs. Ref)' }),
|
| 473 |
+
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Current <i>I</i> (A)' }),
|
| 474 |
showlegend: true,
|
| 475 |
+
legend: { x: 0.02, y: 0.98, bgcolor: 'rgba(15, 23, 42, 0.8)', font: { color: '#f1f5f9' } }
|
| 476 |
});
|
| 477 |
|
| 478 |
+
Plotly.react('live-chart', [traceExp], layout, { responsive: true, displaylogo: false });
|
| 479 |
}
|
| 480 |
|
| 481 |
function updateLivePlotProgress(currentFit) {
|
|
|
|
| 486 |
mode: 'lines',
|
| 487 |
type: 'scatter',
|
| 488 |
name: 'Experimental Data',
|
| 489 |
+
line: { color: '#38bdf8', width: 2.2 }
|
| 490 |
};
|
| 491 |
|
| 492 |
const traceSim = {
|
|
|
|
| 494 |
y: currentFit.current,
|
| 495 |
mode: 'lines',
|
| 496 |
type: 'scatter',
|
| 497 |
+
name: 'Fitted Physical Model',
|
| 498 |
+
line: { color: '#f43f5e', width: 2.6 }
|
| 499 |
};
|
| 500 |
|
| 501 |
const layout = Object.assign({}, layoutConfig, {
|
| 502 |
+
title: { text: 'Experimental vs. Fitted Cyclic Voltammogram Overlay', font: { color: '#ffffff', size: 14 } },
|
| 503 |
+
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Applied Potential <i>V</i> (V vs. Ref)' }),
|
| 504 |
+
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Current <i>I</i> (A)' }),
|
| 505 |
showlegend: true,
|
| 506 |
+
legend: { x: 0.02, y: 0.98, bgcolor: 'rgba(15, 23, 42, 0.8)', font: { color: '#f1f5f9' } }
|
| 507 |
});
|
| 508 |
|
| 509 |
+
Plotly.react('live-chart', [traceExp, traceSim], layout, { responsive: true, displaylogo: false });
|
| 510 |
}
|
| 511 |
|
| 512 |
function displayExtractedResults(results) {
|
|
|
|
| 519 |
const params = results.params || {};
|
| 520 |
|
| 521 |
const cards = [
|
| 522 |
+
{ label: 'Diffusivity Constant (D₀)', value: `${(params.D0 || 0).toExponential(3)} cm²/s` },
|
| 523 |
+
{ label: 'Thermodynamic Potential (V_c)', value: `${(params.Vc || 0).toFixed(4)} V` },
|
| 524 |
+
{ label: 'Asymmetry Factor Left (β_L)', value: `${(params.beta_L || 0).toFixed(4)} V⁻²` },
|
| 525 |
+
{ label: 'Asymmetry Factor Right (β_R)', value: `${(params.beta_R || 0).toFixed(4)} V⁻²` },
|
| 526 |
+
{ label: 'Baseline DC Offset (I_offset)', value: `${(params.I_offset || 0).toExponential(3)} A` },
|
| 527 |
+
{ label: 'Objective Loss (L_final)', value: results.final_loss ? results.final_loss.toExponential(4) : 'Converged' }
|
| 528 |
];
|
| 529 |
|
| 530 |
cards.forEach(c => {
|
|
|
|
| 556 |
mode: 'lines',
|
| 557 |
type: 'scatter',
|
| 558 |
name: `Sub-band ${i+1}`,
|
| 559 |
+
line: { width: 1, dash: 'dot', color: 'rgba(56, 189, 248, 0.35)' },
|
| 560 |
+
showlegend: false
|
| 561 |
});
|
| 562 |
});
|
| 563 |
}
|
|
|
|
| 572 |
});
|
| 573 |
|
| 574 |
const dosLayout = Object.assign({}, layoutConfig, {
|
| 575 |
+
title: { text: 'Extracted Density of States DOS(V)', font: { color: '#ffffff', size: 14 } },
|
| 576 |
+
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Potential <i>V</i> (V vs. Ref)', autorange: true }),
|
| 577 |
+
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'DOS (a.u.)', autorange: true, tickformat: '.2e' }),
|
| 578 |
showlegend: false
|
| 579 |
});
|
| 580 |
|
| 581 |
+
Plotly.react('dos-chart', dosTraces, dosLayout, { responsive: true, displaylogo: false });
|
| 582 |
|
| 583 |
// Diffusivity D(V) Plot
|
| 584 |
const traceDiff = {
|
|
|
|
| 591 |
};
|
| 592 |
|
| 593 |
const diffLayout = Object.assign({}, layoutConfig, {
|
| 594 |
+
title: { text: 'Voltage-Dependent Diffusivity Profile D(V)', font: { color: '#ffffff', size: 14 } },
|
| 595 |
+
xaxis: Object.assign({}, layoutConfig.xaxis, { title: 'Potential <i>V</i> (V vs. Ref)', autorange: true }),
|
| 596 |
+
yaxis: Object.assign({}, layoutConfig.yaxis, { title: 'Diffusivity <i>D</i> (cm²/s)', type: 'log', autorange: true, tickformat: '.1e' }),
|
| 597 |
showlegend: false
|
| 598 |
});
|
| 599 |
|
| 600 |
+
Plotly.react('diffusivity-chart', [traceDiff], diffLayout, { responsive: true, displaylogo: false });
|
| 601 |
}
|
| 602 |
|
| 603 |
// Global Export Functions
|
| 604 |
window.exportResultsJson = function() {
|
| 605 |
if (!latestResults) return;
|
| 606 |
const jsonStr = JSON.stringify(latestResults, null, 2);
|
| 607 |
+
downloadFile(jsonStr, 'cv_extracted_parameters.json', 'application/json');
|
| 608 |
};
|
| 609 |
|
| 610 |
window.exportResultsCsv = function() {
|
|
|
|
| 623 |
rows.push(`${i},${pot},${expCur},${simCur},${vp},${dv},${dos}`);
|
| 624 |
}
|
| 625 |
|
| 626 |
+
downloadFile(rows.join("\n"), 'cv_extracted_curves.csv', 'text/csv');
|
| 627 |
};
|
| 628 |
|
| 629 |
function downloadFile(content, fileName, contentType) {
|
|
|
|
| 641 |
|
| 642 |
// Master Initialization Function
|
| 643 |
window.__initCVApp = function() {
|
| 644 |
+
if (window.Plotly && expPotential.length > 0) {
|
| 645 |
+
Plotly.Plots.resize('live-chart');
|
|
|
|
|
|
|
| 646 |
}
|
| 647 |
};
|
| 648 |
|
| 649 |
+
// Window resize observer to keep Plotly charts perfectly proportioned
|
| 650 |
+
window.addEventListener('resize', () => {
|
| 651 |
+
if (window.Plotly) {
|
| 652 |
+
const chartIds = ['live-chart', 'dos-chart', 'diffusivity-chart'];
|
| 653 |
+
chartIds.forEach(id => {
|
| 654 |
+
const el = document.getElementById(id);
|
| 655 |
+
if (el && el.data) {
|
| 656 |
+
Plotly.Plots.resize(id);
|
| 657 |
+
}
|
| 658 |
+
});
|
| 659 |
+
}
|
| 660 |
+
});
|
| 661 |
+
|
| 662 |
// Run initialization immediately and on DOM load
|
| 663 |
if (typeof document !== 'undefined') {
|
| 664 |
if (document.readyState === 'loading') {
|