Major update - new analysis routines: real space g2, recentered angular spectral distribution curves.

This commit is contained in:
Karthik 2025-10-07 20:05:25 +02:00
parent c012497eef
commit ebebce9ef9
15 changed files with 1808 additions and 110 deletions

View File

@ -1,8 +1,8 @@
function analysis_results = analyzeG2Structures(g2_results, opts)
%% analyzeG2Structures
% Computes peak anisotropy of g² correlation matrices using ROI and boundary
% analysis. Anisotropy is zero if the region cannot be well approximated
% by an ellipse or does not stretch enough within the ROI.
% analysis, and fits an oriented two-mode Gaussian aligned with the
% structure's principal axis within the ROI.
%
% Inputs:
% g2_results : struct with fields
@ -14,57 +14,56 @@ function analysis_results = analyzeG2Structures(g2_results, opts)
% - roi.size : [w, h] width and height (µm)
% - roi.angle : rotation angle (radians, CCW)
% - threshold : fraction of peak maximum to define core (default 0.5)
% - deviationThreshold : RMS deviation threshold to accept ellipse (default 0.23)
% - minEllipseFraction : fraction of ROI diagonal that major axis must span (default 0.8)
% - angleLimit : required ellipse angle (radians)
% - angleTolerance : tolerance around angleLimit (radians)
% - skipLivePlot : true/false (default false)
% - fitDeviationThreshold : max normalized RMS deviation for accepting fit
% - boundaryPad : extra pixels around boundary to allow Gaussian peaks
%
% Outputs:
% analysis_results : struct with fields
% - anisotropy_vals : anisotropy per image (0 = irregular or too small, 01 = elliptical)
% - peak_centroid : [x, y] per image
% - boundary_coords : [x, y] coordinates of peak boundary
% - ellipse_params : [x0, y0, a, b, theta] per image
% - anisotropy_vals : numeric array
% - peak_centroid : {N×1} fitted centroids [x, y]
% - ellipse_params : {N×1} fitted parameters + angle
% - boundary_coords : {N×1} [x, y] boundary points
if ~isfield(opts, "skipLivePlot"), opts.skipLivePlot = false; end
if ~isfield(opts, "threshold"), opts.threshold = 0.5; end
if ~isfield(opts, "deviationThreshold"), opts.deviationThreshold = 0.23; end
if ~isfield(opts, "minEllipseFraction"), opts.minEllipseFraction = 0.8; end
if ~isfield(opts, "font"), opts.font = 'Arial'; end
if ~isfield(opts, "fitDeviationThreshold"), opts.fitDeviationThreshold = 0.2; end
if ~isfield(opts, "boundaryPad"), opts.boundaryPad = 2; end
N_images = numel(g2_results.g2_matrices);
analysis_results = struct();
analysis_results.anisotropy_vals = zeros(1, N_images);
analysis_results.peak_centroid = cell(1, N_images);
analysis_results = struct();
analysis_results.anisotropy_vals = zeros(1, N_images);
analysis_results.peak_centroid = cell(1, N_images);
analysis_results.boundary_coords = cell(1, N_images);
analysis_results.ellipse_params = cell(1, N_images);
analysis_results.ellipse_params = cell(1, N_images);
% ROI definition
x0 = opts.roi.center(1);
y0 = opts.roi.center(2);
w = opts.roi.size(1);
h = opts.roi.size(2);
x0 = opts.roi.center(1);
y0 = opts.roi.center(2);
w = opts.roi.size(1);
h = opts.roi.size(2);
roi_theta = opts.roi.angle;
% Precompute ROI diagonal for minimum major axis
roi_diag = sqrt(w^2 + h^2);
minMajorAxis_auto = opts.minEllipseFraction * roi_diag;
for k = 1:N_images
g2_matrix = g2_results.g2_matrices{k};
dx_phys = g2_results.dx_phys{k};
dy_phys = g2_results.dy_phys{k};
[X, Y] = meshgrid(dx_phys, dy_phys);
% ---- Rotated ROI mask ----
Xc = X - x0; Yc = Y - y0;
%% ---- Core ROI extraction (rotated rectangle) ----
Xc = X - x0;
Yc = Y - y0;
Xr = cos(roi_theta)*Xc + sin(roi_theta)*Yc;
Yr = -sin(roi_theta)*Xc + cos(roi_theta)*Yc;
roi_mask = (abs(Xr) <= w/2) & (abs(Yr) <= h/2);
% ---- Threshold ROI to define core region ----
x_roi = Xr(roi_mask);
y_roi = Yr(roi_mask);
z_roi = g2_matrix(roi_mask);
%% ---- Threshold ROI to define core region ----
g2_roi = zeros(size(g2_matrix));
g2_roi(roi_mask) = g2_matrix(roi_mask);
thresh_val = opts.threshold * max(g2_roi(:));
@ -74,88 +73,225 @@ for k = 1:N_images
warning('No peak found in ROI for image %d', k);
analysis_results.peak_centroid{k} = [NaN, NaN];
analysis_results.anisotropy_vals(k) = NaN;
analysis_results.ellipse_params{k} = [NaN, NaN, NaN, NaN, NaN];
analysis_results.ellipse_params{k} = nan(1,7);
analysis_results.boundary_coords{k} = [NaN, NaN];
continue;
end
%% ---- Boundary coordinates ----
%% ---- Boundary coordinates (edge detection inside ROI) ----
B = bwboundaries(BW);
boundary = B{1};
x_bound = dx_phys(boundary(:,2));
y_bound = dy_phys(boundary(:,1));
boundary = B{1};
x_bound = dx_phys(boundary(:,2));
y_bound = dy_phys(boundary(:,1));
analysis_results.boundary_coords{k} = [x_bound, y_bound];
%% ---- Ellipse fitting and anisotropy using boundary region ----
stats = regionprops(BW, 'Centroid','MajorAxisLength','MinorAxisLength','Orientation');
if ~isempty(stats)
a = stats(1).MajorAxisLength / 2 * mean(diff(dx_phys));
b = stats(1).MinorAxisLength / 2 * mean(diff(dy_phys));
theta = -stats(1).Orientation * pi/180;
%% ---- Compute centroid and orientation (use PCA on boundary in ROI-local frame) ----
% Transform boundary points to ROI-local coordinates (same local coords as Xr,Yr)
Xc_b = x_bound - x0;
Yc_b = y_bound - y0;
Xr_b = cos(roi_theta)*Xc_b + sin(roi_theta)*Yc_b;
Yr_b = -sin(roi_theta)*Xc_b + cos(roi_theta)*Yc_b;
x_c = interp1(1:numel(dx_phys), dx_phys, stats(1).Centroid(1), 'linear', 'extrap');
y_c = interp1(1:numel(dy_phys), dy_phys, stats(1).Centroid(2), 'linear', 'extrap');
% PCA on boundary points in ROI-local frame (robust and simple)
boundary_mean = [mean(Xr_b), mean(Yr_b)];
XYb = [Xr_b - boundary_mean(1), Yr_b - boundary_mean(2)];
try
coeff = pca(XYb);
major = coeff(:,1);
angle_region = atan2(major(2), major(1)); % angle in ROI-local coords (radians)
catch
% fallback: use roi_theta as orientation if PCA fails
angle_region = 0;
end
Xb = x_bound - x_c;
Yb = y_bound - y_c;
Xrot = cos(theta)*Xb + sin(theta)*Yb;
Yrot = -sin(theta)*Xb + cos(theta)*Yb;
% We'll define longitudinal axis in ROI-local coordinates as angle_region,
% transverse axis is angle_region + pi/2.
r_norm = (Xrot/a).^2 + (Yrot/b).^2;
deviation = sqrt(mean((r_norm - 1).^2));
%% ---- Use points inside boundary for 1D profile (use ROI-local coords) ----
% We already have axis-aligned ROI samples x_roi, y_roi, z_roi in ROI-local coords.
xData_local = x_roi;
yData_local = y_roi;
zData = z_roi;
%% ---- Interpolate onto regular axis-aligned grid in ROI-local coords ----
Nx = ceil(w*10); Ny = ceil(h*10);
xq = linspace(-w/2, w/2, Nx);
yq = linspace(-h/2, h/2, Ny);
[Xq, Yq] = meshgrid(xq, yq);
Zq = griddata(xData_local, yData_local, zData, Xq, Yq, 'cubic');
% ---- Angle criterion (± allowed) ----
allowed = opts.angleLimit;
tol = opts.angleTolerance;
angle_ok = (abs(theta - allowed) <= tol) || (abs(theta + allowed) <= tol);
% If griddata produced NaNs in large patches, fill with nearest to avoid empty bins
if any(isnan(Zq(:)))
Zq = fillmissing(Zq,'nearest');
end
% Only accept ellipse if deviation small, major axis long enough, and angle ok
if (deviation > opts.deviationThreshold) || (a < minMajorAxis_auto) || (~angle_ok)
anisotropy = 0;
style = '--'; % dashed if rejected
% --- Build rotated coordinates so major axis aligns with new X'
theta = angle_region; % ROI-local angle of major axis
Rrot = [cos(theta), sin(theta); -sin(theta), cos(theta)]; % rotation that maps [x;y] -> [x';y'] where x' along major
XY = [Xq(:)'; Yq(:)'];
XY_rot = Rrot * XY;
Xrot = reshape(XY_rot(1,:), size(Xq));
Yrot = reshape(XY_rot(2,:), size(Yq));
% --- Compute 1D longitudinal profile by binning along X' (major axis)
nbins_long = size(Xq,1); % choose number of bins ~ Ny (vertical resolution)
x_edges_long = linspace(min(Xrot(:)), max(Xrot(:)), nbins_long+1);
x_centers_long = 0.5*(x_edges_long(1:end-1)+x_edges_long(2:end));
prof_long = nan(1, nbins_long);
for ii = 1:nbins_long
mask_bin = Xrot >= x_edges_long(ii) & Xrot < x_edges_long(ii+1);
vals = Zq(mask_bin);
if isempty(vals)
prof_long(ii) = NaN;
else
anisotropy = 1 - b/a;
style = '-'; % solid if accepted
prof_long(ii) = mean(vals,'omitnan');
end
end
analysis_results.peak_centroid{k} = [x_c, y_c];
analysis_results.anisotropy_vals(k) = anisotropy;
analysis_results.ellipse_params{k} = [x_c, y_c, a, b, theta];
else
x_c = NaN; y_c = NaN; a=NaN; b=NaN; theta=NaN; anisotropy=NaN; style='--';
% --- Compute 1D transverse profile by binning along Y' (minor axis)
nbins_trans = size(Xq,2);
y_edges_trans = linspace(min(Yrot(:)), max(Yrot(:)), nbins_trans+1);
y_centers_trans = 0.5*(y_edges_trans(1:end-1)+y_edges_trans(2:end));
prof_trans = nan(1, nbins_trans);
for ii = 1:nbins_trans
mask_bin = Yrot >= y_edges_trans(ii) & Yrot < y_edges_trans(ii+1);
vals = Zq(mask_bin);
if isempty(vals)
prof_trans(ii) = NaN;
else
prof_trans(ii) = mean(vals,'omitnan');
end
end
% --- Prepare x-axis positions for profile centers (in ROI-local units)
xcent_long = x_centers_long; % positions along major axis (µm)
xcent_trans = y_centers_trans; % positions along minor axis (µm)
% --- Remove NaNs from profiles
valid_long = ~isnan(prof_long);
x_long = xcent_long(valid_long);
y_long = prof_long(valid_long);
valid_trans = ~isnan(prof_trans);
x_trans = xcent_trans(valid_trans);
y_trans = prof_trans(valid_trans);
% --- If not enough points, skip
if numel(x_long) < 8 || numel(x_trans) < 8
warning('Too few points for longitudinal/transverse fits for image %d', k);
analysis_results.peak_centroid{k} = [NaN, NaN];
analysis_results.anisotropy_vals(k) = NaN;
analysis_results.ellipse_params{k} = [NaN, NaN, NaN, NaN, NaN];
analysis_results.ellipse_params{k} = nan(1,7);
continue;
end
%% ---- Fit two-Gaussian to longitudinal profile (use your working 1D fit) ----
twoGauss1D = @(p,x) ...
p(1) * exp(-0.5*((x - p(2))/max(p(3),1e-6)).^2) + ... % Gaussian 1
p(4) * exp(-0.5*((x - p(5))/max(p(6),1e-6)).^2) + ... % Gaussian 2
p(7); % offset
% initial guesses (longitudinal)
[~, mainIdxL] = max(y_long);
A1_guessL = y_long(mainIdxL);
mu1_guessL = x_long(mainIdxL);
sigma1_guessL = range(x_long)/10;
A2_guessL = 0.8 * A1_guessL;
mu2_guessL = mu1_guessL + range(x_long)/5;
sigma2_guessL = sigma1_guessL;
offset_guessL = min(y_long);
p0L = [A1_guessL, mu1_guessL, sigma1_guessL, A2_guessL, mu2_guessL, sigma2_guessL, offset_guessL];
lbL = [0, min(x_long), 1e-6, 0, min(x_long), 1e-6, -Inf];
ubL = [2, max(x_long), range(x_long), 2, max(x_long), range(x_long), Inf];
opts_lsq = optimoptions('lsqcurvefit','Display','off','MaxFunctionEvaluations',1e4,'MaxIterations',1e4);
try
pFitL = lsqcurvefit(twoGauss1D, p0L, x_long, y_long, lbL, ubL, opts_lsq);
catch
warning('Longitudinal 1D fit failed for image %d, using initial guess.', k);
pFitL = p0L;
end
%% ---- Fit two-Gaussian to transverse profile (use same logic) ----
[~, mainIdxT] = max(y_trans);
A1_guessT = y_trans(mainIdxT);
mu1_guessT = x_trans(mainIdxT);
sigma1_guessT = range(x_trans)/10;
A2_guessT = 0.8 * A1_guessT;
mu2_guessT = mu1_guessT + range(x_trans)/5;
sigma2_guessT = sigma1_guessT;
offset_guessT = min(y_trans);
p0T = [A1_guessT, mu1_guessT, sigma1_guessT, A2_guessT, mu2_guessT, sigma2_guessT, offset_guessT];
lbT = [0, min(x_trans), 1e-6, 0, min(x_trans), 1e-6, -Inf];
ubT = [2, max(x_trans), range(x_trans), 2, max(x_trans), range(x_trans), Inf];
try
pFitT = lsqcurvefit(twoGauss1D, p0T, x_trans, y_trans, lbT, ubT, opts_lsq);
catch
warning('Transverse 1D fit failed for image %d, using initial guess.', k);
pFitT = p0T;
end
%% ---- Determine anisotropy from the two fits (ratio of widths) ----
sigma_long = mean([pFitL(3), pFitL(6)]);
sigma_trans = mean([pFitT(3), pFitT(6)]);
anisotropy = sigma_trans / sigma_long; % transverse / longitudinal (ratio)
% store centroid roughly as ROI-local centroid rotated back to lab coords
centroid_local_roi = boundary_mean'; % mean of boundary in ROI-local coords
% rotate back to lab frame
rotBack = [cos(roi_theta), -sin(roi_theta); sin(roi_theta), cos(roi_theta)];
centroid_lab = rotBack * centroid_local_roi + [x0; y0];
centroid_lab = centroid_lab(:)';
analysis_results.peak_centroid{k} = centroid_lab;
analysis_results.anisotropy_vals(k) = anisotropy;
analysis_results.ellipse_params{k} = [pFitL, pFitT, angle_region];
%% ---- Visualization ----
if ~opts.skipLivePlot
fig=figure(100); clf;
set(fig, 'Color', 'w', 'Position',[100 100 950 750]);
fig = figure(100); clf;
set(fig,'Color','w','Position',[100 100 1600 450]);
tiledlayout(1,3,'Padding','compact','TileSpacing','compact');
% --- Panel 1: 2D g² map + ROI + boundary
nexttile;
imagesc(dx_phys, dy_phys, g2_matrix);
axis image;
set(gca, 'YDir', 'normal', 'FontName', opts.font, 'FontSize', 14);
colormap(Colormaps.coolwarm()); colorbar;
hold on;
axis image; set(gca,'YDir','normal'); colormap(Colormaps.coolwarm()); colorbar; hold on;
corners = [ -w/2, -h/2; w/2, -h/2; w/2, h/2; -w/2, h/2];
% ROI bounding box (red dashed)
rect = [-w/2, -h/2; w/2, -h/2; w/2, h/2; -w/2, h/2; -w/2, -h/2];
R = [cos(roi_theta), -sin(roi_theta); sin(roi_theta), cos(roi_theta)];
corners_rot = (R*corners')' + [x0, y0];
corners_rot = [corners_rot; corners_rot(1,:)];
plot(corners_rot(:,1), corners_rot(:,2), 'r--', 'LineWidth',1.5);
rect_rot = (R * rect')';
plot(rect_rot(:,1)+x0, rect_rot(:,2)+y0, 'r--', 'LineWidth',2);
plot(x_bound, y_bound, 'g-', 'LineWidth',2);
plot(x_c, y_c, 'mo', 'MarkerSize',8, 'LineWidth',2);
% Detected peak boundary (green solid)
if ~isempty(x_bound)
plot(x_bound, y_bound, 'g-', 'LineWidth',2);
end
t = linspace(0, 2*pi, 200);
ellipse_x = x_c + a*cos(t)*cos(theta) - b*sin(t)*sin(theta);
ellipse_y = y_c + a*cos(t)*sin(theta) + b*sin(t)*cos(theta);
plot(ellipse_x, ellipse_y, ['y' style], 'LineWidth',2);
xlabel('\Deltax (\mum)'); ylabel('\Deltay (\mum)');
title(sprintf('Image %d | 2D g^2', k));
title(sprintf('Image %d | Anisotropy = %.2f | Deviation = %.2f | θ = %.2f°', ...
k, anisotropy, deviation, rad2deg(theta)));
xlabel('x (\mum)'); ylabel('y (\mum)');
drawnow; pause(1.0);
% --- Panel 2: Longitudinal 1D profile and fit
nexttile;
plot(x_long, y_long, 'k.-', 'DisplayName','Data'); hold on;
xFine = linspace(min(x_long), max(x_long), 500);
yFitL = twoGauss1D(pFitL, xFine);
plot(xFine, yFitL, 'r-', 'LineWidth',1.5,'DisplayName','Fit');
title('Longitudinal profile','FontName',opts.font);
xlabel('x_{longitudinal} (\mum)');
ylabel('g^2');
legend('Location','northeast'); grid on;
% --- Panel 3: Transverse 1D profile and fit
nexttile;
plot(x_trans, y_trans, 'b.-', 'DisplayName','Data'); hold on;
xFineT = linspace(min(x_trans), max(x_trans), 500);
yFitT = twoGauss1D(pFitT, xFineT);
plot(xFineT, yFitT, 'r-', 'LineWidth',1.5,'DisplayName','Fit');
title('Transverse profile','FontName',opts.font);
xlabel('x_{transverse} (\mum)');
ylabel('g^2');
legend('Location','northeast'); grid on;
drawnow;
end
end

View File

@ -67,7 +67,6 @@ function results = conductSpectralAnalysis(od_imgs, scan_parameter_values, optio
%% ===== Initialization =====
N_shots = length(od_imgs); % total number of images
fft_imgs = cell(1, N_shots); % FFT of each image
angular_spectral_distribution = cell(1, N_shots); % S(θ) angular spectrum
radial_spectral_contrast = zeros(1, N_shots); % radial contrast metric
angular_spectral_weight = zeros(1, N_shots); % integrated angular weight
@ -147,8 +146,7 @@ function results = conductSpectralAnalysis(od_imgs, scan_parameter_values, optio
S_k_smoothed = movmean(S_k, radial_window_size);
% Store results
angular_spectral_distribution{k} = S_theta;
radial_spectral_contrast(k) = Calculator.computeRadialSpectralContrast(k_rho_vals, S_k_smoothed, k_min, k_max);
radial_spectral_contrast(k) = Calculator.computeRadialSpectralContrast(k_rho_vals, S_k_smoothed, k_min, k_max);
% Normalize angular spectrum and compute weight
S_theta_norm = S_theta / max(S_theta);
@ -281,7 +279,6 @@ function results = conductSpectralAnalysis(od_imgs, scan_parameter_values, optio
results.S_theta_all = S_theta_all;
results.k_rho_vals = k_rho_vals;
results.S_k_all = S_k_all;
results.angular_spectral_distribution = angular_spectral_distribution;
results.S_k_smoothed_all = S_k_smoothed_all;
results.radial_spectral_contrast = radial_spectral_contrast;
results.S_theta_norm_all = S_theta_norm_all;

View File

@ -0,0 +1,175 @@
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.0
%
% 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)
%
% Output:
% fitResults - struct array with fields:
% .pFit - fitted parameters
% .thetaFit - theta values used for fit
% .xFit - curve values used for fit
% .yFit - fitted curve evaluated on thetaFine
% .thetaFine - fine theta for plotting
% .isValid - whether fit passed deviation threshold
% .fitMaxTheta - theta used as fit limit
% --- 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));
parse(p, varargin{:});
opts = p.Results;
Ncurves = numel(S_theta_all);
fitResults = struct('pFit',[],'thetaFit',[],'xFit',[],'yFit',[],...
'thetaFine',[],'isValid',[],'fitMaxTheta',[]);
% --- 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};
% --- Find first peak in 0MaxTheta range ---
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
% Take only the part from the peak to the end
S_theta_all_shifted{k} = curve(peak_idx:end);
end
% --- Pad curves to the 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
% --- Define recentered theta values ---
theta_recentered = theta_vals(1:Npoints_shifted) - theta_vals(1);
for k = 1:Ncurves
x = S_theta_all_shifted{k}; % use recentered curve
theta = theta_recentered; % recentered x-values
validIdx = ~isnan(x);
x = x(validIdx);
theta = theta(validIdx);
% --- Restrict to 0MaxTheta ---
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);
continue;
end
% --- Normalize so S(0)=1 ---
x = x / x(1);
% --- Smooth for stable peak detection ---
xSmooth = smooth(x, 5, 'moving');
% --- Find local maxima ---
[pk, locIdx] = findpeaks(xSmooth);
thetaPeaks = theta(locIdx);
if isempty(pk)
warning('Curve %d has no significant peaks, restricting to within pi/4.', k);
fitMaxTheta = pi/4;
else
% --- Primary peak ---
[mainAmp, mainIdx] = max(pk);
mainTheta = thetaPeaks(mainIdx);
thetaPeaks = thetaPeaks(:); % column vector
pk = pk(:);
% --- Secondary peak 50% of primary after main ---
secondaryIdx = find((thetaPeaks>mainTheta) & (pk>=0.50*mainAmp), 1, 'first');
if isempty(secondaryIdx)
fitMaxTheta = opts.MaxTheta;
else
fitMaxTheta = thetaPeaks(secondaryIdx);
end
end
% --- Extract data up to secondary peak ---
fitMask = theta>=0 & theta<=fitMaxTheta;
thetaFit = theta(fitMask);
xFit = x(fitMask);
if numel(thetaFit) < 8
warning('Curve %d has too few points for fitting, skipping.', k);
continue;
end
% --- Two-Gaussian model ---
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 = [mu1, sigma1, A2, mu2, sigma2]
% --- Initial guesses ---
mu1_guess = 0;
sigma1_guess = 0.1*fitMaxTheta;
A2_guess = max(xFit)*0.8;
mu2_guess = fitMaxTheta/2;
sigma2_guess = fitMaxTheta/5;
p0 = [mu1_guess, sigma1_guess, A2_guess, mu2_guess, sigma2_guess];
% --- Bounds ---
lb = [0, 1e-6, 0, 0, 1e-6];
ub = [fitMaxTheta/4, fitMaxTheta, 2, fitMaxTheta, 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, using initial guess.', k);
pFit = p0;
end
% --- Evaluate fitted curve ---
thetaFine = linspace(0, opts.MaxTheta, 500);
yFit = twoGauss(pFit, thetaFine);
% --- Compute relative deviation ---
yFitInterp = twoGauss(pFit, thetaFit);
relDeviation = abs(yFitInterp - xFit) ./ max(xFit, 1e-6);
maxRelDev = max(relDeviation);
% --- Goodness-of-fit ---
isValid = maxRelDev <= 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;
end
end

View File

@ -74,11 +74,11 @@ function [results, scan_parameter_values, scan_reference_values] = performAnalys
% Extract angular correlations
full_g2_results = Analyzer.extractAutocorrelation(...
spectral_analysis_results.theta_vals, ...
spectral_analysis_results.angular_spectral_distribution, ...
spectral_analysis_results.S_theta_all, ...
scan_parameter_values, N_shots, options.N_angular_bins);
custom_g_results = Analyzer.extractCustomCorrelation(...
spectral_analysis_results.angular_spectral_distribution, ...
spectral_analysis_results.S_theta_all, ...
scan_parameter_values, N_shots, options.N_angular_bins);
fprintf('\n[INFO] Spectral analysis complete!\n');

View File

@ -0,0 +1,150 @@
function plotFitParameterPDF(fitResults, scanValues, paramName, varargin)
%% plotFitParameterPDF
% Author: Karthik
% Date: 2025-10-06
% Version: 1.0
%
% Description:
% Plots 2D PDF (heatmap) of any parameter from two-Gaussian fit results
% for multiple scan parameters, with optional overlay of mean ± SEM.
%
% Inputs:
% fitResults - struct array from fitTwoGaussianCurves
% scanValues - vector of scan parameter values
% paramName - string specifying the parameter to plot:
% 'mu1', 'sigma1', 'A2', 'mu2', 'sigma2'
%
% Optional name-value pairs (same as before, plus OverlayMeanSEM):
% 'OverlayMeanSEM' - logical, overlay mean ± SEM (default: true)
% --- Parse optional inputs ---
p = inputParser;
addParameter(p, 'Title', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'XLabel', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'YLabel', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'FigNum', 1, @(x) isscalar(x));
addParameter(p, 'FontName', 'Arial', @ischar);
addParameter(p, 'FontSize', 14, @isnumeric);
addParameter(p, 'SkipSaveFigures', false, @islogical);
addParameter(p, 'SaveFileName', 'FitParameterPDF.fig', @ischar);
addParameter(p, 'SaveDirectory', pwd, @ischar);
addParameter(p, 'NumPoints', 200, @(x) isscalar(x));
addParameter(p, 'DataRange', [], @(x) isempty(x) || numel(x)==2);
addParameter(p, 'XLim', [], @(x) isempty(x) || numel(x)==2);
addParameter(p, 'Colormap', @jet);
addParameter(p, 'PlotType', 'histogram', @(x) any(validatestring(x,{'kde','histogram'})));
addParameter(p, 'NumberOfBins', 50, @isscalar);
addParameter(p, 'NormalizeHistogram', true, @islogical);
addParameter(p, 'OverlayMeanSEM', true, @islogical);
parse(p, varargin{:});
opts = p.Results;
% --- Map paramName to index ---
paramMap = struct('mu1',1,'sigma1',2,'A2',3,'mu2',4,'sigma2',5);
if ~isfield(paramMap,paramName)
error('Invalid paramName. Must be one of: mu1, sigma1, A2, mu2, sigma2');
end
paramIdx = paramMap.(paramName);
% --- Determine repetitions and scan parameters ---
N_params = numel(scanValues);
N_total = numel(fitResults);
N_reps = N_total / N_params;
% --- Extract chosen parameter values ---
paramValues = nan(N_reps, N_params);
for k = 1:N_total
paramIdxScan = mod(k-1, N_params) + 1;
repIdx = floor((k-1)/N_params) + 1;
if fitResults(k).isValid
paramValues(repIdx, paramIdxScan) = fitResults(k).pFit(paramIdx);
end
end
% --- Prepare data per scan parameter ---
dataCell = cell(N_params,1);
for i = 1:N_params
dataCell{i} = paramValues(:,i);
end
% --- Determine y-range ---
if isempty(opts.DataRange)
allData = cell2mat(dataCell(:));
y_min = min(allData);
y_max = max(allData);
else
y_min = opts.DataRange(1);
y_max = opts.DataRange(2);
end
% --- Prepare PDF grid/matrix ---
if strcmpi(opts.PlotType,'kde')
y_grid = linspace(y_min, y_max, opts.NumPoints);
pdf_matrix = zeros(numel(y_grid), N_params);
else
edges = linspace(y_min, y_max, opts.NumberOfBins+1);
binCenters = (edges(1:end-1) + edges(2:end))/2;
pdf_matrix = zeros(numel(binCenters), N_params);
end
% --- Compute PDFs ---
for i = 1:N_params
data = dataCell{i};
data = data(~isnan(data));
if isempty(data), continue; end
if strcmpi(opts.PlotType,'kde')
f = ksdensity(data, y_grid);
pdf_matrix(:,i) = f;
else
counts = histcounts(data, edges);
if opts.NormalizeHistogram
binWidth = edges(2) - edges(1);
counts = counts / (sum(counts) * binWidth);
end
pdf_matrix(:,i) = counts(:);
end
end
% --- Plot heatmap ---
fig = figure(opts.FigNum); clf(fig);
set(fig, 'Color', 'w', 'Position', [100 100 950 750]);
if strcmpi(opts.PlotType,'kde')
imagesc(scanValues, y_grid, pdf_matrix);
else
imagesc(scanValues, binCenters, pdf_matrix);
end
set(gca, 'YDir', 'normal', 'FontName', opts.FontName, 'FontSize', opts.FontSize);
xlabel(opts.XLabel, 'FontSize', opts.FontSize, 'FontName', opts.FontName);
ylabel(opts.YLabel, 'FontSize', opts.FontSize, 'FontName', opts.FontName);
title(opts.Title, 'FontSize', opts.FontSize+2, 'FontWeight', 'bold');
colormap(feval(opts.Colormap));
c = colorbar;
if strcmpi(opts.PlotType,'kde') || opts.NormalizeHistogram
ylabel(c, 'Probability Density', 'Rotation', -90, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
else
ylabel(c, 'Counts', 'Rotation', -90, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
end
if ~isempty(opts.XLim)
xlim(opts.XLim);
end
% --- Overlay mean ± SEM if requested ---
if opts.OverlayMeanSEM
meanParam = nanmean(paramValues,1);
semParam = nanstd(paramValues,0,1) ./ sqrt(sum(~isnan(paramValues),1));
hold on;
xVec = reshape(scanValues, 1, []); % ensures 1 × N_params
fill([xVec, fliplr(xVec)], [meanParam - semParam, fliplr(meanParam + semParam)], ...
[0.2 0.4 0.8], 'FaceAlpha',0.2, 'EdgeColor','none');
plot(scanValues, meanParam, 'k-', 'LineWidth', 2);
end
% --- Save figure ---
if ~opts.SkipSaveFigures
if ~exist(opts.SaveDirectory,'dir'), mkdir(opts.SaveDirectory); end
savefig(fig, fullfile(opts.SaveDirectory, opts.SaveFileName));
end
end

View File

@ -41,7 +41,7 @@ function plotMeanWithSE(scan_values, data_values, varargin)
else
groupVals = group;
end
mean_vals(i) = mean(groupVals);
mean_vals(i) = mean(groupVals, 'omitnan');
stderr_vals(i) = std(groupVals) / sqrt(length(groupVals));
end

View File

@ -0,0 +1,182 @@
function plotODG2withAnalysis(od_imgs, scan_parameter_values, g2_results, analysis_results, options, varargin)
%% plotODG2withAnalysis
% Author: Karthik
% Date: 2025-09-30
% Version: 1.0
%
% Description:
% For each scan parameter value, produces one or more compact figures.
% Each row contains:
% - Col 1: OD image
% - Col 2: g² correlation matrix
% Overlays from analysis (boundary, ellipse, centroid, anisotropy) can be
% optionally toggled on/off.
%
% Inputs:
% od_imgs : cell array of OD images
% scan_parameter_values: vector/array of scan parameters (one per image)
% g2_results : struct with fields
% - g2_matrices{k}
% - dx_phys{k}, dy_phys{k}
% analysis_results : struct with fields
% - boundary_coords{k}
% - ellipse_params{k}
% - peak_centroid{k}
% - anisotropy_vals(k)
% - roi_params{k} (optional)
% options : struct with imaging calibration
% - pixel_size (in meters)
% - magnification (unitless)
% varargin : name-value pairs
% - 'FontName', 'FontSize', 'SkipLivePlot', 'SkipSaveFigures',
% 'SaveDirectory', 'ShowOverlays', 'RepsPerPage'
%
% Notes:
% Requires conductCorrelationAnalysis and analyzeG2Structures outputs.
% --- Parse optional name-value pairs ---
p = inputParser;
addParameter(p, 'FontName', 'Arial', @ischar);
addParameter(p, 'FontSize', 12, @isnumeric);
addParameter(p, 'SkipLivePlot', false, @islogical);
addParameter(p, 'SkipSaveFigures', true, @islogical);
addParameter(p, 'SaveDirectory', pwd, @ischar);
addParameter(p, 'ShowOverlays', true, @islogical);
addParameter(p, 'RepsPerPage', 10, @isnumeric); % pagination
parse(p, varargin{:});
opts = p.Results;
% --- Setup save directory if needed ---
if ~opts.SkipSaveFigures
saveFolder = fullfile(opts.SaveDirectory, 'Results', 'SavedFigures', 'OD_G2_withAnalysis');
if ~exist(saveFolder, 'dir')
mkdir(saveFolder);
end
end
% --- Group by parameter value ---
param_vals = scan_parameter_values(:);
unique_params = unique(param_vals, 'rows');
N_params = size(unique_params, 1);
for pIdx = 1:N_params
matches = ismember(param_vals, unique_params(pIdx, :), 'rows');
idx_list = find(matches);
N_reps = numel(idx_list);
% --- Pagination over repetitions ---
numPages = ceil(N_reps / opts.RepsPerPage);
for pageIdx = 1:numPages
repStart = (pageIdx-1)*opts.RepsPerPage + 1;
repEnd = min(pageIdx*opts.RepsPerPage, N_reps);
repSubset = idx_list(repStart:repEnd);
N_rows = numel(repSubset);
% --- Create compact figure ---
if ~opts.SkipLivePlot
fig = figure('Color', 'w', 'Position', [100 100 400 800]);
figure(fig); % ensure visible
else
fig = figure('Color', 'w', 'Position', [100 100 400 800], 'Visible','off'); % invisible for direct save
end
t = tiledlayout(fig, N_rows, 2, 'TileSpacing', 'compact', 'Padding', 'compact');
title(t, sprintf('Scan parameter: %s | Page %d/%d', ...
mat2str(unique_params(pIdx, :)), pageIdx, numPages), ...
'FontSize', opts.FontSize + 2, 'FontWeight', 'bold', 'FontName', opts.FontName);
for r = 1:N_rows
k = repSubset(r);
% --- OD image ---
nexttile;
[M, N] = size(od_imgs{k});
x_phys = ((1:N) - ceil(N/2)) * (options.pixel_size / options.magnification * 1e6); % µm
y_phys = ((1:M) - ceil(M/2)) * (options.pixel_size / options.magnification * 1e6); % µm
imagesc(x_phys, y_phys, od_imgs{k});
axis image;
set(gca, 'YDir', 'normal', 'FontName', opts.FontName, 'FontSize', 14);
colormap(gca, Colormaps.inferno());
xlabel('x (\mum)');
ylabel('y (\mum)');
% --- g² correlation matrix ---
nexttile;
dx = g2_results.dx_phys{k};
dy = g2_results.dy_phys{k};
g2_matrix = g2_results.g2_matrices{k};
imagesc(dx, dy, g2_matrix);
axis image;
set(gca, 'YDir', 'normal', 'FontName', opts.FontName, 'FontSize', 14);
colormap(gca, Colormaps.coolwarm()); colorbar;
xlabel('\Deltax (\mum)'); ylabel('\Deltay (\mum)');
hold on;
% --- Optional overlays ---
if opts.ShowOverlays
boundary = analysis_results.boundary_coords{k};
ellipse = analysis_results.ellipse_params{k};
centroid = analysis_results.peak_centroid{k};
anisotropy = analysis_results.anisotropy_vals(k);
theta = analysis_results.ellipse_params{k}(5);
% ROI rectangle (rotated)
if isfield(analysis_results, 'roi_params')
roi = analysis_results.roi_params{k};
if ~isempty(roi) && all(~isnan(roi))
x0 = roi(1); y0 = roi(2);
w = roi(3); h = roi(4);
roi_theta = roi(5);
corners = [ -w/2, -h/2; w/2, -h/2; w/2, h/2; -w/2, h/2];
R = [cos(roi_theta), -sin(roi_theta); sin(roi_theta), cos(roi_theta)];
corners_rot = (R*corners')' + [x0, y0];
corners_rot = [corners_rot; corners_rot(1,:)];
plot(corners_rot(:,1), corners_rot(:,2), 'r--', 'LineWidth',1.5);
end
end
if all(~isnan(boundary(:)))
plot(boundary(:, 1), boundary(:, 2), 'g-', 'LineWidth', 2);
end
if all(~isnan(centroid))
plot(centroid(1), centroid(2), 'mo', 'MarkerSize', 8, 'LineWidth', 2);
x_c = centroid(1); y_c = centroid(2);
else
x_c = NaN; y_c = NaN;
end
if all(~isnan(ellipse)) && ~isnan(anisotropy) && anisotropy ~= 0
a = ellipse(3);
b = ellipse(4);
tEllipse = linspace(0, 2*pi, 200);
ellipse_x = x_c + a*cos(tEllipse)*cos(theta) - b*sin(tEllipse)*sin(theta);
ellipse_y = y_c + a*cos(tEllipse)*sin(theta) + b*sin(tEllipse)*cos(theta);
if anisotropy > 1.0
plot(ellipse_x, ellipse_y, 'y--', 'LineWidth', 2);
else
plot(ellipse_x, ellipse_y, 'y-', 'LineWidth', 2);
end
end
end
end
if ~opts.SkipLivePlot
drawnow; % render figure
end
% --- Save figure ---
if ~opts.SkipSaveFigures
saveFileName = sprintf('OD_G2_analysis_param_%03d_page_%02d.png', pIdx, pageIdx);
Plotter.saveFigure(fig, ...
'SaveFileName', saveFileName, ...
'SaveDirectory', saveFolder, ...
'SkipSaveFigures', opts.SkipSaveFigures);
end
% --- Close invisible figure to free memory ---
if opts.SkipLivePlot
close(fig);
end
end
end
end

View File

@ -0,0 +1,122 @@
function results = plotSpectralCurves(S_theta_all, theta_vals, scan_reference_values, varargin)
%% plotSpectralCurves
% Author: Karthik
% Date: 2025-10-01
% Version: 1.0
%
% Description:
% Plot raw spectral curves with mean, SEM, and highlights
%
% Inputs:
% S_theta_all - 1x(N_reps*N_params) cell array of curves
% scan_reference_values - vector of unique scan parameters
%
% Optional name-value pairs:
% 'Title' - Figure title (default: 'Spectral Curves')
% 'XLabel' - Label for x-axis (default: '\theta / \pi')
% 'YLabel' - Label for y-axis (default: 'S(\theta)')
% 'FontName' - Font name (default: 'Arial')
% 'FontSize' - Font size (default: 14)
% 'FigNum' - Figure number (default: [])
% 'SkipSaveFigures' - Logical flag to skip saving (default: false)
% 'SaveFileName' - Name of figure file (default: 'SpectralCurves.fig')
% 'SaveDirectory' - Directory to save figure (default: pwd)
% 'TileTitlePrefix' - Prefix for tile titles (default: 'Scan Parameter')
% 'TileTitleSuffix' - Suffix for tile titles (default: '')
% 'HighlightEvery' - Highlight every Nth repetition (default: 10)
%
% Notes:
% Automatically computes number of repetitions from S_theta_all length and scan_reference_values.
% --- Parse name-value pairs ---
p = inputParser;
addParameter(p, 'Title', 'Spectral Curves', @(x) ischar(x) || isstring(x));
addParameter(p, 'XLabel', '\theta / \pi', @(x) ischar(x) || isstring(x));
addParameter(p, 'YLabel', 'S(\theta)', @(x) ischar(x) || isstring(x));
addParameter(p, 'FontName', 'Arial', @ischar);
addParameter(p, 'FontSize', 14, @isnumeric);
addParameter(p, 'FigNum', [], @(x) isempty(x) || (isnumeric(x) && isscalar(x)));
addParameter(p, 'SkipSaveFigures', false, @islogical);
addParameter(p, 'SaveFileName', 'SpectralCurves.fig', @ischar);
addParameter(p, 'SaveDirectory', pwd, @ischar);
addParameter(p, 'TileTitlePrefix', 'Scan Parameter', @(x) ischar(x) || isstring(x));
addParameter(p, 'TileTitleSuffix', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'HighlightEvery', 10, @(x) isnumeric(x) && isscalar(x) && x>=1);
parse(p, varargin{:});
opts = p.Results;
% --- Determine number of scan parameters and repetitions ---
N_params = numel(scan_reference_values);
Ntotal = numel(S_theta_all);
Nreps = Ntotal / N_params;
% --- Prepare curves, mean, SEM ---
curves_all = cell(1, N_params);
curves_mean = cell(1, N_params);
curves_err = cell(1, N_params);
Npoints = numel(S_theta_all{1});
for i = 1:N_params
curves = zeros(Nreps, Npoints);
for r = 1:Nreps
idx = (r-1)*N_params + i; % correct interleaved indexing
curves(r,:) = S_theta_all{idx};
end
curves_all{i} = curves;
curves_mean{i} = mean(curves,1);
curves_err{i} = std(curves,0,1)/sqrt(Nreps);
end
% --- Create results struct compatible with plotting ---
results.curves = curves_all;
results.x_values = theta_vals; % generic x-axis
results.curves_mean = curves_mean;
results.curves_error = curves_err;
results.scan_parameter_values = scan_reference_values;
% --- Create figure ---
if isempty(opts.FigNum), fig = figure; else, fig = figure(opts.FigNum); end
clf(fig);
set(fig,'Color','w','Position',[100 100 950 750]);
t = tiledlayout('TileSpacing','compact','Padding','compact');
title(t, opts.Title, 'FontName', opts.FontName, 'FontSize', opts.FontSize+2);
% --- Loop over scan parameters ---
for i = 1:N_params
ax = nexttile; hold(ax,'on');
G = results.curves{i}; % [N_reps × Npoints]
% --- Plot all repetitions in light grey ---
plot(ax, results.x_values, G', 'Color', [0.7 0.7 0.7, 0.5]);
% --- Highlight every Nth repetition ---
idx = opts.HighlightEvery:opts.HighlightEvery:size(G,1);
for j = idx
plot(ax, results.x_values, G(j,:), 'Color', [0.3 0.3 0.3, 1], 'LineWidth', 1.5);
end
% --- Mean + SEM shading ---
mu = results.curves_mean{i};
se = results.curves_error{i};
fill(ax, [results.x_values fliplr(results.x_values)], [mu-se fliplr(mu+se)], ...
[0.2 0.4 0.8], 'FaceAlpha',0.2, 'EdgeColor','none');
% --- Mean curve ---
plot(ax, results.x_values, mu, 'b-', 'LineWidth', 2);
% --- Axes formatting ---
grid(ax,'on');
xlabel(ax, opts.XLabel, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
ylabel(ax, opts.YLabel, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
title(ax, sprintf('%s=%.3g%s', opts.TileTitlePrefix, results.scan_parameter_values(i), opts.TileTitleSuffix), ...
'FontName', opts.FontName, 'FontSize', opts.FontSize);
set(ax, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
end
% --- Save figure ---
if ~opts.SkipSaveFigures
if ~exist(opts.SaveDirectory,'dir'), mkdir(opts.SaveDirectory); end
savefig(fig, fullfile(opts.SaveDirectory, opts.SaveFileName));
end
end

View File

@ -0,0 +1,162 @@
function results = plotSpectralCurvesRecentered(S_theta_all, theta_vals, scan_reference_values, varargin)
%% plotSpectralCurves
% Author: Karthik
% Date: 2025-10-01
% Version: 1.0
%
% Description:
% Plot raw spectral curves with mean, SEM, and highlights
% Preprocess: shifts each curve so its first peak is at zero (no wrap-around)
%
% Inputs:
% S_theta_all - 1x(N_reps*N_params) cell array of curves
% theta_vals - vector of theta values (radians)
% scan_reference_values - vector of unique scan parameters
%
% Optional name-value pairs:
% 'Title' - Figure title (default: 'Spectral Curves')
% 'XLabel' - Label for x-axis (default: '\theta / \pi')
% 'YLabel' - Label for y-axis (default: 'S(\theta)')
% 'FontName' - Font name (default: 'Arial')
% 'FontSize' - Font size (default: 14)
% 'FigNum' - Figure number (default: [])
% 'SkipSaveFigures' - Logical flag to skip saving (default: false)
% 'SaveFileName' - Name of figure file (default: 'SpectralCurves.fig')
% 'SaveDirectory' - Directory to save figure (default: pwd)
% 'TileTitlePrefix' - Prefix for tile titles (default: 'Scan Parameter')
% 'TileTitleSuffix' - Suffix for tile titles (default: '')
% 'HighlightEvery' - Highlight every Nth repetition (default: 10)
%
% Notes:
% Automatically computes number of repetitions from S_theta_all length and scan_reference_values.
% --- Parse name-value pairs ---
p = inputParser;
addParameter(p, 'Title', 'Spectral Curves', @(x) ischar(x) || isstring(x));
addParameter(p, 'XLabel', '\theta / \pi', @(x) ischar(x) || isstring(x));
addParameter(p, 'YLabel', 'S(\theta)', @(x) ischar(x) || isstring(x));
addParameter(p, 'FontName', 'Arial', @ischar);
addParameter(p, 'FontSize', 14, @isnumeric);
addParameter(p, 'FigNum', [], @(x) isempty(x) || (isnumeric(x) && isscalar(x)));
addParameter(p, 'SkipSaveFigures', false, @islogical);
addParameter(p, 'SaveFileName', 'SpectralCurves.fig', @ischar);
addParameter(p, 'SaveDirectory', pwd, @ischar);
addParameter(p, 'TileTitlePrefix', 'Scan Parameter', @(x) ischar(x) || isstring(x));
addParameter(p, 'TileTitleSuffix', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'HighlightEvery', 10, @(x) isnumeric(x) && isscalar(x) && x>=1);
parse(p, varargin{:});
opts = p.Results;
% --- Determine number of scan parameters and repetitions ---
N_params = numel(scan_reference_values);
Ntotal = numel(S_theta_all);
Nreps = Ntotal / N_params;
theta_min = deg2rad(0);
theta_max = deg2rad(90);
% --- Preprocess curves: shift first peak to zero without wrapping ---
S_theta_all_shifted = cell(size(S_theta_all));
for k = 1:Ntotal
curve = S_theta_all{k};
% --- Find first peak only in specified theta range ---
idx_range = find(theta_vals >= theta_min & theta_vals <= theta_max);
if isempty(idx_range)
% fallback: search entire curve if range is empty
[~, peak_idx] = max(curve);
else
[~, local_max_idx] = max(curve(idx_range));
peak_idx = idx_range(local_max_idx);
end
% Take only the part from peak to end
S_theta_all_shifted{k} = curve(peak_idx:end);
end
% --- Adjust Npoints for plotting ---
Npoints_shifted = max(cellfun(@numel, S_theta_all_shifted));
for k = 1:Ntotal
% Pad shorter curves with NaN to keep sizes consistent
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
% --- Prepare curves, mean, SEM ---
curves_all = cell(1, N_params);
curves_mean = cell(1, N_params);
curves_err = cell(1, N_params);
for i = 1:N_params
curves = zeros(Nreps, Npoints_shifted);
for r = 1:Nreps
idx = (r-1)*N_params + i; % correct interleaved indexing
curves(r,:) = S_theta_all_shifted{idx};
end
curves_all{i} = curves;
curves_mean{i} = nanmean(curves,1);
curves_err{i} = nanstd(curves,0,1)/sqrt(Nreps);
end
% --- Create results struct compatible with plotting ---
results.curves = curves_all;
results.x_values = theta_vals(1:Npoints_shifted) - theta_vals(1); % center first peak at zero
results.curves_mean = curves_mean;
results.curves_error = curves_err;
results.scan_parameter_values = scan_reference_values;
% --- Create figure ---
if isempty(opts.FigNum), fig = figure; else, fig = figure(opts.FigNum); end
clf(fig);
set(fig,'Color','w','Position',[100 100 950 750]);
t = tiledlayout('TileSpacing','compact','Padding','compact');
title(t, opts.Title, 'FontName', opts.FontName, 'FontSize', opts.FontSize+2);
% --- Loop over scan parameters ---
for i = 1:N_params
ax = nexttile; hold(ax,'on');
G = results.curves{i}; % [N_reps × Npoints]
% --- Plot all repetitions in light grey ---
plot(ax, results.x_values, G', 'Color', [0.7 0.7 0.7, 0.5]);
% --- Highlight every Nth repetition ---
idx = opts.HighlightEvery:opts.HighlightEvery:size(G,1);
for j = idx
plot(ax, results.x_values, G(j,:), 'Color', [0.3 0.3 0.3, 1], 'LineWidth', 1.5);
end
% --- Mean + SEM shading ---
mu = results.curves_mean{i};
se = results.curves_error{i};
fill(ax, [results.x_values fliplr(results.x_values)], [mu-se fliplr(mu+se)], ...
[0.2 0.4 0.8], 'FaceAlpha',0.2, 'EdgeColor','none');
% --- Mean curve ---
plot(ax, results.x_values, mu, 'b-', 'LineWidth', 2);
% --- Vertical reference lines at pi/3 and 2pi/3 ---
xlines = [1/3 2/3];
for xl = xlines
xline(ax, xl, 'k--', 'LineWidth', 1.5, 'Alpha', 0.5);
end
% --- Axes formatting ---
grid(ax,'on');
xlabel(ax, opts.XLabel, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
ylabel(ax, opts.YLabel, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
title(ax, sprintf('%s=%.3g%s', opts.TileTitlePrefix, results.scan_parameter_values(i), opts.TileTitleSuffix), ...
'FontName', opts.FontName, 'FontSize', opts.FontSize);
set(ax, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
end
% --- Save figure ---
if ~opts.SkipSaveFigures
if ~exist(opts.SaveDirectory,'dir'), mkdir(opts.SaveDirectory); end
savefig(fig, fullfile(opts.SaveDirectory, opts.SaveFileName));
end
end

View File

@ -0,0 +1,90 @@
function plotSpectralDistributionCumulants(results, varargin)
%% plotSpectralCumulants
% Author: Karthik
% Date: 2025-10-02
% Version: 1.0
%
% Description:
% Plot first four cumulants of recentered spectral curves vs scan parameter.
%
% Inputs:
% results_struct - struct returned by plotSpectralCurvesRecentered
% (fields: curves, x_values, scan_parameter_values)
%
% Notes:
% Computes cumulants across repetitions at selected theta values
% and plots them in 4 tiled subplots.
% --- Parse name-value pairs ---
p = inputParser;
addParameter(p, 'Title', 'Spectral Cumulants', @(x) ischar(x) || isstring(x));
addParameter(p, 'XLabel', 'Scan Parameter', @(x) ischar(x) || isstring(x));
addParameter(p, 'FontName', 'Arial', @ischar);
addParameter(p, 'FontSize', 14, @isnumeric);
addParameter(p, 'FigNum', [], @(x) isempty(x) || (isnumeric(x) && isscalar(x)));
addParameter(p, 'SkipSaveFigures', false, @islogical);
addParameter(p, 'SaveFileName', 'SpectralCumulants.fig', @ischar);
addParameter(p, 'SaveDirectory', pwd, @ischar);
addParameter(p, 'MaxOrder', 4, @isnumeric);
parse(p, varargin{:});
opts = p.Results;
% --- Setup ---
N_params = numel(results.curves);
xvals = results.scan_parameter_values;
thetaVals = results.x_values * pi;
% --- Select specific theta indices (like in g2 case) ---
desiredTheta = [pi/12 pi/6 pi/3];
[~, thIdx] = arrayfun(@(t) min(abs(thetaVals - t)), desiredTheta);
thetaLabels = {'\pi/12','\pi/6','\pi/3'};
% --- Compute cumulants: [theta × scan × cumulantOrder] ---
kappa = zeros(numel(thIdx), N_params, opts.MaxOrder);
for i = 1:N_params
for t = 1:numel(thIdx)
reps_values = results.curves{i}(:, thIdx(t));
kappa(t,i,:) = Calculator.computeCumulants(reps_values, opts.MaxOrder);
end
end
% --- Colormap ---
fullCmap = Colormaps.coolwarm(256);
Ntheta = numel(thIdx);
blueSide = fullCmap(1:100,:); % avoid white
redSide = fullCmap(157:end,:);
trimmedCmap = [blueSide; redSide];
indices = round(linspace(1,size(trimmedCmap,1),Ntheta));
cmap = trimmedCmap(indices,:);
% --- Create figure ---
if isempty(opts.FigNum), fig = figure; else, fig = figure(opts.FigNum); end
clf(fig);
set(fig,'Color','w','Position',[100 100 950 750]);
t = tiledlayout(2,2,'TileSpacing','Compact','Padding','Compact');
title(t, opts.Title, 'FontName', opts.FontName, 'FontSize', opts.FontSize+4);
cumulLabels = {'\kappa_1','\kappa_2','\kappa_3','\kappa_4'};
cumulTitles = {'Mean','Variance','Skewness','Binder Cumulant'};
for k = 1:opts.MaxOrder
ax = nexttile; hold(ax,'on');
for idx = 1:numel(thIdx)
plot(ax, xvals, squeeze(kappa(idx,:,k)), '-o', ...
'Color', cmap(idx,:), 'LineWidth', 2, 'MarkerSize', 8, 'MarkerFaceColor', cmap(idx,:));
end
ylabel(ax, cumulLabels{k}, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
xlabel(ax, opts.XLabel, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
title(ax, cumulTitles{k}, 'FontName', opts.FontName, 'FontSize', opts.FontSize+2);
grid(ax,'on'); set(ax,'FontName',opts.FontName,'FontSize',opts.FontSize);
% --- Legend ---
legend(ax, thetaLabels, 'Location', 'best', 'FontSize', opts.FontSize-2);
end
% --- Save figure ---
if ~opts.SkipSaveFigures
if ~exist(opts.SaveDirectory,'dir'), mkdir(opts.SaveDirectory); end
savefig(fig, fullfile(opts.SaveDirectory, opts.SaveFileName));
end
end

View File

@ -46,12 +46,19 @@ function saveFigure(fig, varargin)
[~, name, ext] = fileparts(opts.SaveFileName);
if isempty(ext)
ext = '.fig';
elseif ~strcmpi(ext, '.fig')
warning('Overriding extension to .fig (was %s).', ext);
ext = '.fig';
end
saveFullPath = fullfile(opts.SaveDirectory, [name ext]);
savefig(fig, saveFullPath);
fprintf('Figure saved as MATLAB .fig: %s\n', saveFullPath);
switch lower(ext)
case '.fig'
savefig(fig, saveFullPath);
fprintf('Figure saved as MATLAB .fig: %s\n', saveFullPath);
case '.png'
saveas(fig, saveFullPath);
fprintf('Figure saved as PNG: %s\n', saveFullPath);
otherwise
warning('Unsupported extension, saving as .fig');
savefig(fig, fullfile(opts.SaveDirectory, [name '.fig']));
end
end

View File

@ -187,13 +187,251 @@ Plotter.plotMultiplePCAResults(compiled_results.pca_results, scan_parameter_valu
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveDirectory', figSaveDir);
%% ------------------ 7. Plot of all Angular Spectral Distribution Curves ------------------
Plotter.plotSpectralCurves( ...
results_all{1}.results.spectral_analysis_results.S_theta_norm_all, ...
results_all{1}.results.spectral_analysis_results.theta_vals/pi, ... % correct θ values
results_all{1}.scan_reference_values, ... % correct scan params
'Title', options.titleString, ...
'XLabel', '\theta / \pi', ...
'YLabel', 'S(\theta)', ...
'HighlightEvery', 10, ... % highlight every 10th repetition
'FontName', options.font, ...
'FigNum', 20, ...
'TileTitlePrefix', '\alpha', ... % user-defined tile prefix
'TileTitleSuffix', '^\circ', ... % user-defined suffix (e.g., degrees symbol)
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SpectralCurves.fig', ...
'SaveDirectory', figSaveDir);
%% ------------------ 8. Plot of all Angular Spectral Distribution Curves shifted ------------------
results = Plotter.plotSpectralCurvesRecentered( ...
results_all{1}.results.spectral_analysis_results.S_theta_norm_all, ...
results_all{1}.results.spectral_analysis_results.theta_vals/pi, ... % correct θ values
results_all{1}.scan_reference_values, ... % correct scan params
'Title', options.titleString, ...
'XLabel', '\theta / \pi', ...
'YLabel', 'S(\theta)', ...
'HighlightEvery', 10, ... % highlight every 10th repetition
'FontName', options.font, ...
'FigNum', 21, ...
'TileTitlePrefix', '\alpha', ... % user-defined tile prefix
'TileTitleSuffix', '^\circ', ... % user-defined suffix (e.g., degrees symbol)
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SpectralCurves.fig', ...
'SaveDirectory', figSaveDir);
%% ------------------ 9. Plot cumulants from shifted Angular Spectral Distribution Curves ------------------
Plotter.plotSpectralDistributionCumulants(results, ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'FontName', options.font, ...
'FontSize', 14, ...
'FigNum', 22, ...
'SkipSaveFigures', false, ...
'SaveFileName', 'SpectralCumulants.fig');
%% ------------------ 10. Fit shifted Angular Spectral Distribution Curves ------------------
fitResults = Analyzer.fitTwoGaussianCurves(...
results_all{1}.results.spectral_analysis_results.S_theta_norm_all, ...
results_all{1}.results.spectral_analysis_results.theta_vals, ...
'MaxTheta', pi/2, ...
'DeviationLimit', 1.00);
%% ------------------ 11. Plot fit parameters - position ------------------
Plotter.plotFitParameterPDF(fitResults, results_all{1}.scan_reference_values, 'mu2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak position (\theta, rad)', ...
'FontName', options.font, ...
'FontSize', 16, ...
'FigNum', 23, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakPositionPDF.fig', ...
'PlotType', 'histogram', ...
'NumberOfBins', 20, ...
'NormalizeHistogram', true, ...
'Colormap', @Colormaps.coolwarm, ...
'XLim', [min(results_all{1}.scan_reference_values), max(results_all{1}.scan_reference_values)]);
%% ------------------ 12. Plot fit parameters - width ------------------
Plotter.plotFitParameterPDF(fitResults, results_all{1}.scan_reference_values, 'sigma2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak width (\sigma, rad)', ...
'FontName', options.font, ...
'FontSize', 16, ...
'FigNum', 24, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakWidthPDF.fig', ...
'PlotType', 'histogram', ...
'NumberOfBins', 20, ...
'NormalizeHistogram', true, ...
'Colormap', @Colormaps.coolwarm, ...
'XLim', [min(results_all{1}.scan_reference_values), max(results_all{1}.scan_reference_values)]);
%% ------------------ 13. Plot fit parameters of mean shifted Angular Spectral Distribution Curves ------------------
S_theta_all = results_all{1}.results.spectral_analysis_results.S_theta_norm_all;
theta_vals = results_all{1}.results.spectral_analysis_results.theta_vals;
scanValues = results_all{1}.scan_reference_values;
N_params = numel(scanValues);
N_total = numel(S_theta_all);
N_reps = N_total / N_params;
theta_min = deg2rad(0);
theta_max = deg2rad(90);
% --- Shift curves so first peak is at start ---
S_theta_all_shifted = cell(size(S_theta_all));
for k = 1:N_total
curve = S_theta_all{k};
idx_range = find(theta_vals >= theta_min & theta_vals <= theta_max);
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 shorter curves with NaN to match lengths ---
Npoints_shifted = max(cellfun(@numel, S_theta_all_shifted));
for k = 1:N_total
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
% --- Compute mean curves per scan parameter ---
meanCurves = cell(1, N_params);
for i = 1:N_params
curves = zeros(N_reps, Npoints_shifted);
for r = 1:N_reps
idx = (r-1)*N_params + i; % interleaved indexing
curves(r,:) = S_theta_all_shifted{idx};
end
meanCurves{i} = nanmean(curves,1); % mean over repetitions
end
% --- Fit two-Gaussian model to mean curves ---
fitResultsMean = Analyzer.fitTwoGaussianCurves(meanCurves, theta_vals(1:Npoints_shifted)-theta_vals(1), ...
'MaxTheta', pi/2, ...
'DeviationLimit', 1.0);
% --- Scatter plot of secondary peak position (mu2) vs scan parameter ---
mu2_vals = nan(1, N_params);
sigma2_vals = nan(1, N_params);
for i = 1:N_params
if fitResultsMean(i).isValid
mu2_vals(i) = fitResultsMean(i).pFit(4); % secondary peak position
sigma2_vals(i) = fitResultsMean(i).pFit(5); % secondary peak width
end
end
% Secondary peak position
plotSecondaryPeakScatter(fitResultsMean, results_all{1}.scan_reference_values, 'mu2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak position (\theta, rad)', ...
'FigNum', 23, ...
'FontName', options.font, ...
'FontSize', 16, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakPositionScatter.fig');
% Secondary peak width
plotSecondaryPeakScatter(fitResultsMean, results_all{1}.scan_reference_values, 'sigma2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak width (\sigma, rad)', ...
'FigNum', 24, ...
'FontName', options.font, ...
'FontSize', 16, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakWidthScatter.fig');
function plotSecondaryPeakScatter(fitResultsMean, scanValues, parameterName, varargin)
%% plotSecondaryPeakScatter
% Author: Karthik
% Date: 2025-10-06
% Version: 1.0
%
% Description:
% Scatter plot of secondary peak fit parameters (mu2 or sigma2) vs scan parameter
% in the same style as plotMeanWithSE.
% --- Parse optional inputs ---
p = inputParser;
addParameter(p, 'Title', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'XLabel', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'YLabel', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'FigNum', [], @(x) isempty(x) || isscalar(x));
addParameter(p, 'FontName', 'Arial', @ischar);
addParameter(p, 'FontSize', 14, @isnumeric);
addParameter(p, 'YLim', [], @(x) isempty(x) || isnumeric(x));
addParameter(p, 'SkipSaveFigures', false, @islogical);
addParameter(p, 'SaveFileName', 'secondary_peak_scatter.fig', @ischar);
addParameter(p, 'SaveDirectory', pwd, @ischar);
parse(p, varargin{:});
opts = p.Results;
% --- Extract parameter values ---
N_params = numel(fitResultsMean);
paramVals = nan(1, N_params);
for i = 1:N_params
if fitResultsMean(i).isValid
switch parameterName
case 'mu2'
paramVals(i) = fitResultsMean(i).pFit(4);
case 'sigma2'
paramVals(i) = fitResultsMean(i).pFit(5);
otherwise
error('Unknown parameter name: %s', parameterName);
end
end
end
% --- Prepare figure ---
if isempty(opts.FigNum)
fig = figure;
else
fig = figure(opts.FigNum);
clf(fig);
end
set(fig, 'Color', 'w', 'Position', [100 100 950 750]);
% --- Plot as mean ± SE style (SE=0 here) ---
errorbar(scanValues, paramVals, zeros(size(paramVals)), 'o--', ...
'LineWidth', 1.8, 'MarkerSize', 6, 'CapSize', 5);
% --- Axis formatting ---
set(gca, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
if ~isempty(opts.YLim)
ylim(opts.YLim);
end
xlabel(opts.XLabel, 'Interpreter', 'tex', 'FontName', opts.FontName, 'FontSize', opts.FontSize);
ylabel(opts.YLabel, 'Interpreter', 'tex', 'FontName', opts.FontName, 'FontSize', opts.FontSize);
title(opts.Title, 'FontName', opts.FontName, 'FontSize', opts.FontSize + 2, 'FontWeight', 'bold');
grid on;
% --- Save figure ---
if ~opts.SkipSaveFigures
if ~exist(opts.SaveDirectory, 'dir'), mkdir(opts.SaveDirectory); end
savefig(fig, fullfile(opts.SaveDirectory, opts.SaveFileName));
end
end
%{
%% ------------------ 7. Average of Spectra Plots ------------------
%% ------------------ 14. Average of Spectra Plots ------------------
Plotter.plotAverageSpectra(scan_parameter_values, ...
spectral_analysis_results, ...
'ScanParameterName', scan_parameter, ...
'FigNum', 20, ...
'FigNum', 25, ...
'ColormapPS', Colormaps.coolwarm(), ...
'Font', 'Bahnschrift', ...
'SaveFileName', 'avgSpectra.fig', ...

View File

@ -98,7 +98,7 @@ end
[od_imgs, scan_parameter_values, scan_reference_values, file_list] = Helper.collectODImages(options);
%% Conduct correlation analysis
options.skipLivePlot = true;
g2_analysis_results = Analyzer.conductCorrelationAnalysis(od_imgs, scan_parameter_values, options);
%% Analyze G2 matrices
@ -108,23 +108,32 @@ options.roi.center = [3, 3]; % center of ROI in µm (x0, y0)
options.roi.size = [3, 11]; % width and height in µm
options.roi.angle = pi/4; % rotation angle (radians, CCW)
options.threshold = 0.85;
options.deviationThreshold = 0.30;
options.minEllipseFraction = 0.30;
options.angleLimit = deg2rad(45);
options.angleTolerance = deg2rad(5);
options.fitDeviationThreshold = 0.9;
% Plot control
options.skipLivePlot = false; % set true if you don't want per-image figures
options.skipLivePlot = true;
analysis_results = Analyzer.analyzeG2Structures(g2_analysis_results, options);
%% Plot raw OD images and the corresponding real space g2 matrix
options.skipLivePlot = true;
options.skipSaveFigures = false;
saveDirectory = 'C:\Users\Karthik-OfficePC\Documents\GitRepositories\Calculations\Data-Analyzer\+Scripts';
Plotter.plotODG2withAnalysis(od_imgs, scan_parameter_values, g2_analysis_results, analysis_results, options, ...
'FontName', options.font, ...
'FontSize', 14, ...
'ShowOverlays', true, ...
'RepsPerPage', 5, ... % paginate 10 repetitions per figure
'SaveDirectory', saveDirectory, ...
'SkipLivePlot', options.skipLivePlot, ...
'SkipSaveFigures', options.skipSaveFigures);
%% Plot mean and standard error of anisotropy
Plotter.plotMeanWithSE(scan_parameter_values, analysis_results.anisotropy_vals, ...
'Title', options.titleString, ...
'YLim', [0,1], ...
'YLim', [2,5], ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Anisotropy of correlation peaks', ...
'FigNum', 1, ...

View File

@ -188,13 +188,251 @@ Plotter.plotMultiplePCAResults(compiled_results.pca_results, scan_parameter_valu
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveDirectory', figSaveDir);
%% ------------------ 7. Plot of all Angular Spectral Distribution Curves ------------------
Plotter.plotSpectralCurves( ...
results_all{1}.results.spectral_analysis_results.S_theta_norm_all, ...
results_all{1}.results.spectral_analysis_results.theta_vals/pi, ... % correct θ values
results_all{1}.scan_reference_values, ... % correct scan params
'Title', options.titleString, ...
'XLabel', '\theta / \pi', ...
'YLabel', 'S(\theta)', ...
'HighlightEvery', 10, ... % highlight every 10th repetition
'FontName', options.font, ...
'FigNum', 20, ...
'TileTitlePrefix', '\alpha', ... % user-defined tile prefix
'TileTitleSuffix', '^\circ', ... % user-defined suffix (e.g., degrees symbol)
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SpectralCurves.fig', ...
'SaveDirectory', figSaveDir);
%% ------------------ 8. Plot of all Angular Spectral Distribution Curves shifted ------------------
results = Plotter.plotSpectralCurvesRecentered( ...
results_all{1}.results.spectral_analysis_results.S_theta_norm_all, ...
results_all{1}.results.spectral_analysis_results.theta_vals/pi, ... % correct θ values
results_all{1}.scan_reference_values, ... % correct scan params
'Title', options.titleString, ...
'XLabel', '\theta / \pi', ...
'YLabel', 'S(\theta)', ...
'HighlightEvery', 10, ... % highlight every 10th repetition
'FontName', options.font, ...
'FigNum', 21, ...
'TileTitlePrefix', '\alpha', ... % user-defined tile prefix
'TileTitleSuffix', '^\circ', ... % user-defined suffix (e.g., degrees symbol)
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SpectralCurves.fig', ...
'SaveDirectory', figSaveDir);
%% ------------------ 9. Plot cumulants from shifted Angular Spectral Distribution Curves ------------------
Plotter.plotSpectralDistributionCumulants(results, ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'FontName', options.font, ...
'FontSize', 14, ...
'FigNum', 22, ...
'SkipSaveFigures', false, ...
'SaveFileName', 'SpectralCumulants.fig');
%% ------------------ 10. Fit shifted Angular Spectral Distribution Curves ------------------
fitResults = Analyzer.fitTwoGaussianCurves(...
results_all{1}.results.spectral_analysis_results.S_theta_norm_all, ...
results_all{1}.results.spectral_analysis_results.theta_vals, ...
'MaxTheta', pi/2, ...
'DeviationLimit', 1.00);
%% ------------------ 11. Plot fit parameters - position ------------------
Plotter.plotFitParameterPDF(fitResults, results_all{1}.scan_reference_values, 'mu2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak position (\theta, rad)', ...
'FontName', options.font, ...
'FontSize', 16, ...
'FigNum', 23, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakPositionPDF.fig', ...
'PlotType', 'histogram', ...
'NumberOfBins', 20, ...
'NormalizeHistogram', true, ...
'Colormap', @Colormaps.coolwarm, ...
'XLim', [min(results_all{1}.scan_reference_values), max(results_all{1}.scan_reference_values)]);
%% ------------------ 12. Plot fit parameters - width ------------------
Plotter.plotFitParameterPDF(fitResults, results_all{1}.scan_reference_values, 'sigma2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak width (\sigma, rad)', ...
'FontName', options.font, ...
'FontSize', 16, ...
'FigNum', 24, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakWidthPDF.fig', ...
'PlotType', 'histogram', ...
'NumberOfBins', 20, ...
'NormalizeHistogram', true, ...
'Colormap', @Colormaps.coolwarm, ...
'XLim', [min(results_all{1}.scan_reference_values), max(results_all{1}.scan_reference_values)]);
%% ------------------ 13. Plot fit parameters of mean shifted Angular Spectral Distribution Curves ------------------
S_theta_all = results_all{1}.results.spectral_analysis_results.S_theta_norm_all;
theta_vals = results_all{1}.results.spectral_analysis_results.theta_vals;
scanValues = results_all{1}.scan_reference_values;
N_params = numel(scanValues);
N_total = numel(S_theta_all);
N_reps = N_total / N_params;
theta_min = deg2rad(0);
theta_max = deg2rad(90);
% --- Shift curves so first peak is at start ---
S_theta_all_shifted = cell(size(S_theta_all));
for k = 1:N_total
curve = S_theta_all{k};
idx_range = find(theta_vals >= theta_min & theta_vals <= theta_max);
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 shorter curves with NaN to match lengths ---
Npoints_shifted = max(cellfun(@numel, S_theta_all_shifted));
for k = 1:N_total
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
% --- Compute mean curves per scan parameter ---
meanCurves = cell(1, N_params);
for i = 1:N_params
curves = zeros(N_reps, Npoints_shifted);
for r = 1:N_reps
idx = (r-1)*N_params + i; % interleaved indexing
curves(r,:) = S_theta_all_shifted{idx};
end
meanCurves{i} = nanmean(curves,1); % mean over repetitions
end
% --- Fit two-Gaussian model to mean curves ---
fitResultsMean = Analyzer.fitTwoGaussianCurves(meanCurves, theta_vals(1:Npoints_shifted)-theta_vals(1), ...
'MaxTheta', pi/2, ...
'DeviationLimit', 1.0);
% --- Scatter plot of secondary peak position (mu2) vs scan parameter ---
mu2_vals = nan(1, N_params);
sigma2_vals = nan(1, N_params);
for i = 1:N_params
if fitResultsMean(i).isValid
mu2_vals(i) = fitResultsMean(i).pFit(4); % secondary peak position
sigma2_vals(i) = fitResultsMean(i).pFit(5); % secondary peak width
end
end
% Secondary peak position
plotSecondaryPeakScatter(fitResultsMean, results_all{1}.scan_reference_values, 'mu2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak position (\theta, rad)', ...
'FigNum', 23, ...
'FontName', options.font, ...
'FontSize', 16, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakPositionScatter.fig');
% Secondary peak width
plotSecondaryPeakScatter(fitResultsMean, results_all{1}.scan_reference_values, 'sigma2', ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Secondary peak width (\sigma, rad)', ...
'FigNum', 24, ...
'FontName', options.font, ...
'FontSize', 16, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'SecondaryPeakWidthScatter.fig');
function plotSecondaryPeakScatter(fitResultsMean, scanValues, parameterName, varargin)
%% plotSecondaryPeakScatter
% Author: Karthik
% Date: 2025-10-06
% Version: 1.0
%
% Description:
% Scatter plot of secondary peak fit parameters (mu2 or sigma2) vs scan parameter
% in the same style as plotMeanWithSE.
% --- Parse optional inputs ---
p = inputParser;
addParameter(p, 'Title', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'XLabel', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'YLabel', '', @(x) ischar(x) || isstring(x));
addParameter(p, 'FigNum', [], @(x) isempty(x) || isscalar(x));
addParameter(p, 'FontName', 'Arial', @ischar);
addParameter(p, 'FontSize', 14, @isnumeric);
addParameter(p, 'YLim', [], @(x) isempty(x) || isnumeric(x));
addParameter(p, 'SkipSaveFigures', false, @islogical);
addParameter(p, 'SaveFileName', 'secondary_peak_scatter.fig', @ischar);
addParameter(p, 'SaveDirectory', pwd, @ischar);
parse(p, varargin{:});
opts = p.Results;
% --- Extract parameter values ---
N_params = numel(fitResultsMean);
paramVals = nan(1, N_params);
for i = 1:N_params
if fitResultsMean(i).isValid
switch parameterName
case 'mu2'
paramVals(i) = fitResultsMean(i).pFit(4);
case 'sigma2'
paramVals(i) = fitResultsMean(i).pFit(5);
otherwise
error('Unknown parameter name: %s', parameterName);
end
end
end
% --- Prepare figure ---
if isempty(opts.FigNum)
fig = figure;
else
fig = figure(opts.FigNum);
clf(fig);
end
set(fig, 'Color', 'w', 'Position', [100 100 950 750]);
% --- Plot as mean ± SE style (SE=0 here) ---
errorbar(scanValues, paramVals, zeros(size(paramVals)), 'o--', ...
'LineWidth', 1.8, 'MarkerSize', 6, 'CapSize', 5);
% --- Axis formatting ---
set(gca, 'FontName', opts.FontName, 'FontSize', opts.FontSize);
if ~isempty(opts.YLim)
ylim(opts.YLim);
end
xlabel(opts.XLabel, 'Interpreter', 'tex', 'FontName', opts.FontName, 'FontSize', opts.FontSize);
ylabel(opts.YLabel, 'Interpreter', 'tex', 'FontName', opts.FontName, 'FontSize', opts.FontSize);
title(opts.Title, 'FontName', opts.FontName, 'FontSize', opts.FontSize + 2, 'FontWeight', 'bold');
grid on;
% --- Save figure ---
if ~opts.SkipSaveFigures
if ~exist(opts.SaveDirectory, 'dir'), mkdir(opts.SaveDirectory); end
savefig(fig, fullfile(opts.SaveDirectory, opts.SaveFileName));
end
end
%{
%% ------------------ 7. Average of Spectra Plots ------------------
%% ------------------ 14. Average of Spectra Plots ------------------
Plotter.plotAverageSpectra(scan_parameter_values, ...
spectral_analysis_results, ...
'ScanParameterName', scan_parameter, ...
'FigNum', 20, ...
'FigNum', 25, ...
'ColormapPS', Colormaps.coolwarm(), ...
'Font', 'Bahnschrift', ...
'SaveFileName', 'avgSpectra.fig', ...

View File

@ -0,0 +1,192 @@
%% ===== BEC-Stripes-Droplets Settings =====
% Specify data location to run analysis on
dataSources = {
struct('sequence', 'TwoDGas', ...
'date', '2025/06/24', ...
'runs', [1]) % specify run numbers as a string in "" or just as a numeric value
};
options = struct();
% File paths
options.baseDataFolder = '//DyLabNAS/Data';
options.FullODImagesFolder = 'E:/Data - Experiment/FullODImages/202506';
options.measurementName = 'StripesToDroplets';
scriptFullPath = mfilename('fullpath');
options.saveDirectory = fileparts(scriptFullPath);
% Camera / imaging settings
options.cam = 4; % 1 - ODT_1_Axis_Camera; 2 - ODT_2_Axis_Camera; 3 - Horizontal_Axis_Camera;, 4 - Vertical_Axis_Camera;
options.angle = 0; % angle by which image will be rotated
options.center = [1410, 2030];
options.span = [200, 200];
options.fraction = [0.1, 0.1];
options.pixel_size = 5.86e-6; % in meters
options.magnification = 23.94;
options.ImagingMode = 'HighIntensity';
options.PulseDuration = 5e-6; % in s
% Fourier analysis settings
options.theta_min = deg2rad(0);
options.theta_max = deg2rad(180);
options.N_radial_bins = 500;
options.Radial_Sigma = 2;
options.Radial_WindowSize = 5; % odd number
options.k_min = 1.2771; % μm¹
options.k_max = 2.5541; % μm¹
options.N_angular_bins = 180;
options.Angular_Threshold = 75;
options.Angular_Sigma = 2;
options.Angular_WindowSize = 5;
options.zoom_size = 50;
%
options.maximumShift = 8;
options.Radial_Theta = deg2rad(45);
options.Radial_Minimum = 2;
options.Radial_Maximum = 6;
options.skipLivePlot = false;
% Flags
options.skipUnshuffling = false;
options.skipNormalization = false;
options.skipFringeRemoval = true;
options.skipPreprocessing = true;
options.skipMasking = true;
options.skipIntensityThresholding = true;
options.skipBinarization = true;
options.skipFullODImagesFolderUse = false;
options.skipSaveData = false;
options.skipSaveFigures = true;
options.skipSaveProcessedOD = true;
options.skipLivePlot = false;
options.showProgressBar = true;
% Extras
options.font = 'Bahnschrift';
switch options.measurementName
case 'BECToDroplets'
options.scan_parameter = 'rot_mag_field';
options.flipSortOrder = false;
options.scanParameterUnits = 'gauss';
options.titleString = 'BEC to Droplets';
case 'BECToStripes'
options.scan_parameter = 'rot_mag_field';
options.flipSortOrder = false;
options.scanParameterUnits = 'gauss';
options.titleString = 'BEC to Stripes';
case 'DropletsToStripes'
options.scan_parameter = 'ps_rot_mag_fin_pol_angle';
options.flipSortOrder = false;
options.scanParameterUnits = 'degrees';
options.titleString = 'Droplets to Stripes';
case 'StripesToDroplets'
options.scan_parameter = 'ps_rot_mag_fin_pol_angle';
options.flipSortOrder = false;
options.scanParameterUnits = 'degrees';
options.titleString = 'Stripes to Droplets';
end
%% ===== Collect Images and Launch Viewer =====
[options.selectedPath, options.folderPath] = Helper.selectDataSourcePath(dataSources, options);
[od_imgs, scan_parameter_values, scan_reference_values, file_list] = Helper.collectODImages(options);
%% Conduct correlation analysis
g2_analysis_results = Analyzer.conductCorrelationAnalysis(od_imgs, scan_parameter_values, options);
%% Analyze G2 matrices
% ROI definition
options.roi.center = [3, 3]; % center of ROI in µm (x0, y0)
options.roi.size = [3, 11]; % width and height in µm
options.roi.angle = pi/4; % rotation angle (radians, CCW)
options.threshold = 0.85;
options.deviationThreshold = 0.30;
options.minEllipseFraction = 0.30;
options.angleLimit = deg2rad(45);
options.angleTolerance = deg2rad(5);
% Plot control
options.skipLivePlot = true;
analysis_results = Analyzer.analyzeG2Structures(g2_analysis_results, options);
%% Plot raw OD images and the corresponding real space g2 matrix
options.skipLivePlot = true;
options.skipSaveFigures = false;
saveDirectory = 'C:\Users\Karthik-OfficePC\Documents\GitRepositories\Calculations\Data-Analyzer\+Scripts';
Plotter.plotODG2withAnalysis(od_imgs, scan_parameter_values, g2_analysis_results, analysis_results, options, ...
'FontName', options.font, ...
'FontSize', 14, ...
'ShowOverlays', true, ...
'RepsPerPage', 5, ... % paginate 10 repetitions per figure
'SaveDirectory', saveDirectory, ...
'SkipLivePlot', options.skipLivePlot, ...
'SkipSaveFigures', options.skipSaveFigures);
%% Plot mean and standard error of anisotropy
Plotter.plotMeanWithSE(scan_parameter_values, analysis_results.anisotropy_vals, ...
'Title', options.titleString, ...
'YLim', [0,1], ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Anisotropy of correlation peaks', ...
'FigNum', 1, ...
'FontName', options.font, ...
'SaveFileName', 'RadialSpectralContrast.fig', ...
'SaveDirectory', pwd, ... % save figures inside dataset-specific folder
'SkipSaveFigures', options.skipSaveFigures);
%% Plot distribution of anisotropy
grouped_data = groupDataByScan(scan_parameter_values, analysis_results.anisotropy_vals);
% call plotPDF
Plotter.plotPDF(grouped_data, ...
scan_reference_values, ...
'Title', options.titleString, ...
'XLabel', '\alpha (degrees)', ...
'YLabel', 'Anisotropy of correlation peaks', ...
'FigNum', 2, ...
'FontName', options.font, ...
'SkipSaveFigures', options.skipSaveFigures, ...
'SaveFileName', 'PDF_MaxG2AcrossTransition.fig', ...
'SaveDirectory', pwd, ...
'NumberOfBins', 10, ...
'NormalizeHistogram', true, ...
'DataRange', [0 1.0], ...
'Colormap', @Colormaps.coolwarm, ...
'XLim', [min(scan_reference_values) max(scan_reference_values)]);
function groupedData = groupDataByScan(scan_values, data_values)
%% groupByScanValues
% Groups data according to unique scan parameter values.
%
% Inputs:
% scan_values : array of scan parameters (length = N_reps * N_scan)
% data_values : numeric array or cell array of measured values
% (same length as scan_values)
%
% Output:
% groupedData : 1 x N_unique cell array, each containing all repetitions
% corresponding to a unique scan value
[unique_vals, ~, idx] = unique(scan_values, 'stable'); % preserve order
groupedData = cell(1, numel(unique_vals));
for i = 1:numel(unique_vals)
if iscell(data_values)
group = data_values(idx == i);
groupedData{i} = [group{:}]; % concatenate if nested cells
else
groupedData{i} = data_values(idx == i);
end
end
end