import numpy as np import pandas as pd from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt class LightweightAnalyzer: """Lightweight analyzer that works on Hugging Face Spaces""" def __init__(self): # Predefined reference patterns (no internet needed) self.reference_phases = { 'Fe3O4': {'peaks': [30.1, 35.5, 43.1, 53.4, 57.0, 62.6]}, 'CoFe2O4': {'peaks': [30.2, 35.6, 43.2, 53.5, 57.1, 62.7]}, 'TiO2_anatase': {'peaks': [25.3, 37.8, 48.0, 53.9, 55.1, 62.7]}, 'TiO2_rutile': {'peaks': [27.4, 36.1, 41.2, 54.3, 56.6, 69.0]} } def load_csv(self, file_path): """Load CSV with auto column detection""" df = pd.read_csv(file_path) cols = [c.lower() for c in df.columns] # X-axis if 'wavelength' in cols: x_col = df.columns[cols.index('wavelength')] elif '2theta' in cols: x_col = df.columns[cols.index('2theta')] elif 'h' in cols: x_col = df.columns[cols.index('h')] else: x_col = df.columns[0] # Y-axis if 'intensity' in cols: y_col = df.columns[cols.index('intensity')] elif 'm' in cols: y_col = df.columns[cols.index('m')] elif 'absorption' in cols: y_col = df.columns[cols.index('absorption')] else: y_col = df.columns[1] x = df[x_col].values.astype(float) y = df[y_col].values.astype(float) valid = np.isfinite(x) & np.isfinite(y) return x[valid], y[valid] def analyze_xrd(self, x, y): """Lightweight XRD analysis""" # Find peaks peaks, _ = find_peaks(y, height=np.max(y)*0.1, distance=10) peak_positions = x[peaks].tolist() # Phase matching (simple nearest neighbor) best_match = "Unknown" best_score = 0 for phase, ref in self.reference_phases.items(): score = 0 for ref_peak in ref['peaks']: if any(abs(ref_peak - p) < 2.0 for p in peak_positions): score += 1 if score > best_score: best_score = score best_match = phase # Estimate crystallite size (simplified Scherrer) if len(peaks) > 0: # Estimate FWHM of strongest peak main_peak = peaks[np.argmax(y[peaks])] half_max = y[main_peak] / 2 left = main_peak while left > 0 and y[left] > half_max: left -= 1 right = main_peak while right < len(y)-1 and y[right] > half_max: right += 1 fwhm = x[right] - x[left] if right > left else 1.0 theta = x[main_peak] / 2 size = 0.9 * 1.54 / (fwhm * np.cos(np.radians(theta)) * np.pi/180) else: size = 0 return { 'peaks': peak_positions, 'phase': best_match, 'crystallite_size_nm': float(size), 'amorphous_ratio': float(np.mean(gaussian_filter1d(y, sigma=50)) / np.mean(y)) } def analyze_vsm(self, x, y): """Lightweight VSM analysis""" # Normalize y = y / np.max(np.abs(y)) # Coercivity mid = len(x) // 2 asc_y = y[mid:] asc_x = x[mid:] zero_cross = np.where(np.diff(np.sign(asc_y)))[0] Hc = float(asc_x[zero_cross[0]]) if len(zero_cross) > 0 else 0.0 # Remanence zero_idx = np.argmin(np.abs(x)) Mr = float(y[zero_idx]) return {'Hc': Hc, 'Mr': Mr} def analyze_uvvis(self, x, y): """Lightweight UV-Vis analysis""" # Normalize y = y / np.max(y) # Find absorption edge (80% of max) edge_idx = np.argmax(y > 0.8 * np.max(y)) if edge_idx == 0: edge_wl = x[-1] else: edge_wl = x[edge_idx] # Estimate bandgap energy = 1240 / edge_wl return {'bandgap_eV': float(energy), 'edge_wavelength_nm': float(edge_wl)} def analyze_pl(self, x, y): """Lightweight PL analysis""" # Normalize y = y / np.max(y) # Find main peak peaks, _ = find_peaks(y, height=np.max(y)*0.1, distance=10) if len(peaks) > 0: main_peak = peaks[np.argmax(y[peaks])] peak_wl = float(x[main_peak]) # Estimate FWHM half_max = y[main_peak] / 2 left = main_peak while left > 0 and y[left] > half_max: left -= 1 right = main_peak while right < len(y)-1 and y[right] > half_max: right += 1 fwhm = float(x[right] - x[left]) if right > left else 0.0 else: peak_wl = 0.0 fwhm = 0.0 return {'peak_wavelength_nm': peak_wl, 'fwhm_nm': fwhm} def generate_report(self, results): """Generate analysis report""" lines = [] lines.append("=" * 50) lines.append("šŸ”¬ MULTI-MODAL MATERIALS ANALYSIS") lines.append("=" * 50) if 'xrd' in results: xrd = results['xrd'] lines.append(f"\nšŸ“Š XRD RESULTS:") lines.append(f" • Identified phase: {xrd['phase']}") lines.append(f" • Crystallite size: {xrd['crystallite_size_nm']:.1f} nm") lines.append(f" • Amorphous ratio: {xrd['amorphous_ratio']:.3f}") if 'vsm' in results: vsm = results['vsm'] lines.append(f"\n🧲 VSM RESULTS:") lines.append(f" • Coercivity (Hc): {vsm['Hc']:.1f} Oe") lines.append(f" • Remanence (Mr): {vsm['Mr']:.3f}") if 'uvvis' in results: uvvis = results['uvvis'] lines.append(f"\n🌈 UV-VIS RESULTS:") lines.append(f" • Bandgap: {uvvis['bandgap_eV']:.2f} eV") lines.append(f" • Absorption edge: {uvvis['edge_wavelength_nm']:.1f} nm") if 'pl' in results: pl = results['pl'] lines.append(f"\nšŸ’” PL RESULTS:") lines.append(f" • Emission peak: {pl['peak_wavelength_nm']:.1f} nm") lines.append(f" • FWHM: {pl['fwhm_nm']:.1f} nm") lines.append("\nšŸ’” NOTE: This is a lightweight analysis.") lines.append("For advanced analysis, use local installation.") lines.append("=" * 50) return "\n".join(lines) def generate_plots(self, results, sample_name, output_dir="."): """Generate plots""" import os os.makedirs(output_dir, exist_ok=True) plots = [] if 'xrd' in results: plt.figure(figsize=(6, 4)) # We don't have raw data, so skip plotting plt.text(0.5, 0.5, "XRD: Phase identified", ha='center', va='center') plt.title(f"XRD - {sample_name}") path = os.path.join(output_dir, f"{sample_name}_xrd.png") plt.savefig(path, dpi=150, bbox_inches='tight') plt.close() plots.append(path) # Similar for other modalities (simplified) for modality in ['vsm', 'uvvis', 'pl']: if modality in results: plt.figure(figsize=(6, 4)) plt.text(0.5, 0.5, f"{modality.upper()}: Analyzed", ha='center', va='center') plt.title(f"{modality.upper()} - {sample_name}") path = os.path.join(output_dir, f"{sample_name}_{modality}.png") plt.savefig(path, dpi=150, bbox_inches='tight') plt.close() plots.append(path) return plots