# Auto-generated Script
# Task: Fit each pixel's spectrum with a Lorentzian or Voigt peak model (using lmfit) to extract the LSPR peak center position, FWHM (linewidth/damping), and peak amplitude. The energy range 0.2–1.13 eV contains a single dominant plasmon peak that shifts spatially. Use a Lorentzian model (physically appropriate for a damped plasmon oscillation): center bounded [0.25, 0.65] eV, FWHM bounded [0.05, 0.5] eV, amplitude > 0. Add a linear background. Generate spatial maps of: (1) peak center energy, (2) FWHM, and (3) integrated peak area. These maps will reveal spatial variations in carrier concentration and dielectric coupling across the nanocrystal array far more directly than NMF components.

def analyze_feature(data, axis):
    ny, nx, ne = data.shape
    n_pixels = ny * nx
    spectra = data.reshape(n_pixels, ne)

    # Light smoothing for robust initial parameter estimation
    smoothed = gaussian_filter(spectra, sigma=(0, 2))

    # Lorentzian + linear background model
    # L(x) = A * (gamma/2)^2 / ((x - x0)^2 + (gamma/2)^2) + slope*x + intercept
    # where A = peak height, gamma = FWHM
    def model(x, center, gamma, amplitude, slope, intercept):
        hg = gamma * 0.5
        hg2 = hg * hg
        lor = amplitude * hg2 / ((x - center)**2 + hg2)
        return lor + slope * x + intercept

    # Output arrays
    center_map = np.full(n_pixels, np.nan)
    fwhm_map = np.full(n_pixels, np.nan)
    area_map = np.full(n_pixels, np.nan)

    # Bounds: [center, gamma, amplitude, slope, intercept]
    lb = np.array([0.25, 0.05, 0.0, -np.inf, -np.inf])
    ub = np.array([0.65, 0.50, np.inf, np.inf, np.inf])

    # Pre-compute peak search region mask
    peak_mask = (axis >= 0.25) & (axis <= 0.65)
    axis_peak = axis[peak_mask]

    # Background estimation edge width
    n_bg = max(20, ne // 20)
    x_left_mean = np.mean(axis[:n_bg])
    x_right_mean = np.mean(axis[-n_bg:])
    dx_bg = x_right_mean - x_left_mean

    for i in range(n_pixels):
        spec = spectra[i]
        smo = smoothed[i]

        # Find initial peak center from smoothed spectrum in bounded region
        smo_peak = smo[peak_mask]
        if len(smo_peak) == 0:
            continue

        idx_max = np.argmax(smo_peak)
        center0 = axis_peak[idx_max]

        # Estimate linear background from spectrum edges (median for robustness)
        bg_left = np.median(smo[:n_bg])
        bg_right = np.median(smo[-n_bg:])

        slope0 = (bg_right - bg_left) / dx_bg if dx_bg != 0 else 0.0
        intercept0 = bg_left - slope0 * x_left_mean

        # Amplitude = peak value above estimated background
        bg_at_peak = slope0 * center0 + intercept0
        amp0 = max(smo_peak[idx_max] - bg_at_peak, 1e-10)

        # Estimate FWHM from half-max width of the peak region
        peak_above_bg = smo_peak - (slope0 * axis_peak + intercept0)
        half_max_val = amp0 / 2.0
        above_half = peak_above_bg > half_max_val
        if np.sum(above_half) > 1:
            indices_above = np.where(above_half)[0]
            gamma0 = axis_peak[indices_above[-1]] - axis_peak[indices_above[0]]
            gamma0 = float(np.clip(gamma0, 0.06, 0.45))
        else:
            gamma0 = 0.15

        p0 = [center0, gamma0, amp0, slope0, intercept0]

        try:
            popt, _ = curve_fit(model, axis, spec, p0=p0,
                                bounds=(lb, ub), maxfev=5000,
                                method='trf')
            center_map[i] = popt[0]
            fwhm_map[i] = popt[1]
            # Analytical integral of Lorentzian: A * pi * gamma / 2
            area_map[i] = popt[2] * np.pi * popt[1] * 0.5
        except Exception:
            # Fallback with simpler initial guess
            p0_fb = [0.45, 0.15, max(np.max(smo) * 0.3, 1e-10), 0.0, np.median(smo)]
            try:
                popt, _ = curve_fit(model, axis, spec, p0=p0_fb,
                                    bounds=(lb, ub), maxfev=5000,
                                    method='trf')
                center_map[i] = popt[0]
                fwhm_map[i] = popt[1]
                area_map[i] = popt[2] * np.pi * popt[1] * 0.5
            except Exception:
                pass  # Leave as NaN

    # Reshape to spatial dimensions
    center_map = center_map.reshape(ny, nx)
    fwhm_map = fwhm_map.reshape(ny, nx)
    area_map = area_map.reshape(ny, nx)

    return {
        "maps": {
            "LSPR_Peak_Center": center_map,
            "LSPR_FWHM": fwhm_map,
            "LSPR_Integrated_Area": area_map
        },
        "units": {
            "LSPR_Peak_Center": "eV",
            "LSPR_FWHM": "eV",
            "LSPR_Integrated_Area": "eV·counts"
        },
        "description": "Pixel-wise Lorentzian + linear background fitting of the LSPR plasmon peak (0.2-1.13 eV). Peak center energy maps spatial variations in free-carrier concentration via the Drude model (omega_p^2 proportional to n_e). FWHM maps plasmon damping arising from dielectric losses, grain boundary scattering, and finite-size effects. Integrated Lorentzian area (A*pi*gamma/2) reflects oscillator strength and near-field electromagnetic coupling across the nanocrystal array."
    }
