SissiFeng commited on
Commit
cfab4f2
·
verified ·
1 Parent(s): 0e530c7

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +560 -0
  2. requirements.txt +20 -0
  3. workflow_integration.py +258 -0
app.py ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AutoEIS Hugging Face Space Application
3
+ Optimized for limited resources with workflow integration
4
+ """
5
+
6
+ import gradio as gr
7
+ import pandas as pd
8
+ import numpy as np
9
+ import base64
10
+ import json
11
+ import requests
12
+ import io
13
+ import os
14
+ import psutil
15
+ import gc
16
+ from typing import Dict, Any, Optional, Tuple
17
+ from datetime import datetime
18
+ import traceback
19
+ import asyncio
20
+ import aiohttp
21
+ from urllib.parse import parse_qs, urlparse
22
+
23
+ # Import AutoEIS with error handling
24
+ try:
25
+ import autoeis as ae
26
+ except ImportError as e:
27
+ print(f"Warning: AutoEIS import issue: {e}")
28
+ ae = None
29
+
30
+ # Memory monitoring
31
+ def get_memory_usage():
32
+ """Get current memory usage in MB"""
33
+ process = psutil.Process(os.getpid())
34
+ return process.memory_info().rss / 1024 / 1024
35
+
36
+ def check_memory_available():
37
+ """Check if enough memory is available"""
38
+ memory_mb = get_memory_usage()
39
+ available_mb = psutil.virtual_memory().available / 1024 / 1024
40
+ return available_mb > 500 # Need at least 500MB free
41
+
42
+ # Global variables for workflow integration
43
+ workflow_context = {
44
+ "workflow_id": None,
45
+ "node_id": None,
46
+ "callback_url": None,
47
+ "auth_token": None
48
+ }
49
+
50
+ # Optimized parameters for HF Spaces
51
+ HF_OPTIMIZED_PARAMS = {
52
+ "iters": 20, # Reduced from 50
53
+ "complexity": 8, # Reduced from 12
54
+ "generations": 15, # Reduced from 30
55
+ "population_size": 50, # Reduced from 100
56
+ "test_set_frac": 0.2, # Increased for faster validation
57
+ "random_state": 42
58
+ }
59
+
60
+ def parse_workflow_params(request: gr.Request) -> Dict[str, Any]:
61
+ """Parse workflow parameters from URL or headers"""
62
+ params = {}
63
+
64
+ # Try to get params from URL query string
65
+ if request and hasattr(request, 'query_params'):
66
+ query_params = dict(request.query_params)
67
+ if 'params' in query_params:
68
+ try:
69
+ encoded_params = query_params['params']
70
+ decoded = base64.b64decode(encoded_params)
71
+ params = json.loads(decoded)
72
+ except Exception as e:
73
+ print(f"Error parsing URL params: {e}")
74
+
75
+ return params
76
+
77
+ def decode_csv_data(encoded_data: str) -> pd.DataFrame:
78
+ """Decode base64 CSV data to DataFrame"""
79
+ try:
80
+ csv_bytes = base64.b64decode(encoded_data)
81
+ csv_string = csv_bytes.decode('utf-8')
82
+ df = pd.read_csv(io.StringIO(csv_string))
83
+ return df
84
+ except Exception as e:
85
+ print(f"Error decoding CSV: {e}")
86
+ return None
87
+
88
+ async def send_callback(results: Dict[str, Any]) -> bool:
89
+ """Send results back to workflow system"""
90
+ if not workflow_context["callback_url"]:
91
+ return False
92
+
93
+ try:
94
+ headers = {
95
+ "Content-Type": "application/json",
96
+ "Authorization": f"Bearer {workflow_context['auth_token']}"
97
+ }
98
+
99
+ payload = {
100
+ "workflow_id": workflow_context["workflow_id"],
101
+ "node_id": workflow_context["node_id"],
102
+ "status": "completed",
103
+ "results": results,
104
+ "analysis_timestamp": datetime.utcnow().isoformat() + "Z"
105
+ }
106
+
107
+ async with aiohttp.ClientSession() as session:
108
+ async with session.post(
109
+ workflow_context["callback_url"],
110
+ json=payload,
111
+ headers=headers,
112
+ timeout=aiohttp.ClientTimeout(total=30)
113
+ ) as response:
114
+ return response.status == 200
115
+ except Exception as e:
116
+ print(f"Callback error: {e}")
117
+ return False
118
+
119
+ def create_sample_data() -> pd.DataFrame:
120
+ """Create sample EIS data for demonstration"""
121
+ frequencies = np.logspace(5, -2, 50) # 100kHz to 0.01Hz
122
+
123
+ # Simple RC circuit simulation
124
+ R0 = 100 # Ohms
125
+ R1 = 500 # Ohms
126
+ C1 = 1e-6 # Farads
127
+
128
+ omega = 2 * np.pi * frequencies
129
+ Z_R0 = R0
130
+ Z_RC = R1 / (1 + 1j * omega * R1 * C1)
131
+ Z_total = Z_R0 + Z_RC
132
+
133
+ df = pd.DataFrame({
134
+ 'frequency': frequencies,
135
+ 'z_real': Z_total.real,
136
+ 'z_imag': -Z_total.imag
137
+ })
138
+
139
+ return df
140
+
141
+ def analyze_eis_optimized(
142
+ df: pd.DataFrame,
143
+ circuit_model: str = "auto",
144
+ algorithm: str = "lm",
145
+ use_hf_params: bool = True,
146
+ progress_callback=None
147
+ ) -> Tuple[Dict[str, Any], str, str]:
148
+ """
149
+ Analyze EIS data with HF optimization
150
+ Returns: (results_dict, nyquist_plot, bode_plot)
151
+ """
152
+ if ae is None:
153
+ return {"error": "AutoEIS not available"}, None, None
154
+
155
+ # Check memory before starting
156
+ if not check_memory_available():
157
+ gc.collect() # Try garbage collection
158
+ if not check_memory_available():
159
+ return {"error": "Insufficient memory available"}, None, None
160
+
161
+ try:
162
+ # Prepare impedance data
163
+ Z = df['z_real'].values + 1j * df['z_imag'].values
164
+ freq = df['frequency'].values
165
+
166
+ # Use optimized parameters for HF
167
+ params = HF_OPTIMIZED_PARAMS.copy() if use_hf_params else {}
168
+
169
+ if progress_callback:
170
+ progress_callback(0.2, "Initializing AutoEIS...")
171
+
172
+ # Circuit detection with limited complexity
173
+ if circuit_model == "auto":
174
+ if progress_callback:
175
+ progress_callback(0.4, "Detecting circuit model...")
176
+
177
+ # Use simpler approach for HF
178
+ circuits = ae.core.generate_equivalent_circuits(
179
+ Z,
180
+ freq,
181
+ iters=params.get("iters", 20),
182
+ complexity=params.get("complexity", 8),
183
+ generations=params.get("generations", 15),
184
+ population_size=params.get("population_size", 50),
185
+ test_set_frac=params.get("test_set_frac", 0.2),
186
+ random_state=params.get("random_state", 42)
187
+ )
188
+
189
+ if circuits and len(circuits) > 0:
190
+ circuit_str = circuits[0] # Take the best circuit
191
+ else:
192
+ circuit_str = "R0-[R1,C1]" # Fallback simple circuit
193
+ else:
194
+ circuit_str = circuit_model
195
+
196
+ if progress_callback:
197
+ progress_callback(0.6, "Fitting circuit parameters...")
198
+
199
+ # Fit the circuit
200
+ circuit = ae.core.get_parameterized_circuit(circuit_str)
201
+ fitted_params = ae.core.fit_parameters(circuit, freq, Z)
202
+
203
+ if progress_callback:
204
+ progress_callback(0.8, "Generating plots...")
205
+
206
+ # Generate plots
207
+ import matplotlib
208
+ matplotlib.use('Agg') # Non-interactive backend
209
+ import matplotlib.pyplot as plt
210
+
211
+ # Nyquist plot
212
+ fig_nyquist, ax_nyquist = plt.subplots(figsize=(8, 6))
213
+ ax_nyquist.plot(Z.real, Z.imag, 'bo', label='Data', markersize=6)
214
+
215
+ # Add fitted curve if available
216
+ if fitted_params:
217
+ Z_fit = circuit(freq, **fitted_params)
218
+ ax_nyquist.plot(Z_fit.real, Z_fit.imag, 'r-', label='Fit', linewidth=2)
219
+
220
+ ax_nyquist.set_xlabel('Z\' (Ω)')
221
+ ax_nyquist.set_ylabel('-Z\'\' (Ω)')
222
+ ax_nyquist.set_title('Nyquist Plot')
223
+ ax_nyquist.legend()
224
+ ax_nyquist.grid(True, alpha=0.3)
225
+ ax_nyquist.set_aspect('equal')
226
+
227
+ # Bode plot
228
+ fig_bode, (ax_mag, ax_phase) = plt.subplots(2, 1, figsize=(8, 8))
229
+
230
+ Z_mag = np.abs(Z)
231
+ Z_phase = np.angle(Z, deg=True)
232
+
233
+ ax_mag.loglog(freq, Z_mag, 'bo', label='Data', markersize=6)
234
+ ax_mag.set_ylabel('|Z| (Ω)')
235
+ ax_mag.set_title('Bode Plot - Magnitude')
236
+ ax_mag.grid(True, which="both", alpha=0.3)
237
+ ax_mag.legend()
238
+
239
+ ax_phase.semilogx(freq, Z_phase, 'bo', label='Data', markersize=6)
240
+
241
+ if fitted_params:
242
+ Z_fit = circuit(freq, **fitted_params)
243
+ ax_mag.loglog(freq, np.abs(Z_fit), 'r-', label='Fit', linewidth=2)
244
+ ax_phase.semilogx(freq, np.angle(Z_fit, deg=True), 'r-', label='Fit', linewidth=2)
245
+
246
+ ax_phase.set_xlabel('Frequency (Hz)')
247
+ ax_phase.set_ylabel('Phase (°)')
248
+ ax_phase.set_title('Bode Plot - Phase')
249
+ ax_phase.grid(True, alpha=0.3)
250
+ ax_phase.legend()
251
+
252
+ plt.tight_layout()
253
+
254
+ # Calculate fit quality
255
+ if fitted_params:
256
+ Z_fit = circuit(freq, **fitted_params)
257
+ residuals = Z - Z_fit
258
+ chi_squared = np.sum(np.abs(residuals)**2) / len(Z)
259
+ fit_error = np.sqrt(chi_squared)
260
+ else:
261
+ chi_squared = None
262
+ fit_error = None
263
+
264
+ # Prepare results
265
+ results = {
266
+ "circuit_model": circuit_str,
267
+ "fit_parameters": fitted_params if fitted_params else {},
268
+ "fit_error": float(fit_error) if fit_error else None,
269
+ "chi_squared": float(chi_squared) if chi_squared else None,
270
+ "memory_usage_mb": get_memory_usage()
271
+ }
272
+
273
+ if progress_callback:
274
+ progress_callback(1.0, "Analysis complete!")
275
+
276
+ # Clean up memory
277
+ gc.collect()
278
+
279
+ return results, fig_nyquist, fig_bode
280
+
281
+ except Exception as e:
282
+ error_msg = f"Analysis error: {str(e)}\n{traceback.format_exc()}"
283
+ print(error_msg)
284
+ return {"error": error_msg}, None, None
285
+ finally:
286
+ # Always try to free memory
287
+ gc.collect()
288
+
289
+ def process_analysis(
290
+ data_file,
291
+ circuit_model,
292
+ algorithm,
293
+ use_optimization,
294
+ progress=gr.Progress()
295
+ ):
296
+ """Main analysis function for Gradio interface"""
297
+
298
+ progress(0.1, "Starting analysis...")
299
+
300
+ # Load data
301
+ if data_file is None:
302
+ progress(0.2, "Using sample data...")
303
+ df = create_sample_data()
304
+ else:
305
+ try:
306
+ df = pd.read_csv(data_file.name)
307
+ except Exception as e:
308
+ return {"error": f"Failed to read CSV: {e}"}, None, None
309
+
310
+ # Run analysis
311
+ results, nyquist_plot, bode_plot = analyze_eis_optimized(
312
+ df,
313
+ circuit_model=circuit_model,
314
+ algorithm=algorithm,
315
+ use_hf_params=use_optimization,
316
+ progress_callback=progress
317
+ )
318
+
319
+ return results, nyquist_plot, bode_plot
320
+
321
+ async def send_to_workflow(results):
322
+ """Send results back to workflow"""
323
+ if workflow_context["callback_url"]:
324
+ success = await send_callback(results)
325
+ return "✅ Results sent to workflow!" if success else "❌ Failed to send results"
326
+ return "No workflow callback URL configured"
327
+
328
+ # Create Gradio interface
329
+ def create_interface():
330
+ with gr.Blocks(title="AutoEIS Analyzer", theme=gr.themes.Soft()) as app:
331
+ # Header
332
+ gr.Markdown("""
333
+ # 🔬 AutoEIS Analysis Tool
334
+ ### Automated Electrochemical Impedance Spectroscopy Analysis
335
+
336
+ Optimized for Hugging Face Spaces with workflow integration support.
337
+ """)
338
+
339
+ # Memory monitor
340
+ with gr.Row():
341
+ memory_display = gr.Textbox(
342
+ label="Memory Usage",
343
+ value=f"{get_memory_usage():.1f} MB",
344
+ interactive=False,
345
+ scale=1
346
+ )
347
+ workflow_info = gr.Textbox(
348
+ label="Workflow Context",
349
+ value="No workflow connected",
350
+ interactive=False,
351
+ scale=3
352
+ )
353
+
354
+ with gr.Tabs():
355
+ # Data Input Tab
356
+ with gr.Tab("📊 Data Input"):
357
+ with gr.Row():
358
+ data_file = gr.File(
359
+ label="Upload EIS Data (CSV)",
360
+ file_types=[".csv"],
361
+ value=None
362
+ )
363
+
364
+ with gr.Row():
365
+ gr.Markdown("""
366
+ **Expected CSV Format:**
367
+ - Column 1: `frequency` (Hz)
368
+ - Column 2: `z_real` (Ω)
369
+ - Column 3: `z_imag` (Ω)
370
+
371
+ Leave empty to use sample data.
372
+ """)
373
+
374
+ data_preview = gr.DataFrame(
375
+ label="Data Preview (first 10 rows)",
376
+ interactive=False
377
+ )
378
+
379
+ # Parameters Tab
380
+ with gr.Tab("⚙️ Parameters"):
381
+ with gr.Row():
382
+ circuit_model = gr.Dropdown(
383
+ choices=["auto", "R0-[R1,C1]", "R0-[R1,P1]", "R0-[R1,C1]-[R2,C2]"],
384
+ value="auto",
385
+ label="Circuit Model",
386
+ info="Select 'auto' for automatic detection"
387
+ )
388
+
389
+ algorithm = gr.Radio(
390
+ choices=["lm", "trf", "dogbox"],
391
+ value="lm",
392
+ label="Fitting Algorithm",
393
+ info="Levenberg-Marquardt (lm) is usually best"
394
+ )
395
+
396
+ with gr.Row():
397
+ use_optimization = gr.Checkbox(
398
+ value=True,
399
+ label="Use HF-optimized parameters",
400
+ info="Recommended for Hugging Face Spaces (faster, less memory)"
401
+ )
402
+
403
+ with gr.Row():
404
+ gr.Markdown("""
405
+ **Optimization Settings (when enabled):**
406
+ - Reduced iterations: 20 (vs 50)
407
+ - Lower complexity: 8 (vs 12)
408
+ - Smaller population: 50 (vs 100)
409
+ - Faster validation: 20% test set
410
+ """)
411
+
412
+ # Results Tab
413
+ with gr.Tab("📈 Results"):
414
+ with gr.Row():
415
+ results_json = gr.JSON(
416
+ label="Analysis Results",
417
+ value=None
418
+ )
419
+
420
+ with gr.Row():
421
+ nyquist_plot = gr.Plot(
422
+ label="Nyquist Plot",
423
+ show_label=True
424
+ )
425
+
426
+ bode_plot = gr.Plot(
427
+ label="Bode Plot",
428
+ show_label=True
429
+ )
430
+
431
+ with gr.Row():
432
+ workflow_btn = gr.Button(
433
+ "📤 Send to Workflow",
434
+ variant="secondary",
435
+ visible=False
436
+ )
437
+ workflow_status = gr.Textbox(
438
+ label="Workflow Status",
439
+ interactive=False,
440
+ visible=False
441
+ )
442
+
443
+ # Action buttons
444
+ with gr.Row():
445
+ analyze_btn = gr.Button(
446
+ "🚀 Run Analysis",
447
+ variant="primary",
448
+ size="lg"
449
+ )
450
+
451
+ clear_btn = gr.Button(
452
+ "🔄 Clear",
453
+ variant="secondary"
454
+ )
455
+
456
+ # Event handlers
457
+ def update_preview(file):
458
+ if file is None:
459
+ df = create_sample_data()
460
+ return df.head(10), f"Memory: {get_memory_usage():.1f} MB"
461
+ try:
462
+ df = pd.read_csv(file.name)
463
+ return df.head(10), f"Memory: {get_memory_usage():.1f} MB"
464
+ except:
465
+ return None, f"Memory: {get_memory_usage():.1f} MB"
466
+
467
+ def clear_all():
468
+ gc.collect()
469
+ return (
470
+ None, # data_file
471
+ None, # data_preview
472
+ "auto", # circuit_model
473
+ "lm", # algorithm
474
+ True, # use_optimization
475
+ None, # results_json
476
+ None, # nyquist_plot
477
+ None, # bode_plot
478
+ f"Memory: {get_memory_usage():.1f} MB" # memory_display
479
+ )
480
+
481
+ # Wire up events
482
+ data_file.change(
483
+ fn=update_preview,
484
+ inputs=[data_file],
485
+ outputs=[data_preview, memory_display]
486
+ )
487
+
488
+ analyze_btn.click(
489
+ fn=process_analysis,
490
+ inputs=[
491
+ data_file,
492
+ circuit_model,
493
+ algorithm,
494
+ use_optimization
495
+ ],
496
+ outputs=[
497
+ results_json,
498
+ nyquist_plot,
499
+ bode_plot
500
+ ]
501
+ )
502
+
503
+ clear_btn.click(
504
+ fn=clear_all,
505
+ outputs=[
506
+ data_file,
507
+ data_preview,
508
+ circuit_model,
509
+ algorithm,
510
+ use_optimization,
511
+ results_json,
512
+ nyquist_plot,
513
+ bode_plot,
514
+ memory_display
515
+ ]
516
+ )
517
+
518
+ workflow_btn.click(
519
+ fn=lambda r: asyncio.run(send_to_workflow(r)),
520
+ inputs=[results_json],
521
+ outputs=[workflow_status]
522
+ )
523
+
524
+ # Load workflow params on startup
525
+ def on_load(request: gr.Request):
526
+ params = parse_workflow_params(request)
527
+ if params:
528
+ workflow_context.update({
529
+ "workflow_id": params.get("workflow_id"),
530
+ "node_id": params.get("node_id"),
531
+ "callback_url": params.get("callback_url"),
532
+ "auth_token": params.get("auth_token")
533
+ })
534
+
535
+ if params.get("input_data", {}).get("csv_data"):
536
+ # Decode and process CSV data
537
+ df = decode_csv_data(params["input_data"]["csv_data"])
538
+ if df is not None:
539
+ return f"Workflow: {params.get('workflow_id', 'Unknown')}", True, True
540
+
541
+ return f"Workflow: {params.get('workflow_id', 'Unknown')}", True, False
542
+
543
+ return "No workflow connected", False, False
544
+
545
+ app.load(
546
+ fn=on_load,
547
+ outputs=[workflow_info, workflow_btn, workflow_status]
548
+ )
549
+
550
+ return app
551
+
552
+ # Launch the app
553
+ if __name__ == "__main__":
554
+ app = create_interface()
555
+ app.launch(
556
+ server_name="0.0.0.0",
557
+ server_port=7860,
558
+ share=False,
559
+ show_error=True
560
+ )
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies - optimized for HF Spaces limited resources
2
+ gradio==4.19.0
3
+ autoeis==0.0.39
4
+ pandas>=1.5.0,<2.0.0
5
+ numpy>=1.21.0,<1.24.0
6
+ matplotlib>=3.5.0,<4.0.0
7
+ scikit-learn>=1.0.0,<2.0.0
8
+ requests>=2.28.0
9
+
10
+ # Optional - for enhanced visualization
11
+ plotly>=5.0.0
12
+ schemdraw>=0.15
13
+
14
+ # Memory optimization
15
+ psutil>=5.9.0
16
+
17
+ # For workflow integration
18
+ aiohttp>=3.8.0
19
+ python-jose[cryptography]>=3.3.0 # For JWT handling
20
+ python-multipart>=0.0.5 # For file uploads
workflow_integration.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Workflow Integration Helper for AutoEIS Hugging Face Space
3
+ This module provides utilities for integrating with external workflow systems
4
+ """
5
+
6
+ import base64
7
+ import json
8
+ import requests
9
+ from typing import Dict, Any, Optional
10
+ from datetime import datetime, timedelta
11
+ import jwt
12
+ import hashlib
13
+
14
+
15
+ class WorkflowClient:
16
+ """Client for integrating AutoEIS HF Space with workflow systems"""
17
+
18
+ def __init__(self, hf_space_url: str, secret_key: str = None):
19
+ """
20
+ Initialize workflow client
21
+
22
+ Args:
23
+ hf_space_url: URL of your Hugging Face Space
24
+ secret_key: Secret key for JWT generation (optional)
25
+ """
26
+ self.hf_space_url = hf_space_url.rstrip('/')
27
+ self.secret_key = secret_key or self._generate_secret()
28
+
29
+ def _generate_secret(self) -> str:
30
+ """Generate a secret key if none provided"""
31
+ return hashlib.sha256(
32
+ f"autoeis-{datetime.utcnow().isoformat()}".encode()
33
+ ).hexdigest()
34
+
35
+ def create_jwt_token(self, workflow_id: str, expires_in: int = 1800) -> str:
36
+ """
37
+ Create a JWT token for authentication
38
+
39
+ Args:
40
+ workflow_id: ID of the workflow
41
+ expires_in: Token expiration time in seconds (default 30 minutes)
42
+
43
+ Returns:
44
+ JWT token string
45
+ """
46
+ payload = {
47
+ 'workflow_id': workflow_id,
48
+ 'exp': datetime.utcnow() + timedelta(seconds=expires_in),
49
+ 'iat': datetime.utcnow(),
50
+ 'iss': 'autoeis-workflow'
51
+ }
52
+
53
+ return jwt.encode(payload, self.secret_key, algorithm='HS256')
54
+
55
+ def prepare_analysis_params(
56
+ self,
57
+ workflow_id: str,
58
+ node_id: str,
59
+ callback_url: str,
60
+ csv_data: str,
61
+ filename: str = "eis_data.csv",
62
+ circuit_model: str = "auto",
63
+ fitting_algorithm: str = "lm",
64
+ max_iterations: int = 1000,
65
+ tolerance: float = 1e-8,
66
+ generate_plots: bool = True
67
+ ) -> Dict[str, Any]:
68
+ """
69
+ Prepare parameters for AutoEIS analysis
70
+
71
+ Args:
72
+ workflow_id: Unique workflow identifier
73
+ node_id: Node ID in the workflow
74
+ callback_url: URL to send results back to
75
+ csv_data: CSV content as string
76
+ filename: Name of the CSV file
77
+ circuit_model: Circuit model to use ("auto" for automatic detection)
78
+ fitting_algorithm: Algorithm for fitting ("lm", "trf", or "dogbox")
79
+ max_iterations: Maximum fitting iterations
80
+ tolerance: Fitting tolerance
81
+ generate_plots: Whether to generate plots
82
+
83
+ Returns:
84
+ Dictionary of parameters ready for encoding
85
+ """
86
+ # Encode CSV data to base64
87
+ csv_base64 = base64.b64encode(csv_data.encode()).decode()
88
+
89
+ # Generate auth token
90
+ auth_token = self.create_jwt_token(workflow_id)
91
+
92
+ params = {
93
+ "workflow_id": workflow_id,
94
+ "node_id": node_id,
95
+ "callback_url": callback_url,
96
+ "input_data": {
97
+ "csv_data": csv_base64,
98
+ "filename": filename,
99
+ "parameters": {
100
+ "frequency_column": "frequency",
101
+ "z_real_column": "z_real",
102
+ "z_imag_column": "z_imag",
103
+ "circuit_initial_guess": circuit_model,
104
+ "fitting_algorithm": fitting_algorithm,
105
+ "max_iterations": max_iterations,
106
+ "tolerance": tolerance,
107
+ "generate_plots": generate_plots,
108
+ "save_circuit_diagram": True
109
+ }
110
+ },
111
+ "auth_token": auth_token
112
+ }
113
+
114
+ return params
115
+
116
+ def generate_hf_url(self, params: Dict[str, Any]) -> str:
117
+ """
118
+ Generate the Hugging Face Space URL with encoded parameters
119
+
120
+ Args:
121
+ params: Dictionary of parameters from prepare_analysis_params
122
+
123
+ Returns:
124
+ Complete URL to open the HF Space with parameters
125
+ """
126
+ # Encode parameters to base64
127
+ params_json = json.dumps(params)
128
+ encoded_params = base64.b64encode(params_json.encode()).decode()
129
+
130
+ # Create URL
131
+ url = f"{self.hf_space_url}?params={encoded_params}"
132
+
133
+ return url
134
+
135
+ def send_to_hf_space(
136
+ self,
137
+ workflow_id: str,
138
+ node_id: str,
139
+ callback_url: str,
140
+ csv_data: str,
141
+ **kwargs
142
+ ) -> Dict[str, Any]:
143
+ """
144
+ Send analysis request to HF Space
145
+
146
+ Args:
147
+ workflow_id: Unique workflow identifier
148
+ node_id: Node ID in the workflow
149
+ callback_url: URL to send results back to
150
+ csv_data: CSV content as string
151
+ **kwargs: Additional parameters for prepare_analysis_params
152
+
153
+ Returns:
154
+ Dictionary with action and URL
155
+ """
156
+ # Prepare parameters
157
+ params = self.prepare_analysis_params(
158
+ workflow_id=workflow_id,
159
+ node_id=node_id,
160
+ callback_url=callback_url,
161
+ csv_data=csv_data,
162
+ **kwargs
163
+ )
164
+
165
+ # Generate URL
166
+ url = self.generate_hf_url(params)
167
+
168
+ return {
169
+ "action": "open_external",
170
+ "url": url,
171
+ "wait_for_callback": True,
172
+ "params": params
173
+ }
174
+
175
+ def verify_callback_token(self, token: str) -> Dict[str, Any]:
176
+ """
177
+ Verify a JWT token from callback
178
+
179
+ Args:
180
+ token: JWT token string
181
+
182
+ Returns:
183
+ Decoded payload if valid
184
+
185
+ Raises:
186
+ jwt.InvalidTokenError: If token is invalid
187
+ """
188
+ return jwt.decode(token, self.secret_key, algorithms=['HS256'])
189
+
190
+ def process_callback_results(self, callback_data: Dict[str, Any]) -> Dict[str, Any]:
191
+ """
192
+ Process results received from HF Space callback
193
+
194
+ Args:
195
+ callback_data: Data received from callback
196
+
197
+ Returns:
198
+ Processed results
199
+ """
200
+ results = {
201
+ "workflow_id": callback_data.get("workflow_id"),
202
+ "node_id": callback_data.get("node_id"),
203
+ "status": callback_data.get("status"),
204
+ "circuit_model": callback_data.get("results", {}).get("circuit_model"),
205
+ "fit_parameters": callback_data.get("results", {}).get("fit_parameters"),
206
+ "fit_error": callback_data.get("results", {}).get("fit_error"),
207
+ "chi_squared": callback_data.get("results", {}).get("chi_squared"),
208
+ "timestamp": callback_data.get("analysis_timestamp")
209
+ }
210
+
211
+ # Extract plots if available
212
+ if "plots" in callback_data.get("results", {}):
213
+ results["plots"] = callback_data["results"]["plots"]
214
+
215
+ return results
216
+
217
+
218
+ # Example usage
219
+ if __name__ == "__main__":
220
+ # Initialize client
221
+ client = WorkflowClient(
222
+ hf_space_url="https://huggingface.co/spaces/YOUR_USERNAME/autoeis-analyzer",
223
+ secret_key="your-secret-key-here"
224
+ )
225
+
226
+ # Sample CSV data
227
+ sample_csv = """frequency,z_real,z_imag
228
+ 100000,100.5,5.2
229
+ 50000,102.3,8.7
230
+ 10000,108.9,15.3
231
+ 5000,115.2,22.1
232
+ 1000,125.6,35.8
233
+ 500,138.9,45.2
234
+ 100,156.3,58.9
235
+ 50,172.5,65.3
236
+ 10,195.8,68.2
237
+ 5,215.3,65.8
238
+ 1,245.6,52.3
239
+ 0.5,268.9,38.9
240
+ 0.1,295.3,15.2"""
241
+
242
+ # Prepare and send to HF Space
243
+ result = client.send_to_hf_space(
244
+ workflow_id="test-workflow-123",
245
+ node_id="autoeis-node-1",
246
+ callback_url="https://your-system.com/api/autoeis/callback",
247
+ csv_data=sample_csv,
248
+ circuit_model="auto",
249
+ fitting_algorithm="lm"
250
+ )
251
+
252
+ print("Generated URL:", result["url"])
253
+ print("\nTo integrate with your workflow system:")
254
+ print("1. Open the URL in a new window/iframe")
255
+ print("2. User performs analysis in HF Space")
256
+ print("3. Results will be sent to your callback URL")
257
+ print("4. Verify the JWT token in the callback")
258
+ print("5. Process the results using process_callback_results()")