Calculations/Data-Analyzer/+Analyzer/fitTwoGaussianCurvesToAngularSpectralDistribution.m

440 lines
19 KiB
Matlab
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function [fitResults, rawCurves] = fitTwoGaussianCurvesToAngularSpectralDistribution(S_theta_all, theta_vals, varargin)
%% fitTwoGaussianCurvesToAngularSpectralDistribution
% Fits a two-Gaussian model to multiple spectral curves from θ=0 up to
% the first secondary peak ≥50% of the primary peak, within 0π/2 radians.
%
% Author: Karthik
% Date: 2025-10-06
% Version: 1.1
%
% Inputs:
% S_theta_all - 1xN cell array of spectral curves
% theta_vals - vector of corresponding theta values (radians)
%
% Optional name-value pairs:
% 'MaxTheta' - Maximum theta for search (default: pi/2)
% 'ResidualThreshold' - Max relative deviation allowed (default: 0.3)
% 'PositionThreshold' - Minimum angular separation between main and
% secondary peaks (default: 0.1 rad)
% 'AmplitudeThreshold' - Minimum fraction of primary peak amplitude for secondary peak (default: 0.5)
%
% Output:
% fitResults - struct array with fields:
% .pFit, .thetaFit, .xFit, .yFit, .thetaFine, .isValid, .fitMaxTheta
% --- Parse optional inputs ---
p = inputParser;
addParameter(p, 'MaxTheta', pi/2, @(x) isnumeric(x) && isscalar(x));
addParameter(p, 'ResidualThreshold', 0.3, @(x) isnumeric(x) && isscalar(x));
addParameter(p, 'PositionThreshold', 0.1, @(x) isnumeric(x) && isscalar(x));
addParameter(p, 'AmplitudeThreshold', 0.5, @(x) isnumeric(x) && isscalar(x));
addParameter(p, 'RecenterCurves', true, @islogical);
parse(p, varargin{:});
opts = p.Results;
Ncurves = numel(S_theta_all);
fitResults = struct('pFit',[],'thetaFit',[],'xFit',[],'yFit',[],...
'thetaFine',[],'isValid',[], 'residual',[], 'maxAbs',[], 'R2',[], 'fitMaxTheta',[]);
rawCurves = struct('x', cell(1, Ncurves), 'theta', cell(1, Ncurves));
if opts.RecenterCurves
% --- Preprocess curves: shift first peak to zero without wrapping ---
S_theta_all_shifted = cell(size(S_theta_all));
for k = 1:Ncurves
curve = S_theta_all{k};
idx_range = find(theta_vals >= 0 & theta_vals <= opts.MaxTheta);
if isempty(idx_range)
warning('Curve %d has no data in [0, MaxTheta]; skipping recentring.', k);
S_theta_all_shifted{k} = curve;
continue;
end
% --- Find all peaks in 0MaxTheta range and pick largest ---
[pks, locs] = findpeaks(curve(idx_range));
if isempty(pks)
[~, local_max_idx] = max(curve(idx_range));
else
[~, max_idx_in_peaks] = max(pks);
local_max_idx = locs(max_idx_in_peaks);
end
peak_idx = idx_range(local_max_idx);
% --- Determine cutoff index corresponding to π/2 beyond that peak ---
theta_peak = theta_vals(peak_idx);
theta_end = theta_peak + pi/2;
if theta_end > max(theta_vals)
theta_end = max(theta_vals);
end
idx_end = find(theta_vals <= theta_end, 1, 'last');
% --- Extract segment from main peak up to π/2 after ---
S_theta_all_shifted{k} = curve(peak_idx:idx_end);
end
% --- Pad curves to same length ---
Npoints_shifted = max(cellfun(@numel, S_theta_all_shifted));
for k = 1:Ncurves
len = numel(S_theta_all_shifted{k});
if len < Npoints_shifted
S_theta_all_shifted{k} = [S_theta_all_shifted{k}, nan(1, Npoints_shifted-len)];
end
end
theta_recentered = linspace(0, pi/2, Npoints_shifted);
else
S_theta_all_shifted = S_theta_all;
theta_recentered = theta_vals;
end
for k = 1:Ncurves
x = S_theta_all_shifted{k};
theta = theta_recentered;
if any(isnan(x))
warning('Curve %d has NaNs, skipping.', k);
fitResults(k) = fillNaNStruct();
continue;
end
mask = theta>=0 & theta<=opts.MaxTheta;
x = x(mask);
theta = theta(mask);
x = x / x(1);
% --- Store raw data for plotting ---
rawCurves(k).x = x;
rawCurves(k).theta = theta;
try
% --- Filter 1: Ensure curve covers full 0MaxTheta range ---
thetaRange = max(theta) - min(theta);
if thetaRange < 0.9 * opts.MaxTheta
warning('Curve %d excluded: does not span full 0MaxTheta range.', k);
fitResults(k) = fillZeroStruct(theta, opts.MaxTheta);
continue;
end
if numel(theta) < 8
warning('Curve %d too short (<8 points), skipping.', k);
fitResults(k) = fillNaNStruct();
continue;
end
xSmooth = smooth(x, 5, 'moving');
curveAmp = max(xSmooth) - min(xSmooth);
%% 1) Attempt primary detection via findpeaks
minProm = opts.AmplitudeThreshold * curveAmp;
[pk, locIdx] = findpeaks(xSmooth, 'MinPeakProminence', minProm);
thetaPeaks = theta(locIdx);
foundSecondary = false;
fitMaxTheta = [];
secondaryAmp = [];
secondaryIdx = find(thetaPeaks > opts.PositionThreshold, 1, 'first');
if ~isempty(secondaryIdx)
foundSecondary = true;
fitMaxTheta = thetaPeaks(secondaryIdx);
secondaryAmp = pk(secondaryIdx);
end
%% 2) If secondary found, fit constrained two-Gaussian directly
if foundSecondary
twoGauss = @(p,theta) ...
1.0 * exp(-0.5*((theta - p(1))/max(p(2),1e-6)).^2) + ...
p(3) * exp(-0.5*((theta - p(4))/max(p(5),1e-6)).^2) + ...
p(6); % offset term
mu1_guess = 0;
sigma1_guess = 0.1 * fitMaxTheta;
A2_guess = secondaryAmp;
mu2_guess = fitMaxTheta;
sigma2_guess = fitMaxTheta / 3;
p0 = [mu1_guess, sigma1_guess, A2_guess, mu2_guess, sigma2_guess, 0];
lb = [0, 1e-6, 0, max(fitMaxTheta - 0.1, 0), 1e-6, -0.5];
ub = [fitMaxTheta/4, fitMaxTheta, 2, min(fitMaxTheta + 0.1, opts.MaxTheta), fitMaxTheta, 0.5];
optsLSQ = optimoptions('lsqcurvefit','Display','off', ...
'MaxFunctionEvaluations',1e4,'MaxIterations',1e4);
try
pFit = lsqcurvefit(twoGauss, p0, theta, x, lb, ub, optsLSQ);
% --- Handle missing or weak secondary peak ---
if pFit(3) <= opts.AmplitudeThreshold
% amplitude too small → discard secondary
pFit(3) = 0;
pFit(4:5) = NaN;
end
% --- Map to canonical format with primary amplitude = 1 ---
pCanonical = [1.0, pFit(1), pFit(2), pFit(3), pFit(4), pFit(5), pFit(6)];
thetaFine = linspace(0, opts.MaxTheta, 500);
yFit = 1.0*exp(-0.5*((thetaFine - pCanonical(2))/max(pCanonical(3),1e-6)).^2) + ...
pCanonical(4)*exp(-0.5*((thetaFine - pCanonical(5))/max(pCanonical(6),1e-6)).^2) + ...
pCanonical(7);
% --- Localized residual diagnostics ---
yFit_raw = twoGauss(pFit, theta(:));
r = x(:) - yFit_raw;
% --- Global RMS ---
fitResults(k).residual = sqrt(mean(r.^2));
% --- Max absolute residual ---
fitResults(k).maxAbs = max(abs(r));
% --- R^2 ---
SS_res = sum(r.^2);
SS_tot = sum((x(:) - mean(x(:))).^2);
fitResults(k).R2 = 1 - SS_res/SS_tot;
% --- Check residual threshold: discard secondary peak if either exceeds ---
if pCanonical(1) > 1.25 || fitResults(k).residual > opts.ResidualThreshold || fitResults(k).maxAbs > opts.ResidualThreshold
% residual too large → discard secondary
pCanonical(4) = NaN;
pCanonical(5:6) = NaN;
fitResults(k).isValid = false;
end
if ~isnan(pCanonical)
fitResults(k).pFit = pCanonical;
fitResults(k).thetaFit = theta;
fitResults(k).xFit = x;
fitResults(k).thetaFine = thetaFine;
fitResults(k).yFit = yFit;
fitResults(k).isValid = true;
fitResults(k).fitMaxTheta = fitMaxTheta;
continue; % success → next curve
end
catch
warning('Curve %d constrained fit failed, switching to generic fit.', k);
end
end
%% 3) Generic two-Gaussian fallback (no or failed secondary detection)
try
twoGaussFree = @(p,th) ...
p(1)*exp(-0.5*((th - p(2))/max(p(3),1e-6)).^2) + ...
p(4)*exp(-0.5*((th - p(5))/max(p(6),1e-6)).^2) + ...
p(7); % offset term
% --- Original initial guess ---
p0_base = [1.0, 0, 0.1, 0.3, 1.0, 0.1, 0];
% --- Generate multiple initial guesses ---
p0_candidates = [
p0_base;
[1.0, 0, 0.1, 0.3, 0.50, 0.1, 0];
[0.5, 0, 0.1, 0.3, 0.50, 0.1, 0.5];
[1.0, 0, 0.1, 0.3, 0.25, 0.1, 0];
[0.5, 0, 0.1, 0.3, 0.25, 0.1, 0.5];
];
lb = [0.0, 0, 1e-6, 0, 0, 1e-6, 0.0];
ub = [1.0, opts.PositionThreshold, opts.MaxTheta/2, 1.0, opts.MaxTheta, opts.MaxTheta/2, 0.5];
optsLSQfree = optimoptions('lsqcurvefit', 'Display', 'off', ...
'MaxFunctionEvaluations', 2e4, 'MaxIterations', 2e4);
bestResidual = Inf;
bestMaxAbs = Inf;
pFreeBest = nan(size(p0_base));
for i = 1:size(p0_candidates,1)
try
pTry = lsqcurvefit(twoGaussFree, p0_candidates(i,:), theta(:), x(:), lb, ub, optsLSQfree);
rTry = x(:) - twoGaussFree(pTry, theta(:));
residualTry = sqrt(mean(rTry.^2));
maxAbsTry = max(abs(rTry));
% --- Select candidate with lowest RMS first, then max residual ---
if residualTry < bestResidual || (residualTry == bestResidual && maxAbsTry < bestMaxAbs)
bestResidual = residualTry;
bestMaxAbs = maxAbsTry;
pFreeBest = pTry;
rBest = rTry;
end
catch
% ignore failed fits
end
end
pFree = pFreeBest;
r = rBest;
fitMaxTheta = max(pFree(5), pFree(2));
thetaFine = linspace(0, opts.MaxTheta, 500);
yFit = twoGaussFree(pFree, thetaFine);
% --- Diagnostics ---
fitResults(k).residual = bestResidual;
fitResults(k).maxAbs = bestMaxAbs;
SS_res = sum(r.^2);
SS_tot = sum((x(:) - mean(x(:))).^2);
fitResults(k).R2 = 1 - SS_res/SS_tot;
fitResults(k).thetaFit = theta;
fitResults(k).xFit = x;
fitResults(k).thetaFine = thetaFine;
fitResults(k).yFit = yFit;
fitResults(k).fitMaxTheta = fitMaxTheta;
% --- Check residual threshold: discard secondary if exceeded ---
if pFree(1) > 1.25 || fitResults(k).residual > opts.ResidualThreshold || fitResults(k).maxAbs > opts.ResidualThreshold
% 2-Gaussian fit failed → mark for 3-Gaussian fallback
pFree(4) = NaN;
pFree(5:6) = NaN;
fitResults(k).isValid = false;
fitResults(k).pFit = pFree;
% do NOT continue → next block can attempt 3-Gaussian fit
else
% 2-Gaussian fit succeeded → store results and skip 3-Gaussian
fitResults(k).isValid = true;
fitResults(k).pFit = pFree;
continue;
end
catch
warning('Curve %d: generic two-Gaussian fit failed.', k);
fitResults(k) = fillNaNStruct();
end
%% 4) Three-Gaussian fallback (if two-Gaussian residual too large)
try
% --- Define 3-Gaussian model ---
threeGaussFree = @(p, th) ...
p(1)*exp(-0.5*((th - p(2))/max(p(3),1e-6)).^2) + ...
p(4)*exp(-0.5*((th - p(5))/max(p(6),1e-6)).^2) + ...
p(7)*exp(-0.5*((th - p(8))/max(p(9),1e-6)).^2) + ...
p(10); % offset term
% --- Base initial guess ---
p0_base3 = [1.0, 0, 0.1, 0.5, 0.5, 0.1, 0.3, 1.0, 0.1, 0];
% --- Generate multiple initial guesses (slightly shifted) ---
p0_candidates3 = [
p0_base3;
[1.0, 0, 0.1, 0.5, 0.5, 0.1, 0.3, 0.75, 0.1, 0];
[0.5, 0, 0.1, 0.5, 0.5, 0.1, 0.3, 1.25, 0.1, 0.5];
[1.0, 0, 0.1, 0.5, 0.25, 0.1, 0.3, 1.0, 0.1, 0];
[0.5, 0, 0.1, 0.5, 0.25, 0.1, 0.3, 1.0, 0.1, 0.5];
];
% --- Bounds ---
lb = [0.0, 0, 1e-6, 0.0, 0, 1e-6, 0.0, 0, 1e-6, 0.0];
ub = [1.0, opts.PositionThreshold, opts.MaxTheta/2, ...
1.0, opts.MaxTheta/2, opts.MaxTheta/2, ...
1.0, opts.MaxTheta, opts.MaxTheta/2, 0.5];
% --- Optimizer options ---
optsLSQ3 = optimoptions('lsqcurvefit', 'Display', 'off', ...
'MaxFunctionEvaluations', 2e4, 'MaxIterations', 2e4);
% --- Track best fit ---
bestResidual3 = Inf;
bestMaxAbs3 = Inf;
pFreeBest3 = nan(size(p0_base3));
% --- Loop over initial guesses ---
for i = 1:size(p0_candidates3,1)
try
pTry3 = lsqcurvefit(threeGaussFree, p0_candidates3(i,:), theta(:), x(:), lb, ub, optsLSQ3);
rTry3 = x(:) - threeGaussFree(pTry3, theta(:));
residualTry3 = sqrt(mean(rTry3.^2));
maxAbsTry3 = max(abs(rTry3));
% --- Select best candidate (lowest RMS, then lowest max residual) ---
if residualTry3 < bestResidual3 || (residualTry3 == bestResidual3 && maxAbsTry3 < bestMaxAbs3)
bestResidual3 = residualTry3;
bestMaxAbs3 = maxAbsTry3;
pFreeBest3 = pTry3;
rBest3 = rTry3;
end
catch
% ignore failed fits
end
end
% --- Extract final best parameters ---
pFree3 = pFreeBest3;
r3 = rBest3;
fitMaxTheta3 = max(pFree3([2,5,8]));
thetaFine3 = linspace(0, opts.MaxTheta, 500);
yFit3 = threeGaussFree(pFree3, thetaFine3);
% --- Diagnostics ---
fitResults(k).residual = bestResidual3;
fitResults(k).maxAbs = bestMaxAbs3;
SS_res3 = sum(r3.^2);
SS_tot3 = sum((x(:) - mean(x(:))).^2);
fitResults(k).R2 = 1 - SS_res3/SS_tot3;
% --- Check validity criteria ---
if any(pFree3([1,4,7]) > 1.25) || ...
fitResults(k).residual > opts.ResidualThreshold || ...
fitResults(k).maxAbs > opts.ResidualThreshold
pFree3(4:9) = NaN; % invalidate secondary/tertiary
fitResults(k).isValid = false;
else
fitResults(k).isValid = true;
end
% --- Store results ---
fitResults(k).pFit = pFree3;
fitResults(k).thetaFit = theta;
fitResults(k).xFit = x;
fitResults(k).thetaFine = thetaFine3;
fitResults(k).yFit = yFit3;
fitResults(k).fitMaxTheta = fitMaxTheta3;
catch
warning('Curve %d: generic three-Gaussian fit failed.', k);
fitResults(k) = fillNaNStruct();
end
catch
warning('Curve %d fit failed completely.', k);
fitResults(k) = fillNaNStruct();
end
end
end
% --- Helper: return struct with NaNs (fit failed) ---
function s = fillNaNStruct()
s = struct( ...
'pFit', nan(1,10), ...
'thetaFit', nan, ...
'xFit', nan, ...
'yFit', nan, ...
'thetaFine', nan, ...
'isValid', false, ...
'residual', nan, ...
'maxAbs', nan, ...
'R2', nan, ...
'fitMaxTheta', nan ...
);
end
% --- Helper: return struct with zeros (no secondary peak found) ---
function s = fillZeroStruct(theta, fitMaxTheta)
s = struct( ...
'pFit', zeros(1,10), ...
'thetaFit', theta, ...
'xFit', zeros(size(theta)), ...
'yFit', zeros(size(theta)), ...
'thetaFine', zeros(1,500), ...
'isValid', false, ...
'residual', zeros(size(theta)), ...
'maxAbs', zeros(size(theta)), ...
'R2', zeros(size(theta)), ...
'fitMaxTheta', fitMaxTheta ...
);
end