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

197 lines
7.8 KiB
Matlab
Raw 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 = fitTwoGaussianCurves(S_theta_all, theta_vals, varargin)
%% fitTwoGaussianCurves
% 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)
% 'DeviationLimit' - 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, 'DeviationLimit', 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',[],'fitMaxTheta',[]);
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)
[~, peak_idx] = max(curve);
else
[~, local_max_idx] = max(curve(idx_range));
peak_idx = idx_range(local_max_idx);
end
S_theta_all_shifted{k} = curve(peak_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 = theta_vals(1:Npoints_shifted) - theta_vals(1);
else
S_theta_all_shifted = S_theta_all;
theta_recentered = theta_vals;
end
for k = 1:Ncurves
try
x = S_theta_all_shifted{k};
theta = theta_recentered;
validIdx = ~isnan(x);
x = x(validIdx);
theta = theta(validIdx);
mask = theta>=0 & theta<=opts.MaxTheta;
x = x(mask);
theta = theta(mask);
if numel(theta) < 8
warning('Curve %d too short (<8 points), skipping.', k);
fitResults(k) = fillNaNStruct();
continue;
end
x = x / x(1);
xSmooth = smooth(x, 5, 'moving');
curveAmp = max(xSmooth) - min(xSmooth);
% --- Robust peak detection ---
[pk, locIdx] = findpeaks(xSmooth, 'MinPeakProminence', opts.AmplitudeThreshold * curveAmp);
thetaPeaks = theta(locIdx);
if isempty(pk)
warning('Curve %d has no significant peaks.', k);
fitResults(k) = fillZeroStruct(theta, opts.MaxTheta);
continue;
end
thetaPeaks = thetaPeaks(:);
pk = pk(:);
% --- Find secondary peak ---
secondaryIdx = find((thetaPeaks > opts.PositionThreshold) & ...
(pk >= opts.AmplitudeThreshold * curveAmp), 1, 'first');
if isempty(secondaryIdx)
% No secondary peak → bypass fit, fill with zeros
fitResults(k) = fillZeroStruct(theta, opts.MaxTheta);
continue;
else
fitMaxTheta = thetaPeaks(secondaryIdx);
secondaryAmp = pk(secondaryIdx);
end
% --- Use the full curve for fitting ---
thetaFit = theta(:);
xFit = x(:);
if numel(thetaFit) < 8
warning('Curve %d has too few points for fitting.', k);
fitResults(k) = fillNaNStruct();
continue;
end
% --- Two-Gaussian model ---
twoGauss = @(p,theta) ...
1.0 * exp(-0.5*((theta - p(1))/max(p(2),1e-6)).^2) + ... % main Gaussian
p(3) * exp(-0.5*((theta - p(4))/max(p(5),1e-6)).^2); % secondary Gaussian
% --- Parameter guesses based on secondary peak ---
mu1_guess = 0;
sigma1_guess = 0.1 * fitMaxTheta;
A2_guess = secondaryAmp; % use detected secondary amplitude
mu2_guess = fitMaxTheta; % center at detected secondary peak
sigma2_guess = fitMaxTheta / 3; % broad guess
p0 = [mu1_guess, sigma1_guess, A2_guess, mu2_guess, sigma2_guess];
% --- Bounds that enforce secondary peak constraints ---
lb = [0, 1e-6, 0, max(fitMaxTheta - 0.1, 0), 1e-6];
ub = [fitMaxTheta/4, fitMaxTheta, 2, min(fitMaxTheta + 0.1, opts.MaxTheta), fitMaxTheta];
optsLSQ = optimoptions('lsqcurvefit','Display','off', ...
'MaxFunctionEvaluations',1e4,'MaxIterations',1e4);
% --- Fit ---
try
pFit = lsqcurvefit(twoGauss, p0, thetaFit, xFit, lb, ub, optsLSQ);
catch
warning('Curve %d fit failed.', k);
fitResults(k) = fillNaNStruct();
continue;
end
% --- Evaluate fitted curve ---
thetaFine = linspace(0, opts.MaxTheta, 500);
yFit = twoGauss(pFit, thetaFine);
% --- Assess deviation ---
relDevAmplitude = abs(pFit(3) - secondaryAmp) / max(secondaryAmp,1e-6);
relDevPosition = abs(pFit(4) - fitMaxTheta) / max(fitMaxTheta, 1e-6);
combinedDeviation = sqrt(relDevAmplitude^2 + relDevPosition^2);
isValid = combinedDeviation <= opts.DeviationLimit;
% --- Store results ---
fitResults(k).pFit = pFit;
fitResults(k).thetaFit = thetaFit;
fitResults(k).xFit = xFit;
fitResults(k).thetaFine = thetaFine;
fitResults(k).yFit = yFit;
fitResults(k).isValid = isValid;
fitResults(k).fitMaxTheta = fitMaxTheta;
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,5),'thetaFit',nan,'xFit',nan,'yFit',nan,'thetaFine',nan,...
'isValid',false,'fitMaxTheta',nan);
end
% --- Helper: return struct with zeros (no secondary peak found) ---
function s = fillZeroStruct(theta, fitMaxTheta)
s = struct('pFit',zeros(1,5),'thetaFit',theta,'xFit',zeros(size(theta)),...
'yFit',zeros(size(theta)),'thetaFine',zeros(1,500),...
'isValid',false,'fitMaxTheta',fitMaxTheta);
end