qurashiubaid commited on
Commit
638afcf
Β·
verified Β·
1 Parent(s): 376873a

Upload 7 files

Browse files
Files changed (7) hide show
  1. Project Structure.txt +6 -0
  2. README.md +43 -0
  3. analyzer.py +627 -0
  4. app.py +102 -0
  5. core.py +218 -0
  6. dataset_utils.py +43 -0
  7. requirements.txt +10 -0
Project Structure.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ multimodal-materials-analyzer/
2
+ β”œβ”€β”€ app.py
3
+ β”œβ”€β”€ core.py # Lightweight analysis (no heavy deps)
4
+ β”œβ”€β”€ requirements.txt
5
+ β”œβ”€β”€ README.md
6
+ └── dataset_utils.py # Safe dataset contribution
README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-Modal Materials Characterization Pipeline
2
+
3
+ This repository contains a **Gradio app for Hugging Face Spaces** that provides automated analysis of multi-modal materials characterization data using the Universal Fiber Bundle framework.
4
+
5
+ ## Features
6
+
7
+ - **XRD Analysis**: Phase identification, crystallite size, microstrain
8
+ - **VSM Analysis**: Coercivity, remanence, magnetic phase detection
9
+ - **UV-Vis Analysis**: Bandgap estimation, absorption edge analysis
10
+ - **PL Analysis**: Emission peak detection, defect state analysis
11
+ - **TEM/SEM Analysis**: Particle size distribution, morphology
12
+ - **Cross-Modal Correlations**: Quantum confinement, defect-magnetism relationships
13
+ - **Community Dataset**: Anonymized results contribute to a public dataset
14
+
15
+ ## Data Requirements
16
+
17
+ ### XRD, VSM, UV-Vis, PL
18
+ - CSV files with columns:
19
+ - XRD: `2theta`, `intensity`
20
+ - VSM: `H`, `M`
21
+ - UV-Vis: `wavelength`, `absorption`
22
+ - PL: `wavelength`, `intensity`
23
+
24
+ ### TEM/SEM
25
+ - Image files (PNG, JPG, TIFF) with scale bar (1 pixel = 1 nm assumed)
26
+
27
+ ## Deployment
28
+
29
+ 1. Create a Hugging Face account and dataset repository
30
+ 2. Update `HF_DATASET_REPO` in `app.py`
31
+ 3. Deploy to Hugging Face Spaces
32
+
33
+ ## Usage
34
+
35
+ 1. Upload your data files
36
+ 2. Provide a sample name
37
+ 3. Click "Analyze Sample"
38
+ 4. View the scientific report and plots
39
+ 5. Optionally contribute results to the public dataset
40
+
41
+ ## Citation
42
+
43
+ If you use this tool in your research, please cite:
analyzer.py ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ from scipy.signal import savgol_filter, find_peaks
6
+ from scipy.ndimage import gaussian_filter1d
7
+ from scipy.spatial.distance import pdist, squareform
8
+ from sklearn.preprocessing import StandardScaler
9
+ from pymatgen.core import Structure
10
+ from pymatgen.analysis.diffraction.xrd import XRDCalculator
11
+ import cv2
12
+ from skimage import filters, measure, morphology
13
+ from scipy import ndimage
14
+ import requests
15
+ import re
16
+ import tempfile
17
+ import json
18
+ from typing import Dict, List, Tuple, Optional
19
+
20
+ # Configure matplotlib for headless operation
21
+ plt.switch_backend('Agg')
22
+
23
+ class UniversalFiberBundleAnalyzer:
24
+ """Core analyzer for multi-modal materials data"""
25
+
26
+ def __init__(self):
27
+ self.results = {}
28
+
29
+ def process_sample(self, files: Dict[str, str], sample_name: str = "sample") -> Dict:
30
+ """
31
+ Process all available modalities for a sample
32
+
33
+ Args:
34
+ files: Dictionary with keys: 'xrd', 'vsm', 'uvvis', 'pl', 'tem'
35
+ sample_name: Name for the sample
36
+
37
+ Returns:
38
+ Dictionary with analysis results
39
+ """
40
+ results = {"sample_name": sample_name}
41
+
42
+ # Process XRD
43
+ if files.get('xrd'):
44
+ try:
45
+ xrd_data = self._load_spectral_data(files['xrd'])
46
+ xrd_analyzer = XRDAnalyzer()
47
+ xrd_invariants = xrd_analyzer.compute_local_invariants(xrd_data['x'], xrd_data['y'])
48
+ xrd_features = xrd_analyzer.extract_global_features(xrd_data['x'], xrd_data['y'], xrd_invariants)
49
+ results['xrd'] = {
50
+ 'wavelength': xrd_data['x'],
51
+ 'intensity': xrd_data['y'],
52
+ 'invariants': xrd_invariants,
53
+ 'features': xrd_features
54
+ }
55
+ except Exception as e:
56
+ results['xrd_error'] = str(e)
57
+
58
+ # Process VSM
59
+ if files.get('vsm'):
60
+ try:
61
+ vsm_data = self._load_spectral_data(files['vsm'])
62
+ vsm_analyzer = VSMAnalyzer()
63
+ vsm_invariants = vsm_analyzer.compute_local_invariants(vsm_data['x'], vsm_data['y'])
64
+ Hc, Mr = vsm_analyzer.detect_magnetic_params(vsm_data['x'], vsm_data['y'])
65
+ results['vsm'] = {
66
+ 'H': vsm_data['x'],
67
+ 'M': vsm_data['y'],
68
+ 'invariants': vsm_invariants,
69
+ 'Hc': Hc,
70
+ 'Mr': Mr
71
+ }
72
+ except Exception as e:
73
+ results['vsm_error'] = str(e)
74
+
75
+ # Process UV-Vis
76
+ if files.get('uvvis'):
77
+ try:
78
+ uvvis_data = self._load_spectral_data(files['uvvis'])
79
+ uvvis_analyzer = UVVisAnalyzer()
80
+ uvvis_invariants = uvvis_analyzer.compute_local_invariants(uvvis_data['x'], uvvis_data['y'])
81
+ bandgap = uvvis_analyzer.estimate_bandgap(uvvis_data['x'], uvvis_data['y'])
82
+ results['uvvis'] = {
83
+ 'wavelength': uvvis_data['x'],
84
+ 'absorption': uvvis_data['y'],
85
+ 'invariants': uvvis_invariants,
86
+ 'bandgap_eV': bandgap
87
+ }
88
+ except Exception as e:
89
+ results['uvvis_error'] = str(e)
90
+
91
+ # Process PL
92
+ if files.get('pl'):
93
+ try:
94
+ pl_data = self._load_spectral_data(files['pl'])
95
+ pl_analyzer = PLAnalyzer()
96
+ pl_invariants = pl_analyzer.compute_local_invariants(pl_data['x'], pl_data['y'])
97
+ peaks = pl_analyzer.extract_pl_peaks(pl_data['x'], pl_data['y'])
98
+ results['pl'] = {
99
+ 'wavelength': pl_data['x'],
100
+ 'intensity': pl_data['y'],
101
+ 'invariants': pl_invariants,
102
+ 'peaks': peaks
103
+ }
104
+ except Exception as e:
105
+ results['pl_error'] = str(e)
106
+
107
+ # Process TEM
108
+ if files.get('tem'):
109
+ try:
110
+ tem_results = self._analyze_tem_image(files['tem'])
111
+ results['tem'] = tem_results
112
+ except Exception as e:
113
+ results['tem_error'] = str(e)
114
+
115
+ # Phase identification (requires XRD)
116
+ if 'xrd' in results:
117
+ try:
118
+ phases = self._identify_phases(results['xrd']['wavelength'], results['xrd']['intensity'])
119
+ results['phases'] = phases
120
+ except Exception as e:
121
+ results['phase_error'] = str(e)
122
+
123
+ return results
124
+
125
+ def _load_spectral_data(self, file_path: str) -> Dict[str, np.ndarray]:
126
+ """Load spectral data from CSV"""
127
+ df = pd.read_csv(file_path)
128
+ cols = [c.lower() for c in df.columns]
129
+
130
+ # Detect x column
131
+ if 'wavelength' in cols:
132
+ x_col = df.columns[cols.index('wavelength')]
133
+ elif 'energy' in cols:
134
+ x_col = df.columns[cols.index('energy')]
135
+ elif '2theta' in cols:
136
+ x_col = df.columns[cols.index('2theta')]
137
+ elif 'h' in cols:
138
+ x_col = df.columns[cols.index('h')]
139
+ else:
140
+ x_col = df.columns[0]
141
+
142
+ # Detect y column
143
+ if 'intensity' in cols:
144
+ y_col = df.columns[cols.index('intensity')]
145
+ elif 'm' in cols:
146
+ y_col = df.columns[cols.index('m')]
147
+ elif 'absorption' in cols:
148
+ y_col = df.columns[cols.index('absorption')]
149
+ else:
150
+ y_col = df.columns[1]
151
+
152
+ x = df[x_col].values.astype(float)
153
+ y = df[y_col].values.astype(float)
154
+
155
+ # Remove NaNs
156
+ valid = np.isfinite(x) & np.isfinite(y)
157
+ x, y = x[valid], y[valid]
158
+
159
+ # Sort by x
160
+ sort_idx = np.argsort(x)
161
+ x, y = x[sort_idx], y[sort_idx]
162
+
163
+ return {'x': x, 'y': y}
164
+
165
+ def _analyze_tem_image(self, image_path: str) -> Dict:
166
+ """Analyze TEM/SEM image for particle size"""
167
+ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
168
+ if img is None:
169
+ raise ValueError("Could not load TEM image")
170
+
171
+ # Resize for consistent processing
172
+ img = cv2.resize(img, (1024, 1024))
173
+ img = cv2.GaussianBlur(img, (5, 5), 0)
174
+
175
+ # Threshold
176
+ thresh = filters.threshold_otsu(img)
177
+ binary = img < thresh
178
+
179
+ # Clean up
180
+ binary = morphology.remove_small_objects(binary, min_size=50)
181
+ binary = morphology.binary_closing(binary, morphology.disk(2))
182
+
183
+ # Label particles
184
+ labeled, num_features = ndimage.label(binary)
185
+ props = measure.regionprops(labeled)
186
+
187
+ if not props:
188
+ return {"particle_count": 0}
189
+
190
+ # Assume 1 pixel = 1 nm (user should calibrate)
191
+ pixel_size_nm = 1.0
192
+ areas = [p.area for p in props]
193
+ areas_nm2 = [a * pixel_size_nm**2 for a in areas]
194
+ diameters_nm = [2 * np.sqrt(a / np.pi) for a in areas_nm2]
195
+
196
+ return {
197
+ 'particle_count': len(areas),
198
+ 'mean_diameter_nm': float(np.mean(diameters_nm)),
199
+ 'std_diameter_nm': float(np.std(diameters_nm)),
200
+ 'min_diameter_nm': float(np.min(diameters_nm)),
201
+ 'max_diameter_nm': float(np.max(diameters_nm))
202
+ }
203
+
204
+ def _identify_phases(self, two_theta: np.ndarray, intensity: np.ndarray) -> List[Tuple[str, float]]:
205
+ """Identify phases using COD database"""
206
+ # Common material COD IDs
207
+ candidate_cod_ids = {
208
+ 'Fe3O4': '9008470',
209
+ 'CoFe2O4': '9008464',
210
+ 'Ξ³-Fe2O3': '1011106',
211
+ 'Ξ±-Fe2O3': '9007397',
212
+ 'TiO2_anatase': '9007679',
213
+ 'TiO2_rutile': '9007680'
214
+ }
215
+
216
+ calculator = XRDCalculator(wavelength=1.5406)
217
+ matches = []
218
+
219
+ for phase_name, cod_id in candidate_cod_ids.items():
220
+ structure = self._download_cod_structure(cod_id)
221
+ if structure is None:
222
+ continue
223
+
224
+ try:
225
+ xrd_pattern = calculator.get_pattern(structure)
226
+ sim_2theta = xrd_pattern.x
227
+ sim_intensity = xrd_pattern.y
228
+
229
+ # Interpolate to experimental grid
230
+ sim_interp = np.interp(two_theta, sim_2theta, sim_intensity, left=0, right=0)
231
+ sim_interp = sim_interp / (np.max(sim_interp) + 1e-8)
232
+ exp_norm = intensity / (np.max(intensity) + 1e-8)
233
+
234
+ # Compute correlation
235
+ correlation = np.corrcoef(exp_norm, sim_interp)[0, 1]
236
+ if not np.isnan(correlation):
237
+ matches.append((phase_name, float(correlation)))
238
+ except:
239
+ continue
240
+
241
+ # Sort by correlation
242
+ matches.sort(key=lambda x: x[1], reverse=True)
243
+ return matches[:3]
244
+
245
+ def _download_cod_structure(self, cod_id: str) -> Optional[Structure]:
246
+ """Download structure from Crystallography Open Database"""
247
+ try:
248
+ url = f"https://www.crystallography.net/cod/{cod_id}.cif"
249
+ response = requests.get(url, timeout=10)
250
+ if response.status_code == 200:
251
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.cif', delete=False) as f:
252
+ f.write(response.text)
253
+ temp_path = f.name
254
+
255
+ structure = Structure.from_file(temp_path)
256
+ os.unlink(temp_path)
257
+ return structure
258
+ except:
259
+ return None
260
+
261
+ def generate_report(self, results: Dict) -> str:
262
+ """Generate scientific interpretation report"""
263
+ report = []
264
+ report.append("=" * 60)
265
+ report.append(f"πŸ”¬ MULTI-MODAL MATERIALS ANALYSIS REPORT")
266
+ report.append(f"Sample: {results.get('sample_name', 'Unknown')}")
267
+ report.append("=" * 60)
268
+
269
+ # XRD analysis
270
+ if 'xrd' in results:
271
+ xrd = results['xrd']
272
+ report.append("\nπŸ“Š XRD ANALYSIS:")
273
+ report.append(f" β€’ Crystallite size: {xrd['features']['crystallite_size']:.2f} (rel. units)")
274
+ report.append(f" β€’ Microstrain: {xrd['features']['microstrain']:.3f}")
275
+ report.append(f" β€’ Amorphous ratio: {xrd['features']['amorphous_ratio']:.3f}")
276
+
277
+ # Phase identification
278
+ if 'phases' in results:
279
+ report.append("\nπŸ§ͺ PHASE IDENTIFICATION:")
280
+ for i, (phase, corr) in enumerate(results['phases']):
281
+ report.append(f" {i+1}. {phase} (correlation: {corr:.2f})")
282
+
283
+ # VSM analysis
284
+ if 'vsm' in results:
285
+ vsm = results['vsm']
286
+ report.append("\n🧲 VSM ANALYSIS:")
287
+ report.append(f" β€’ Coercivity (Hc): {vsm['Hc']:.1f} Oe")
288
+ report.append(f" β€’ Remanence (Mr): {vsm['Mr']:.3f} (norm.)")
289
+
290
+ # UV-Vis analysis
291
+ if 'uvvis' in results:
292
+ uvvis = results['uvvis']
293
+ report.append("\n🌈 UV-VIS ANALYSIS:")
294
+ report.append(f" β€’ Bandgap: {uvvis['bandgap_eV']:.2f} eV")
295
+
296
+ # PL analysis
297
+ if 'pl' in results:
298
+ pl = results['pl']
299
+ report.append("\nπŸ’‘ PHOTOLUMINESCENCE:")
300
+ if pl['peaks']:
301
+ peak = pl['peaks'][0]
302
+ report.append(f" β€’ Main peak: {peak['wavelength']:.1f} nm")
303
+ report.append(f" β€’ FWHM: {peak['fwhm']:.1f} nm")
304
+ else:
305
+ report.append(" β€’ No significant peaks detected")
306
+
307
+ # TEM analysis
308
+ if 'tem' in results:
309
+ tem = results['tem']
310
+ if tem['particle_count'] > 0:
311
+ report.append("\nπŸ”¬ TEM ANALYSIS:")
312
+ report.append(f" β€’ Particle count: {tem['particle_count']}")
313
+ report.append(f" β€’ Mean diameter: {tem['mean_diameter_nm']:.1f} Β± {tem['std_diameter_nm']:.1f} nm")
314
+
315
+ # Cross-modal insights
316
+ report.append("\n🧠 CROSS-MODAL INSIGHTS:")
317
+
318
+ # Quantum confinement
319
+ if 'tem' in results and 'uvvis' in results:
320
+ tem = results['tem']
321
+ uvvis = results['uvvis']
322
+ if tem['particle_count'] > 0 and uvvis['bandgap_eV'] > 0:
323
+ report.append(" β€’ Quantum confinement analysis available")
324
+
325
+ # Defect correlation
326
+ if 'xrd' in results and 'pl' in results:
327
+ xrd_disorder = results['xrd']['features']['avg_disorder']
328
+ if results['pl']['peaks']:
329
+ pl_fwhm = results['pl']['peaks'][0]['fwhm']
330
+ report.append(" β€’ XRD disorder and PL FWHM can be correlated for defect analysis")
331
+
332
+ report.append("\nπŸ’‘ RECOMMENDATIONS:")
333
+ report.append("β€’ Validate phase purity with Rietveld refinement")
334
+ report.append("β€’ Correlate particle size with magnetic/optical properties")
335
+ report.append("β€’ For thin films, consider substrate effects")
336
+
337
+ report.append("\n" + "=" * 60)
338
+ return "\n".join(report)
339
+
340
+ def generate_plots(self, results: Dict, output_dir: str = ".") -> List[str]:
341
+ """Generate publication-ready plots"""
342
+ sample_name = results.get('sample_name', 'sample')
343
+ plot_paths = []
344
+
345
+ # Create plots directory
346
+ os.makedirs(output_dir, exist_ok=True)
347
+
348
+ # XRD plot
349
+ if 'xrd' in results:
350
+ plt.figure(figsize=(8, 5))
351
+ plt.plot(results['xrd']['wavelength'], results['xrd']['intensity'], 'b-')
352
+ plt.title(f"XRD Pattern - {sample_name}")
353
+ plt.xlabel("2ΞΈ (degrees)")
354
+ plt.ylabel("Intensity (a.u.)")
355
+ xrd_path = os.path.join(output_dir, f"{sample_name}_xrd.png")
356
+ plt.savefig(xrd_path, dpi=300, bbox_inches='tight')
357
+ plt.close()
358
+ plot_paths.append(xrd_path)
359
+
360
+ # VSM plot
361
+ if 'vsm' in results:
362
+ plt.figure(figsize=(8, 5))
363
+ plt.plot(results['vsm']['H'], results['vsm']['M'], 'r-')
364
+ plt.title(f"VSM Hysteresis Loop - {sample_name}")
365
+ plt.xlabel("Magnetic Field H (Oe)")
366
+ plt.ylabel("Magnetization M (norm.)")
367
+ vsm_path = os.path.join(output_dir, f"{sample_name}_vsm.png")
368
+ plt.savefig(vsm_path, dpi=300, bbox_inches='tight')
369
+ plt.close()
370
+ plot_paths.append(vsm_path)
371
+
372
+ # UV-Vis plot
373
+ if 'uvvis' in results:
374
+ plt.figure(figsize=(8, 5))
375
+ plt.plot(results['uvvis']['wavelength'], results['uvvis']['absorption'], 'g-')
376
+ plt.title(f"UV-Vis Absorption - {sample_name}")
377
+ plt.xlabel("Wavelength (nm)")
378
+ plt.ylabel("Absorption (a.u.)")
379
+ uvvis_path = os.path.join(output_dir, f"{sample_name}_uvvis.png")
380
+ plt.savefig(uvvis_path, dpi=300, bbox_inches='tight')
381
+ plt.close()
382
+ plot_paths.append(uvvis_path)
383
+
384
+ # PL plot
385
+ if 'pl' in results:
386
+ plt.figure(figsize=(8, 5))
387
+ plt.plot(results['pl']['wavelength'], results['pl']['intensity'], 'm-')
388
+ plt.title(f"Photoluminescence - {sample_name}")
389
+ plt.xlabel("Wavelength (nm)")
390
+ plt.ylabel("Intensity (a.u.)")
391
+ pl_path = os.path.join(output_dir, f"{sample_name}_pl.png")
392
+ plt.savefig(pl_path, dpi=300, bbox_inches='tight')
393
+ plt.close()
394
+ plot_paths.append(pl_path)
395
+
396
+ # Correlation plot (if multiple modalities)
397
+ if 'tem' in results and 'uvvis' in results:
398
+ tem = results['tem']
399
+ uvvis = results['uvvis']
400
+ if tem['particle_count'] > 0 and uvvis['bandgap_eV'] > 0:
401
+ plt.figure(figsize=(8, 5))
402
+ plt.scatter([tem['mean_diameter_nm']], [uvvis['bandgap_eV']], s=100)
403
+ plt.title(f"Quantum Confinement - {sample_name}")
404
+ plt.xlabel("Particle Size (nm)")
405
+ plt.ylabel("Bandgap (eV)")
406
+ corr_path = os.path.join(output_dir, f"{sample_name}_confinement.png")
407
+ plt.savefig(corr_path, dpi=300, bbox_inches='tight')
408
+ plt.close()
409
+ plot_paths.append(corr_path)
410
+
411
+ return plot_paths
412
+
413
+ # Modal-specific analyzers
414
+ class XRDAnalyzer:
415
+ def compute_local_invariants(self, two_theta, intensity, window_size=10):
416
+ intensity_smooth = savgol_filter(intensity, window_length=min(21, len(intensity)//2 * 2 + 1), polyorder=2)
417
+ dI = np.gradient(intensity_smooth, two_theta)
418
+ d2I = np.gradient(dI, two_theta)
419
+
420
+ fiber = []
421
+ for i in range(len(two_theta)):
422
+ start = max(0, i - window_size)
423
+ end = min(len(two_theta), i + window_size + 1)
424
+ local_I = intensity[start:end]
425
+ local_var = np.var(local_I)
426
+ local_skew = np.mean((local_I - np.mean(local_I))**3) / (np.std(local_I)**3 + 1e-8)
427
+
428
+ fiber.append([
429
+ intensity[i], intensity_smooth[i], dI[i], d2I[i],
430
+ local_var, local_skew
431
+ ])
432
+ fiber = np.array(fiber)
433
+
434
+ invariants = np.zeros((len(two_theta), 6))
435
+ for i in range(len(two_theta)):
436
+ invariants[i] = [
437
+ abs(fiber[i, 3]), # sharpness
438
+ fiber[i, 4], # disorder
439
+ abs(fiber[i, 5]), # asymmetry
440
+ 1.0 / (fiber[i, 4] + 1e-8), # stability
441
+ abs(fiber[i, 2]), # gradient
442
+ fiber[i, 1] / (np.max(fiber[:, 1]) + 1e-8) # norm intensity
443
+ ]
444
+ return invariants
445
+
446
+ def extract_global_features(self, two_theta, intensity, local_invariants):
447
+ peaks, _ = find_peaks(intensity, height=np.max(intensity)*0.1, distance=20)
448
+ if len(peaks) == 0:
449
+ return {'crystallite_size': 0, 'microstrain': 0, 'amorphous_ratio': 1.0, 'n_peaks': 0, 'avg_disorder': 0}
450
+
451
+ fwhms = []
452
+ for p in peaks:
453
+ half_max = intensity[p] / 2.0
454
+ left = p
455
+ while left > 0 and intensity[left] > half_max:
456
+ left -= 1
457
+ right = p
458
+ while right < len(intensity) - 1 and intensity[right] > half_max:
459
+ right += 1
460
+ fwhm = two_theta[right] - two_theta[left]
461
+ fwhms.append(fwhm)
462
+
463
+ avg_fwhm = np.mean(fwhms)
464
+ theta_bragg = two_theta[peaks[0]] / 2.0
465
+ rel_size = 1.0 / (avg_fwhm * np.cos(np.radians(theta_bragg)) + 1e-8)
466
+ smooth_bg = gaussian_filter1d(intensity, sigma=50)
467
+ amorphous_ratio = np.mean(smooth_bg) / (np.mean(intensity) + 1e-8)
468
+ microstrain = np.std(fwhms) / (avg_fwhm + 1e-8)
469
+ avg_disorder = np.mean(local_invariants[:, 1])
470
+
471
+ return {
472
+ 'crystallite_size': rel_size,
473
+ 'microstrain': microstrain,
474
+ 'amorphous_ratio': amorphous_ratio,
475
+ 'n_peaks': len(peaks),
476
+ 'avg_disorder': avg_disorder
477
+ }
478
+
479
+ class VSMAnalyzer:
480
+ def compute_local_invariants(self, H, M, window_size=5):
481
+ dM = np.gradient(M, H)
482
+ d2M = np.gradient(dM, H)
483
+ fiber = []
484
+ for i in range(len(H)):
485
+ start = max(0, i - window_size)
486
+ end = min(len(H), i + window_size + 1)
487
+ local_M = M[start:end]
488
+ fiber.append([
489
+ M[i], dM[i], d2M[i],
490
+ np.std(local_M),
491
+ np.mean((local_M - np.mean(local_M))**3) / (np.std(local_M)**3 + 1e-8)
492
+ ])
493
+ fiber = np.array(fiber)
494
+
495
+ invariants = np.zeros((len(H), 6))
496
+ for i in range(len(H)):
497
+ # Symmetry breaking: |M(H) + M(-H)|
498
+ H_val = H[i]
499
+ M_val = M[i]
500
+ idx_neg = np.argmin(np.abs(H + H_val))
501
+ sym_break = abs(M_val + M[idx_neg])
502
+
503
+ invariants[i] = [
504
+ abs(fiber[i, 2]), # curvature
505
+ sym_break, # symmetry breaking
506
+ abs(fiber[i, 2]), # sharpness
507
+ fiber[i, 3], # noise
508
+ abs(fiber[i, 1]), # gradient
509
+ 1.0 / (fiber[i, 3] + 1e-8) # stability
510
+ ]
511
+ return invariants
512
+
513
+ def detect_magnetic_params(self, H, M):
514
+ asc_M = M[len(H)//2:]
515
+ asc_H = H[len(H)//2:]
516
+ zero_cross = np.where(np.diff(np.sign(asc_M)))[0]
517
+ Hc = asc_H[zero_cross[0]] if len(zero_cross) > 0 else 0
518
+ Mr = M[np.argmin(np.abs(H))]
519
+ return Hc, Mr
520
+
521
+ class UVVisAnalyzer:
522
+ def compute_local_invariants(self, wavelength, absorption, window_size=10):
523
+ intensity_smooth = savgol_filter(absorption, window_length=min(21, len(absorption)//2 * 2 + 1), polyorder=2)
524
+ dI = np.gradient(intensity_smooth, wavelength)
525
+ d2I = np.gradient(dI, wavelength)
526
+
527
+ fiber = []
528
+ for i in range(len(wavelength)):
529
+ start = max(0, i - window_size)
530
+ end = min(len(wavelength), i + window_size + 1)
531
+ local_I = absorption[start:end]
532
+ local_var = np.var(local_I)
533
+ local_skew = np.mean((local_I - np.mean(local_I))**3) / (np.std(local_I)**3 + 1e-8)
534
+
535
+ fiber.append([
536
+ absorption[i], intensity_smooth[i], dI[i], d2I[i],
537
+ local_var, local_skew
538
+ ])
539
+ fiber = np.array(fiber)
540
+
541
+ invariants = np.zeros((len(wavelength), 6))
542
+ for i in range(len(wavelength)):
543
+ invariants[i] = [
544
+ abs(fiber[i, 3]), # edge sharpness
545
+ fiber[i, 4], # disorder
546
+ abs(fiber[i, 5]), # asymmetry
547
+ 1.0 / (fiber[i, 4] + 1e-8), # stability
548
+ abs(fiber[i, 2]), # gradient
549
+ fiber[i, 1] # norm intensity
550
+ ]
551
+ return invariants
552
+
553
+ def estimate_bandgap(self, wavelength, absorption):
554
+ """Estimate Tauc bandgap for direct semiconductors"""
555
+ energy = 1240 / wavelength # eV (for nm)
556
+ alpha_hv_sq = (absorption * energy) ** 2
557
+
558
+ # Find absorption edge
559
+ edge_idx = np.argmax(absorption > 0.5 * np.max(absorption))
560
+ if edge_idx == 0:
561
+ return 0
562
+
563
+ start = max(0, edge_idx - 20)
564
+ end = min(len(energy), edge_idx + 20)
565
+ if end - start < 5:
566
+ return 0
567
+
568
+ # Linear fit in band edge region
569
+ try:
570
+ coeffs = np.polyfit(energy[start:end], alpha_hv_sq[start:end], 1)
571
+ bandgap = -coeffs[1] / coeffs[0] if coeffs[0] != 0 else 0
572
+ return max(0, bandgap)
573
+ except:
574
+ return 0
575
+
576
+ class PLAnalyzer:
577
+ def compute_local_invariants(self, wavelength, intensity, window_size=10):
578
+ intensity_smooth = savgol_filter(intensity, window_length=min(21, len(intensity)//2 * 2 + 1), polyorder=2)
579
+ dI = np.gradient(intensity_smooth, wavelength)
580
+ d2I = np.gradient(dI, wavelength)
581
+
582
+ fiber = []
583
+ for i in range(len(wavelength)):
584
+ start = max(0, i - window_size)
585
+ end = min(len(wavelength), i + window_size + 1)
586
+ local_I = intensity[start:end]
587
+ local_var = np.var(local_I)
588
+ local_skew = np.mean((local_I - np.mean(local_I))**3) / (np.std(local_I)**3 + 1e-8)
589
+
590
+ fiber.append([
591
+ intensity[i], intensity_smooth[i], dI[i], d2I[i],
592
+ local_var, local_skew
593
+ ])
594
+ fiber = np.array(fiber)
595
+
596
+ invariants = np.zeros((len(wavelength), 6))
597
+ for i in range(len(wavelength)):
598
+ invariants[i] = [
599
+ abs(fiber[i, 3]), # peak sharpness
600
+ fiber[i, 4], # disorder
601
+ abs(fiber[i, 5]), # asymmetry
602
+ 1.0 / (fiber[i, 4] + 1e-8), # stability
603
+ abs(fiber[i, 2]), # gradient
604
+ fiber[i, 1] # norm intensity
605
+ ]
606
+ return invariants
607
+
608
+ def extract_pl_peaks(self, wavelength, intensity):
609
+ """Extract peak positions, FWHM, intensity"""
610
+ peaks, props = find_peaks(intensity, height=np.max(intensity)*0.1, distance=20)
611
+ peak_info = []
612
+ for peak in peaks:
613
+ height = intensity[peak]
614
+ half_max = height / 2.0
615
+ left = peak
616
+ while left > 0 and intensity[left] > half_max:
617
+ left -= 1
618
+ right = peak
619
+ while right < len(intensity) - 1 and intensity[right] > half_max:
620
+ right += 1
621
+ fwhm = wavelength[right] - wavelength[left]
622
+ peak_info.append({
623
+ 'wavelength': float(wavelength[peak]),
624
+ 'intensity': float(height),
625
+ 'fwhm': float(fwhm)
626
+ })
627
+ return peak_info
app.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import tempfile
4
+ from core import LightweightAnalyzer
5
+ from dataset_utils import contribute_to_dataset
6
+
7
+ # Get HF token from environment (set in HF Spaces secrets)
8
+ HF_TOKEN = os.getenv("HF_TOKEN")
9
+ HF_DATASET_REPO = "your-username/multimodal-materials-dataset" # Change this!
10
+
11
+ analyzer = LightweightAnalyzer()
12
+
13
+ def process_files(xrd_file, vsm_file, uvvis_file, pl_file, sample_name, contribute):
14
+ try:
15
+ results = {"sample_name": sample_name}
16
+
17
+ # Process each modality
18
+ if xrd_file is not None:
19
+ x, y = analyzer.load_csv(xrd_file.name)
20
+ results['xrd'] = analyzer.analyze_xrd(x, y)
21
+
22
+ if vsm_file is not None:
23
+ x, y = analyzer.load_csv(vsm_file.name)
24
+ results['vsm'] = analyzer.analyze_vsm(x, y)
25
+
26
+ if uvvis_file is not None:
27
+ x, y = analyzer.load_csv(uvvis_file.name)
28
+ results['uvvis'] = analyzer.analyze_uvvis(x, y)
29
+
30
+ if pl_file is not None:
31
+ x, y = analyzer.load_csv(pl_file.name)
32
+ results['pl'] = analyzer.analyze_pl(x, y)
33
+
34
+ # Generate report
35
+ report = analyzer.generate_report(results)
36
+
37
+ # Contribute to dataset
38
+ if contribute and HF_TOKEN:
39
+ success, msg = contribute_to_dataset(
40
+ results, sample_name, HF_DATASET_REPO, HF_TOKEN
41
+ )
42
+ if success:
43
+ report += f"\n\nβœ… {msg}"
44
+ else:
45
+ report += f"\n\n⚠️ {msg}"
46
+ elif contribute:
47
+ report += "\n\nℹ️ Dataset contribution requires HF token (not available in public demo)."
48
+
49
+ # Generate plots
50
+ with tempfile.TemporaryDirectory() as tmp_dir:
51
+ plot_paths = analyzer.generate_plots(results, sample_name, tmp_dir)
52
+ return report, plot_paths
53
+
54
+ except Exception as e:
55
+ return f"Error: {str(e)}", []
56
+
57
+ # Gradio interface
58
+ with gr.Blocks(title="Materials Analyzer") as demo:
59
+ gr.Markdown("# πŸ”¬ Multi-Modal Materials Analyzer")
60
+ gr.Markdown("Lightweight analysis for XRD, VSM, UV-Vis, and PL data")
61
+
62
+ with gr.Row():
63
+ with gr.Column():
64
+ sample_name = gr.Textbox(label="Sample Name", value="Sample1")
65
+
66
+ xrd_file = gr.File(label="XRD CSV", file_types=[".csv"])
67
+ vsm_file = gr.File(label="VSM CSV", file_types=[".csv"])
68
+ uvvis_file = gr.File(label="UV-Vis CSV", file_types=[".csv"])
69
+ pl_file = gr.File(label="PL CSV", file_types=[".csv"])
70
+
71
+ contribute = gr.Checkbox(
72
+ label="Contribute results to public dataset",
73
+ value=False,
74
+ interactive=bool(HF_TOKEN)
75
+ )
76
+
77
+ submit_btn = gr.Button("Analyze", variant="primary")
78
+
79
+ with gr.Column():
80
+ report = gr.Textbox(label="Analysis Report", lines=20)
81
+ plots = gr.Gallery(label="Results", columns=2)
82
+
83
+ submit_btn.click(
84
+ process_files,
85
+ [xrd_file, vsm_file, uvvis_file, pl_file, sample_name, contribute],
86
+ [report, plots]
87
+ )
88
+
89
+ gr.Markdown("### ℹ️ Instructions")
90
+ gr.Markdown("""
91
+ **CSV Format:**
92
+ - XRD: columns `2theta`, `intensity`
93
+ - VSM: columns `H`, `M`
94
+ - UV-Vis: columns `wavelength`, `absorption`
95
+ - PL: columns `wavelength`, `intensity`
96
+
97
+ **Note:** This is a lightweight demo. For full analysis with TEM and advanced features,
98
+ run locally with the complete pipeline.
99
+ """)
100
+
101
+ if __name__ == "__main__":
102
+ demo.launch()
core.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ from scipy.signal import find_peaks
4
+ from scipy.ndimage import gaussian_filter1d
5
+ import matplotlib
6
+ matplotlib.use('Agg')
7
+ import matplotlib.pyplot as plt
8
+
9
+ class LightweightAnalyzer:
10
+ """Lightweight analyzer that works on Hugging Face Spaces"""
11
+
12
+ def __init__(self):
13
+ # Predefined reference patterns (no internet needed)
14
+ self.reference_phases = {
15
+ 'Fe3O4': {'peaks': [30.1, 35.5, 43.1, 53.4, 57.0, 62.6]},
16
+ 'CoFe2O4': {'peaks': [30.2, 35.6, 43.2, 53.5, 57.1, 62.7]},
17
+ 'TiO2_anatase': {'peaks': [25.3, 37.8, 48.0, 53.9, 55.1, 62.7]},
18
+ 'TiO2_rutile': {'peaks': [27.4, 36.1, 41.2, 54.3, 56.6, 69.0]}
19
+ }
20
+
21
+ def load_csv(self, file_path):
22
+ """Load CSV with auto column detection"""
23
+ df = pd.read_csv(file_path)
24
+ cols = [c.lower() for c in df.columns]
25
+
26
+ # X-axis
27
+ if 'wavelength' in cols:
28
+ x_col = df.columns[cols.index('wavelength')]
29
+ elif '2theta' in cols:
30
+ x_col = df.columns[cols.index('2theta')]
31
+ elif 'h' in cols:
32
+ x_col = df.columns[cols.index('h')]
33
+ else:
34
+ x_col = df.columns[0]
35
+
36
+ # Y-axis
37
+ if 'intensity' in cols:
38
+ y_col = df.columns[cols.index('intensity')]
39
+ elif 'm' in cols:
40
+ y_col = df.columns[cols.index('m')]
41
+ elif 'absorption' in cols:
42
+ y_col = df.columns[cols.index('absorption')]
43
+ else:
44
+ y_col = df.columns[1]
45
+
46
+ x = df[x_col].values.astype(float)
47
+ y = df[y_col].values.astype(float)
48
+ valid = np.isfinite(x) & np.isfinite(y)
49
+ return x[valid], y[valid]
50
+
51
+ def analyze_xrd(self, x, y):
52
+ """Lightweight XRD analysis"""
53
+ # Find peaks
54
+ peaks, _ = find_peaks(y, height=np.max(y)*0.1, distance=10)
55
+ peak_positions = x[peaks].tolist()
56
+
57
+ # Phase matching (simple nearest neighbor)
58
+ best_match = "Unknown"
59
+ best_score = 0
60
+ for phase, ref in self.reference_phases.items():
61
+ score = 0
62
+ for ref_peak in ref['peaks']:
63
+ if any(abs(ref_peak - p) < 2.0 for p in peak_positions):
64
+ score += 1
65
+ if score > best_score:
66
+ best_score = score
67
+ best_match = phase
68
+
69
+ # Estimate crystallite size (simplified Scherrer)
70
+ if len(peaks) > 0:
71
+ # Estimate FWHM of strongest peak
72
+ main_peak = peaks[np.argmax(y[peaks])]
73
+ half_max = y[main_peak] / 2
74
+ left = main_peak
75
+ while left > 0 and y[left] > half_max:
76
+ left -= 1
77
+ right = main_peak
78
+ while right < len(y)-1 and y[right] > half_max:
79
+ right += 1
80
+ fwhm = x[right] - x[left] if right > left else 1.0
81
+ theta = x[main_peak] / 2
82
+ size = 0.9 * 1.54 / (fwhm * np.cos(np.radians(theta)) * np.pi/180)
83
+ else:
84
+ size = 0
85
+
86
+ return {
87
+ 'peaks': peak_positions,
88
+ 'phase': best_match,
89
+ 'crystallite_size_nm': float(size),
90
+ 'amorphous_ratio': float(np.mean(gaussian_filter1d(y, sigma=50)) / np.mean(y))
91
+ }
92
+
93
+ def analyze_vsm(self, x, y):
94
+ """Lightweight VSM analysis"""
95
+ # Normalize
96
+ y = y / np.max(np.abs(y))
97
+
98
+ # Coercivity
99
+ mid = len(x) // 2
100
+ asc_y = y[mid:]
101
+ asc_x = x[mid:]
102
+ zero_cross = np.where(np.diff(np.sign(asc_y)))[0]
103
+ Hc = float(asc_x[zero_cross[0]]) if len(zero_cross) > 0 else 0.0
104
+
105
+ # Remanence
106
+ zero_idx = np.argmin(np.abs(x))
107
+ Mr = float(y[zero_idx])
108
+
109
+ return {'Hc': Hc, 'Mr': Mr}
110
+
111
+ def analyze_uvvis(self, x, y):
112
+ """Lightweight UV-Vis analysis"""
113
+ # Normalize
114
+ y = y / np.max(y)
115
+
116
+ # Find absorption edge (80% of max)
117
+ edge_idx = np.argmax(y > 0.8 * np.max(y))
118
+ if edge_idx == 0:
119
+ edge_wl = x[-1]
120
+ else:
121
+ edge_wl = x[edge_idx]
122
+
123
+ # Estimate bandgap
124
+ energy = 1240 / edge_wl
125
+ return {'bandgap_eV': float(energy), 'edge_wavelength_nm': float(edge_wl)}
126
+
127
+ def analyze_pl(self, x, y):
128
+ """Lightweight PL analysis"""
129
+ # Normalize
130
+ y = y / np.max(y)
131
+
132
+ # Find main peak
133
+ peaks, _ = find_peaks(y, height=np.max(y)*0.1, distance=10)
134
+ if len(peaks) > 0:
135
+ main_peak = peaks[np.argmax(y[peaks])]
136
+ peak_wl = float(x[main_peak])
137
+
138
+ # Estimate FWHM
139
+ half_max = y[main_peak] / 2
140
+ left = main_peak
141
+ while left > 0 and y[left] > half_max:
142
+ left -= 1
143
+ right = main_peak
144
+ while right < len(y)-1 and y[right] > half_max:
145
+ right += 1
146
+ fwhm = float(x[right] - x[left]) if right > left else 0.0
147
+ else:
148
+ peak_wl = 0.0
149
+ fwhm = 0.0
150
+
151
+ return {'peak_wavelength_nm': peak_wl, 'fwhm_nm': fwhm}
152
+
153
+ def generate_report(self, results):
154
+ """Generate analysis report"""
155
+ lines = []
156
+ lines.append("=" * 50)
157
+ lines.append("πŸ”¬ MULTI-MODAL MATERIALS ANALYSIS")
158
+ lines.append("=" * 50)
159
+
160
+ if 'xrd' in results:
161
+ xrd = results['xrd']
162
+ lines.append(f"\nπŸ“Š XRD RESULTS:")
163
+ lines.append(f" β€’ Identified phase: {xrd['phase']}")
164
+ lines.append(f" β€’ Crystallite size: {xrd['crystallite_size_nm']:.1f} nm")
165
+ lines.append(f" β€’ Amorphous ratio: {xrd['amorphous_ratio']:.3f}")
166
+
167
+ if 'vsm' in results:
168
+ vsm = results['vsm']
169
+ lines.append(f"\n🧲 VSM RESULTS:")
170
+ lines.append(f" β€’ Coercivity (Hc): {vsm['Hc']:.1f} Oe")
171
+ lines.append(f" β€’ Remanence (Mr): {vsm['Mr']:.3f}")
172
+
173
+ if 'uvvis' in results:
174
+ uvvis = results['uvvis']
175
+ lines.append(f"\n🌈 UV-VIS RESULTS:")
176
+ lines.append(f" β€’ Bandgap: {uvvis['bandgap_eV']:.2f} eV")
177
+ lines.append(f" β€’ Absorption edge: {uvvis['edge_wavelength_nm']:.1f} nm")
178
+
179
+ if 'pl' in results:
180
+ pl = results['pl']
181
+ lines.append(f"\nπŸ’‘ PL RESULTS:")
182
+ lines.append(f" β€’ Emission peak: {pl['peak_wavelength_nm']:.1f} nm")
183
+ lines.append(f" β€’ FWHM: {pl['fwhm_nm']:.1f} nm")
184
+
185
+ lines.append("\nπŸ’‘ NOTE: This is a lightweight analysis.")
186
+ lines.append("For advanced analysis, use local installation.")
187
+ lines.append("=" * 50)
188
+
189
+ return "\n".join(lines)
190
+
191
+ def generate_plots(self, results, sample_name, output_dir="."):
192
+ """Generate plots"""
193
+ import os
194
+ os.makedirs(output_dir, exist_ok=True)
195
+ plots = []
196
+
197
+ if 'xrd' in results:
198
+ plt.figure(figsize=(6, 4))
199
+ # We don't have raw data, so skip plotting
200
+ plt.text(0.5, 0.5, "XRD: Phase identified", ha='center', va='center')
201
+ plt.title(f"XRD - {sample_name}")
202
+ path = os.path.join(output_dir, f"{sample_name}_xrd.png")
203
+ plt.savefig(path, dpi=150, bbox_inches='tight')
204
+ plt.close()
205
+ plots.append(path)
206
+
207
+ # Similar for other modalities (simplified)
208
+ for modality in ['vsm', 'uvvis', 'pl']:
209
+ if modality in results:
210
+ plt.figure(figsize=(6, 4))
211
+ plt.text(0.5, 0.5, f"{modality.upper()}: Analyzed", ha='center', va='center')
212
+ plt.title(f"{modality.upper()} - {sample_name}")
213
+ path = os.path.join(output_dir, f"{sample_name}_{modality}.png")
214
+ plt.savefig(path, dpi=150, bbox_inches='tight')
215
+ plt.close()
216
+ plots.append(path)
217
+
218
+ return plots
dataset_utils.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ from huggingface_hub import HfApi
5
+ from huggingface_hub.utils import HfHubHTTPError
6
+
7
+ def contribute_to_dataset(results, sample_name, repo_id, token=None):
8
+ """
9
+ Safely contribute to dataset with error handling
10
+ """
11
+ try:
12
+ # Prepare anonymized entry
13
+ entry = {
14
+ "id": str(uuid.uuid4()),
15
+ "sample_name": sample_name,
16
+ "modalities": [k for k in results.keys() if k != 'sample_name'],
17
+ "results": {k: v for k, v in results.items() if k != 'sample_name'}
18
+ }
19
+
20
+ # Save locally first
21
+ os.makedirs("tmp", exist_ok=True)
22
+ local_path = f"tmp/{entry['id']}.json"
23
+ with open(local_path, "w") as f:
24
+ json.dump(entry, f)
25
+
26
+ # Upload to HF
27
+ api = HfApi(token=token)
28
+ api.upload_file(
29
+ path_or_fileobj=local_path,
30
+ path_in_repo=f"entries/{entry['id']}.json",
31
+ repo_id=repo_id,
32
+ repo_type="dataset",
33
+ commit_message=f"Add sample: {sample_name}"
34
+ )
35
+
36
+ return True, "Successfully contributed to dataset!"
37
+ except HfHubHTTPError as e:
38
+ if "401" in str(e):
39
+ return False, "Authentication required to contribute to dataset."
40
+ else:
41
+ return False, f"Dataset contribution failed: {str(e)}"
42
+ except Exception as e:
43
+ return False, f"Unexpected error: {str(e)}"
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==4.40.0
2
+ numpy==1.26.4
3
+ pandas==2.2.2
4
+ scikit-learn==1.5.0
5
+ scipy==1.13.1
6
+ matplotlib==3.9.0
7
+ Pillow==10.3.0
8
+ huggingface-hub==0.23.0
9
+ requests==2.31.0
10
+ # Removed: pymatgen, opencv, scikit-image (too heavy for HF Spaces)