Rodrigo1421 commited on
Commit
da0d346
·
verified ·
1 Parent(s): 5445b59

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +100 -192
app.js CHANGED
@@ -1,53 +1,13 @@
1
- // CV Curve Fitting Pro - Pure Python JAX + SciPy Engine Client
2
- // Connected to Python ZeroGPU Engine (Free ZeroGPU & CPU Ready)
3
 
4
  // Global State
5
  let expPotential = [];
6
  let expCurrent = [];
7
  let latestResults = null;
8
  let stagedFileContent = null;
9
- let stagedFileName = "No file chosen";
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> &bull; 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
- // Robust CSV Column Analysis & Dropdown Populator
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 cycleNum = Math.floor(c / 4) + 1;
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: rawHeader,
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 = ` Detected <strong>${colCount} columns</strong> &bull; Total <strong>${lines.length - startRow} rows</strong>`;
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(2);
298
- vMaxInput.value = vMax.toFixed(2);
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(2)}V to ${vMax.toFixed(2)}V`;
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> &bull; Col ${potCol} (V) &amp; Col ${curCol} (I) &bull; ${previewPot.length} points ready for JAX optimization.`;
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 = '⚡ ZeroGPU A100 Optimizing...';
415
- if (detailsEl) detailsEl.innerText = 'Allocating NVIDIA A100 GPU • Multi-stage JAX L-BFGS-B optimization running...';
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 = `⚡ ZeroGPU JAX Engine (${elapsedSec}s)...`;
445
- if (detailsEl) detailsEl.innerText = `NVIDIA A100 GPU Active Running multi-stage physics optimization...`;
446
 
447
  if (Date.now() - startTime > 180000) {
448
  clearInterval(pollInterval);
449
  handleSolverError("Optimization calculation timed out (3 min).");
450
  }
451
- }, 600);
452
 
453
  grBtn.click();
454
  return;
455
  }
456
 
457
- // 2. HTTP POST fallback for direct standalone hosting
458
  const endpoints = [
459
  window.location.origin + "/api/solve",
460
  window.location.origin + "/solve",
461
- DEFAULT_LOCAL_URL + "/api/solve"
462
  ];
463
 
464
  for (const endpoint of endpoints) {
@@ -486,7 +379,7 @@ async function executeZeroGPUSolver(fileContent, config) {
486
  }
487
  }
488
 
489
- handleSolverError("Could not reach ZeroGPU backend server. Please verify your Space is running.");
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 = '✓ Optimization Successfully Converged';
498
- if (detailsEl) detailsEl.innerText = `Calculated on NVIDIA A100 GPU in ${data.total_iterations || 100} iterations.`;
499
 
500
  stopOptimizationUI();
501
  latestResults = data;
502
  displayExtractedResults(data);
503
 
504
- // Update main plot with final simulation overlay
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 = '❌ Optimization Notice';
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 = 'Optimizing in JAX...';
532
  }
533
  }
534
 
@@ -542,21 +435,24 @@ function stopOptimizationUI() {
542
  }
543
  }
544
 
545
- // Plotly Visualizations
546
  const layoutConfig = {
547
  paper_bgcolor: 'transparent',
548
  plot_bgcolor: 'transparent',
549
- font: { family: 'Roboto, sans-serif', color: '#64748b', size: 12 },
550
- margin: { l: 65, r: 25, t: 35, b: 50 },
551
  xaxis: {
552
- gridcolor: 'rgba(255, 255, 255, 0.05)',
553
- zerolinecolor: 'rgba(255, 255, 255, 0.1)',
554
- tickfont: { color: '#94a3b8' }
 
555
  },
556
  yaxis: {
557
- gridcolor: 'rgba(255, 255, 255, 0.05)',
558
- zerolinecolor: 'rgba(255, 255, 255, 0.1)',
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 CV',
571
- line: { color: '#38bdf8', width: 2 }
572
  };
573
 
574
  const layout = Object.assign({}, layoutConfig, {
575
- title: { text: `Cyclic Voltammogram: ${stagedFileName}`, font: { size: 14 } },
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.7)' }
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: 'JAX Model Fit',
602
- line: { color: '#f43f5e', width: 2.5 }
603
  };
604
 
605
  const layout = Object.assign({}, layoutConfig, {
606
- title: { text: 'Live Hardware-Accelerated JAX Fit Overlay', font: { size: 14 } },
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.7)' }
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: 'Baseline Diffusivity (D₀)', value: `${(params.D0 || 0).toExponential(3)} cm²/s` },
627
- { label: 'Central Voltage (V_c)', value: `${(params.Vc || 0).toFixed(4)} V` },
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 Current Offset (I_offset)', value: `${(params.I_offset || 0).toExponential(3)} A` },
631
- { label: 'Final Objective Loss', value: results.final_loss ? results.final_loss.toExponential(4) : 'N/A' }
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, 'cv_optimization_results.json', 'application/json');
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"), 'cv_optimization_data.csv', 'text/csv');
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
- probePythonBackend();
748
- const urlInput = document.getElementById('backend-url-input');
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> &bull; <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> &bull; Potential (Col ${potCol}) &amp; Current (Col ${curCol}) &bull; ${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') {