Calculations/Data-Analyzer/Deprecated/analyzePhaseTransitionSimulation.m

801 lines
29 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.

%% Extract Images
baseDir = 'D:/Results - Numerics/Data_Full3D/PhaseTransition/DTS/Point2';
JobNumber = 0;
runFolder = sprintf('Run_%03d', JobNumber);
savefileName = 'DropletsToStripes'; % Output file name
datafileName = './DropletsToStripes.mat';
reverseOrder = false; % Set this to true to reverse the theta ordering
TitleString = 'Droplets To Stripes';
%%
baseDir = 'D:/Results - Numerics/Data_Full3D/PhaseTransition/STD/Point2';
JobNumber = 0;
runFolder = sprintf('Run_%03d', JobNumber);
savefileName = 'StripesToDroplets'; % Output file name
datafileName = './StripesToDroplets.mat';
reverseOrder = true; % Set this to true to reverse the theta ordering
TitleString = 'Stripes To Droplets';
%%
folderList = dir(baseDir);
isValid = [folderList.isdir] & ~ismember({folderList.name}, {'.', '..'});
folderNames = {folderList(isValid).name};
nimgs = numel(folderNames);
% Extract theta values from folder names
PolarAngleVals = zeros(1, nimgs);
for k = 1:nimgs
tokens = regexp(folderNames{k}, 'theta_(\d{3})', 'tokens');
if isempty(tokens)
warning('No theta found in folder name: %s', folderNames{k});
PolarAngleVals(k) = NaN;
else
PolarAngleVals(k) = str2double(tokens{1}{1});
end
end
% Choose sort direction
sortDirection = 'ascend';
if reverseOrder
sortDirection = 'descend';
end
% Sort folderNames based on polar angle
[~, sortIdx] = sort(PolarAngleVals, sortDirection);
folderNames = folderNames(sortIdx);
PolarAngleVals = PolarAngleVals(sortIdx); % Optional: if you still want sorted list
imgs = cell(1, nimgs);
alphas = zeros(1, nimgs);
for k = 1:nimgs
folderName = folderNames{k};
SaveDirectory = fullfile(baseDir, folderName, runFolder);
% Extract alpha (theta) again from folder name
tokens = regexp(folderName, 'theta_(\d{3})', 'tokens');
alpha_val = str2double(tokens{1}{1});
alphas(k) = alpha_val;
matPath = fullfile(SaveDirectory, 'psi_gs.mat');
if ~isfile(matPath)
warning('Missing psi_gs.mat in %s', SaveDirectory);
continue;
end
try
Data = load(matPath, 'psi', 'Params', 'Transf', 'Observ');
catch ME
warning('Failed to load %s: %s', matPath, ME.message);
continue;
end
Params = Data.Params;
Transf = Data.Transf;
Observ = Data.Observ;
psi = Data.psi;
if isgpuarray(psi)
psi = gather(psi);
end
if isgpuarray(Observ.residual)
Observ.residual = gather(Observ.residual);
end
% Axes and projection
z = Transf.z * Params.l0 * 1e6;
dz = z(2)-z(1);
if k == 1
x = Transf.x * Params.l0 * 1e6;
y = Transf.y * Params.l0 * 1e6;
% Calculate frequency increment (frequency axes)
Nx = length(x); % grid size along X
Ny = length(y); % grid size along Y
dx = mean(diff(x)); % real space increment in the X direction (in micrometers)
dy = mean(diff(y)); % real space increment in the Y direction (in micrometers)
dvx = 1 / (Nx * dx); % reciprocal space increment in the X direction (in micrometers^-1)
dvy = 1 / (Ny * dy); % reciprocal space increment in the Y direction (in micrometers^-1)
% Create the frequency axes
vx = (-Nx/2:Nx/2-1) * dvx; % Frequency axis in X (micrometers^-1)
vy = (-Ny/2:Ny/2-1) * dvy; % Frequency axis in Y (micrometers^-1)
% Create the Wavenumber axes
kx_full = 2*pi*vx; % Wavenumber axis in X
ky_full = 2*pi*vy; % Wavenumber axis in Y
end
n = abs(psi).^2;
nxy = squeeze(trapz(n * dz, 3));
imgs{k} = nxy;
end
%% Analyze Images
skipMovieRender = true; % Set to true to disable movie creation
skipSaveFigures = false;
font = 'Bahnschrift';
skipPreprocessing = true;
skipMasking = true;
skipIntensityThresholding = true;
skipBinarization = true;
% Run Fourier analysis over images
fft_imgs = cell(1, nimgs);
angular_spectral_distribution = cell(1, nimgs);
theta_values = cell(1, nimgs);
radial_spectral_contrast = zeros(1, nimgs);
angular_spectral_weight = zeros(1, nimgs);
g2_all = cell(1, nimgs);
theta_values_all = cell(1, nimgs);
N_shots = length(imgs);
ps_list = cell(1, N_shots); % 2D power spectrum
s_k_list = cell(1, N_shots); % Radial spectrum
s_theta_list = cell(1, N_shots); % Angular spectrum
% Fourier analysis settings
% Radial Spectral Distribution
theta_min = deg2rad(0);
theta_max = deg2rad(180);
N_radial_bins = 500;
Radial_Sigma = 2;
Radial_WindowSize = 5; % Choose an odd number for a centered moving average
% Angular Spectral Distribution
k_min = 1.2771; % in μm⁻¹
k_max = 2.5541; % in μm⁻¹
N_angular_bins = 180;
Angular_Threshold = 25;
Angular_Sigma = 2;
Angular_WindowSize = 5;
zoom_size = 50; % Zoomed-in region around center
if ~skipMovieRender
% Create VideoWriter object for movie
videoFile = VideoWriter([savefileName '.mp4'], 'MPEG-4');
videoFile.Quality = 100; % Set quality to maximum (0100)
videoFile.FrameRate = 2; % Set the frame rate (frames per second)
open(videoFile); % Open the video file to write
end
if ~skipSaveFigures
% Define folder for saving images
saveFolder = [savefileName '_SavedFigures'];
if ~exist(saveFolder, 'dir')
mkdir(saveFolder);
end
end
% Display the cropped image
for k = 1:nimgs
IMG = imgs{k};
if ~(max(IMG(:)) > 1)
IMGFFT = NaN(size(IMG));
else
[IMGFFT, IMGPR] = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization);
end
% Crop FFT image around center
mid_x = floor(Nx/2);
mid_y = floor(Ny/2);
fft_imgs{k} = IMGFFT(mid_y-zoom_size:mid_y+zoom_size, mid_x-zoom_size:mid_x+zoom_size);
% Crop wavenumber axes to match fft_imgs{k}
kx = kx_full(mid_x - zoom_size : mid_x + zoom_size);
ky = ky_full(mid_y - zoom_size : mid_y + zoom_size);
[theta_vals, S_theta] = computeAngularSpectralDistribution(fft_imgs{k}, kx, ky, k_min, k_max, N_angular_bins, Angular_Threshold, Angular_Sigma, []);
[k_rho_vals, S_k] = computeRadialSpectralDistribution(fft_imgs{k}, kx, ky, theta_min, theta_max, N_radial_bins);
S_k_smoothed = movmean(S_k, Radial_WindowSize); % Compute moving average (use convolution) or use conv for more control
angular_spectral_distribution{k} = S_theta;
theta_values{k} = theta_vals;
radial_spectral_contrast(k) = computeRadialSpectralContrast(k_rho_vals, S_k_smoothed, k_min, k_max);
S_theta_norm = S_theta / max(S_theta); % Normalize to 1
angular_spectral_weight(k) = trapz(theta_vals, S_theta_norm);
g2 = zeros(1, N_angular_bins); % Preallocate
for dtheta = 0:N_angular_bins-1
profile = S_theta;
profile_shifted = circshift(profile, -dtheta, 2);
num = mean(profile .* profile_shifted);
denom = mean(profile.^2);
g2(dtheta+1) = num / denom;
end
ps_list{k} = abs(fft_imgs{k}).^2; % store the power spectrum
s_k_list{k} = S_k_smoothed; % store smoothed radial spectrum
s_theta_list{k} = S_theta; % store angular spectrum
g2_all{k} = g2;
theta_values_all{k} = theta_vals;
figure(1);
clf
set(gcf,'Position',[500 100 1000 800])
t = tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'compact'); % 1x4 grid
y_min = min(y);
y_max = max(y);
x_min = min(x);
x_max = max(x);
% Display the cropped OD image
ax1 = nexttile;
imagesc(x, y, IMG')
% Define normalized positions (relative to axis limits)
x_offset = 0.025; % 5% offset from the edges
y_offset = 0.025; % 5% offset from the edges
% Top-right corner (normalized axis coordinates)
hText = text(1 - x_offset, 1 - y_offset, ['Angle = ', num2str(alphas(k), '%.1f')], ...
'Color', 'white', 'FontWeight', 'bold', 'Interpreter', 'tex', 'FontSize', 20, 'Units', 'normalized', 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top');
axis square;
hcb = colorbar;
colormap(ax1, Colormaps.plasma());
set(gca, 'FontSize', 14); % For tick labels only
hL = ylabel(hcb, 'Optical Density');
set(hL,'Rotation',-90);
set(gca,'YDir','normal')
% set(gca, 'YTick', linspace(y_min, y_max, 5)); % Define y ticks
% set(gca, 'YTickLabel', flip(linspace(y_min, y_max, 5))); % Flip only the labels
hXLabel = xlabel('x (pixels)', 'Interpreter', 'tex');
hYLabel = ylabel('y (pixels)', 'Interpreter', 'tex');
hTitle = title('Density', 'Interpreter', 'tex');
set([hXLabel, hYLabel, hL, hText], 'FontName', font)
set([hXLabel, hYLabel, hL], 'FontSize', 14)
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold'); % Set font and size for title
% ======= FFT POWER SPECTRUM (reciprocal space) =======
ax2 = nexttile;
imagesc(kx, ky, log(1 + ps_list{k}));
axis tight;
set(gca, 'FontSize', 14, 'YDir', 'normal')
xlabel('k_x [\mum^{-1}]', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
ylabel('k_y [\mum^{-1}]', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
title('Power Spectrum - S(k_x,k_y)', 'Interpreter', 'tex', ...
'FontSize', 16, 'FontWeight', 'bold', 'FontName', font);
colorbar;
colormap(ax2, Colormaps.coolwarm());
% ======= ANGULAR DISTRIBUTION (S(θ)) =======
nexttile;
plot(theta_vals/pi, S_theta_norm, 'LineWidth', 2);
axis image;
set(gca, 'FontSize', 14, 'YLim', [0, 1]);
xlabel('\theta/\pi [rad]', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
ylabel('Magnitude (a.u.)', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
title('Angular Spectral Distribution - S(\theta)', 'Interpreter', 'tex', ...
'FontSize', 16, 'FontWeight', 'bold', 'FontName', font);
grid on; % Enable major grid
ax = gca;
ax.MinorGridLineStyle = ':'; % Optional: make minor grid dotted
ax.MinorGridColor = [0.7 0.7 0.7]; % Optional: light gray minor grid color
ax.MinorGridAlpha = 0.5; % Optional: transparency for minor grid
ax.XMinorGrid = 'on'; % Enable minor grid for x-axis
ax.YMinorGrid = 'on'; % Enable minor grid for y-axis (if desired)
% ======= ANGULAR CORRELATION g2(θ) =======
nexttile
plot(theta_vals/pi, g2, 'o-', 'LineWidth', 1.2, 'MarkerSize', 5);
set(gca, 'FontSize', 14);
ylim([0.0 1.0]); % Set y-axis limits here
hXLabel = xlabel('$\delta\theta / \pi$', 'Interpreter', 'latex');
hYLabel = ylabel('$g^{(2)}(\delta\theta)$', 'Interpreter', 'latex');
hTitle = title('Angular Correlation', 'Interpreter', 'tex');
set([hXLabel, hYLabel], 'FontName', font)
set([hXLabel, hYLabel], 'FontSize', 14)
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold'); % Set font and size for title
grid on;
if ~skipMovieRender
% Capture the current frame and write it to the video
frame = getframe(gcf); % Capture the current figure as a frame
writeVideo(videoFile, frame); % Write the frame to the video
end
if ~skipSaveFigures
% Construct a filename for each image
fileNamePNG = fullfile(saveFolder, sprintf('fft_analysis_img_%03d.png', k));
% Save current figure as PNG with high resolution
print(gcf, fileNamePNG, '-dpng', '-r100'); % 300 dpi for high quality
end
if skipMovieRender & skipSaveFigures
pause(0.5);
end
end
if ~skipMovieRender
% Close the video file
close(videoFile);
disp(['Movie saved to ', savefileName]);
end
%% Track across the transition
% Convert spectral distribution to matrix (N_shots x N_angular_bins)
delta_nkr_all = zeros(N_shots, N_angular_bins);
for k = 1:N_shots
delta_nkr_all(k, :) = angular_spectral_distribution{k};
end
N_params = length(alphas);
% Define angular range and conversion
angle_range = 180;
angle_per_bin = angle_range / N_angular_bins;
max_peak_angle = 180;
max_peak_bin = round(max_peak_angle / angle_per_bin);
% Parameters for search
window_size = 10;
angle_threshold = 100;
% Initialize containers for final results
mean_max_g2_values = zeros(1, N_params);
mean_max_g2_angle_values = zeros(1, N_params);
var_max_g2_values = zeros(1, N_params);
var_max_g2_angle_values = zeros(1, N_params);
% Also store raw data per group
g2_all_per_group = cell(1, N_params);
angle_all_per_group = cell(1, N_params);
for i = 1:N_params
group_data = delta_nkr_all(i, :);
N_reps = size(group_data, 1);
g2_values = zeros(1, N_reps);
angle_at_max_g2 = zeros(1, N_reps);
for j = 1:N_reps
profile = group_data(j, :);
% Restrict search to 060° for highest peak
restricted_profile = profile(1:max_peak_bin);
[~, peak_idx_rel] = max(restricted_profile);
peak_idx = peak_idx_rel;
peak_angle = (peak_idx - 1) * angle_per_bin;
if peak_angle < angle_threshold
offsets = round(50 / angle_per_bin) : round(70 / angle_per_bin);
else
offsets = -round(70 / angle_per_bin) : -round(50 / angle_per_bin);
end
ref_window = mod((peak_idx - window_size):(peak_idx + window_size) - 1, N_angular_bins) + 1;
ref = profile(ref_window);
correlations = zeros(size(offsets));
angles = zeros(size(offsets));
for k = 1:length(offsets)
shifted_idx = mod(peak_idx + offsets(k) - 1, N_angular_bins) + 1;
sec_window = mod((shifted_idx - window_size):(shifted_idx + window_size) - 1, N_angular_bins) + 1;
sec = profile(sec_window);
num = mean(ref .* sec);
denom = mean(ref.^2);
g2 = num / denom;
correlations(k) = g2;
angles(k) = mod((peak_idx - 1 + offsets(k)) * angle_per_bin, angle_range);
end
[max_corr, max_idx] = max(correlations);
g2_values(j) = max_corr;
angle_at_max_g2(j) = angles(max_idx);
end
% Store raw values
g2_all_per_group{i} = g2_values;
angle_all_per_group{i} = angle_at_max_g2;
% Final stats
mean_max_g2_values(i) = mean(g2_values, 'omitnan');
var_max_g2_values(i) = var(g2_values, 0, 'omitnan');
mean_max_g2_angle_values(i)= mean(angle_at_max_g2, 'omitnan');
var_max_g2_angle_values(i) = var(angle_at_max_g2, 0, 'omitnan');
end
% Plot peak offset angular correlation
figure(2);
set(gcf,'Position',[100 100 950 750])
set(gca, 'FontSize', 14); % For tick labels only
plot(alphas, ... % x-axis
mean_max_g2_values, ... % y-axis (mean)
'--o', 'LineWidth', 1.8, 'MarkerSize', 6 );
set(gca, 'FontSize', 14, 'YLim', [0, 1]);
hXLabel = xlabel('$\alpha$ (degrees)', 'Interpreter', 'latex');
hYLabel = ylabel('$\mathrm{max}[g^{(2)}_{[50,70]}(\delta\theta)]$', 'Interpreter', 'latex');
hTitle = title(TitleString, 'Interpreter', 'tex');
set([hXLabel, hYLabel], 'FontName', font);
set([hXLabel, hYLabel], 'FontSize', 14);
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');
grid on;
% Plot full angular correlation
figure(3);
clf;
set(gcf,'Position',[100 100 950 750])
hold on;
% Reconstruct theta axis from any one of the stored values
theta_vals = theta_values_all{1}; % assuming it's in radians
legend_entries = cell(nimgs, 1);
% Generate a colormap with enough unique colors
cmap = sky(nimgs); % You can also try 'jet', 'turbo', 'hot', etc.
for i = 1:nimgs
plot(theta_vals/pi, g2_all{i}, ...
'o-', 'Color', cmap(i,:), 'LineWidth', 1.2, ...
'MarkerSize', 5);
legend_entries{i} = sprintf('$\\alpha = %g^\\circ$', alphas(i));
end
ylim([0.0 1.0]); % Set y-axis limits here
set(gca, 'FontSize', 14);
hXLabel = xlabel('$\delta\theta / \pi$', 'Interpreter', 'latex');
hYLabel = ylabel('$g^{(2)}(\delta\theta)$', 'Interpreter', 'latex');
hTitle = title(TitleString, 'Interpreter', 'tex');
legend(legend_entries, 'Interpreter', 'latex', 'Location', 'bestoutside');
set([hXLabel, hYLabel], 'FontName', font)
set([hXLabel, hYLabel], 'FontSize', 14)
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold'); % Set font and size for title
grid on;
save(datafileName, 'alphas', 'mean_max_g2_values', 'theta_vals', 'g2_all');
%% Compare between transitions
set(0,'defaulttextInterpreter','latex')
set(groot, 'defaultAxesTickLabelInterpreter','latex'); set(groot, 'defaultLegendInterpreter','latex');
format long
font = 'Bahnschrift';
% Load data
Data = load('./DropletsToStripes.mat', 'alphas', 'mean_max_g2_values');
dts_alphas = Data.alphas;
dts_max_g2 = Data.mean_max_g2_values;
Data = load('./StripesToDroplets.mat', 'alphas', 'mean_max_g2_values');
std_alphas = Data.alphas;
std_max_g2 = Data.mean_max_g2_values;
figure(5);
set(gcf,'Position',[100 100 950 750])
plot(dts_alphas, dts_max_g2, 'o--', 'LineWidth', 1.5, 'MarkerSize', 6, 'DisplayName' , 'Droplets to Stripes');
hold on
plot(std_alphas, std_max_g2, 'o--', 'LineWidth', 1.5, 'MarkerSize', 6, 'DisplayName' , 'Stripes to Droplets');
set(gca, 'FontSize', 14); % For tick labels only
hXLabel = xlabel('$\alpha$ (degrees)', 'Interpreter', 'latex');
hYLabel = ylabel('$\mathrm{max}[g^{(2)}_{[50,70]}(\delta\theta)]$', 'Interpreter', 'latex');
hTitle = title('Change across transition', 'Interpreter', 'tex');
legend
set([hXLabel, hYLabel], 'FontName', font)
set([hXLabel, hYLabel], 'FontSize', 14)
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold'); % Set font and size for title
grid on
%%
function [IMGFFT, IMGPR] = computeFourierTransform(I, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization)
% computeFourierSpectrum - Computes the 2D Fourier power spectrum
% of binarized and enhanced lattice image features, with optional central mask.
%
% Inputs:
% I - Grayscale or RGB image matrix
%
% Output:
% F_mag - 2D Fourier power spectrum (shifted)
if ~skipPreprocessing
% Preprocessing: Denoise
filtered = imgaussfilt(I, 10);
IMGPR = I - filtered; % adjust sigma as needed
else
IMGPR = I;
end
if ~skipMasking
[rows, cols] = size(IMGPR);
[X, Y] = meshgrid(1:cols, 1:rows);
% Elliptical mask parameters
cx = cols / 2;
cy = rows / 2;
% Shifted coordinates
x = X - cx;
y = Y - cy;
% Ellipse semi-axes
rx = 0.4 * cols;
ry = 0.2 * rows;
% Rotation angle in degrees -> radians
theta_deg = 30; % Adjust as needed
theta = deg2rad(theta_deg);
% Rotated ellipse equation
cos_t = cos(theta);
sin_t = sin(theta);
x_rot = (x * cos_t + y * sin_t);
y_rot = (-x * sin_t + y * cos_t);
ellipseMask = (x_rot.^2) / rx^2 + (y_rot.^2) / ry^2 <= 1;
% Apply cutout mask
IMGPR = IMGPR .* ellipseMask;
end
if ~skipIntensityThresholding
% Apply global intensity threshold mask
intensity_thresh = 0.20;
intensity_mask = IMGPR > intensity_thresh;
IMGPR = IMGPR .* intensity_mask;
end
if ~skipBinarization
% Adaptive binarization and cleanup
IMGPR = imbinarize(IMGPR, 'adaptive', 'Sensitivity', 0.0);
IMGPR = imdilate(IMGPR, strel('disk', 2));
IMGPR = imerode(IMGPR, strel('disk', 1));
IMGPR = imfill(IMGPR, 'holes');
F = fft2(double(IMGPR)); % Compute 2D Fourier Transform
IMGFFT = abs(fftshift(F))'; % Shift zero frequency to center
else
F = fft2(double(IMGPR)); % Compute 2D Fourier Transform
IMGFFT = abs(fftshift(F))'; % Shift zero frequency to center
end
end
function [k_rho_vals, S_radial] = computeRadialSpectralDistribution(IMGFFT, kx, ky, thetamin, thetamax, num_bins)
% IMGFFT : 2D FFT image (fftshifted and cropped)
% kx, ky : 1D physical wavenumber axes [μm⁻¹] matching FFT size
% thetamin : Minimum angle (in radians)
% thetamax : Maximum angle (in radians)
% num_bins : Number of radial bins
[KX, KY] = meshgrid(kx, ky);
K_rho = sqrt(KX.^2 + KY.^2);
Theta = atan2(KY, KX);
if thetamin < thetamax
angle_mask = (Theta >= thetamin) & (Theta <= thetamax);
else
angle_mask = (Theta >= thetamin) | (Theta <= thetamax);
end
power_spectrum = abs(IMGFFT).^2;
r_min = min(K_rho(angle_mask));
r_max = max(K_rho(angle_mask));
r_edges = linspace(r_min, r_max, num_bins + 1);
k_rho_vals = 0.5 * (r_edges(1:end-1) + r_edges(2:end));
S_radial = zeros(1, num_bins);
for i = 1:num_bins
r_low = r_edges(i);
r_high = r_edges(i + 1);
radial_mask = (K_rho >= r_low) & (K_rho < r_high);
full_mask = radial_mask & angle_mask;
S_radial(i) = sum(power_spectrum(full_mask));
end
end
function [theta_vals, S_theta] = computeAngularSpectralDistribution(IMGFFT, kx, ky, k_min, k_max, num_bins, threshold, sigma, windowSize)
% Apply threshold to isolate strong peaks
IMGFFT(IMGFFT < threshold) = 0;
% Create wavenumber meshgrid
[KX, KY] = meshgrid(kx, ky);
Kmag = sqrt(KX.^2 + KY.^2); % radial wavenumber magnitude
Theta = atan2(KY, KX); % range [-pi, pi]
% Restrict to radial band in wavenumber space
radial_mask = (Kmag >= k_min) & (Kmag <= k_max);
% Initialize angular structure factor
S_theta = zeros(1, num_bins);
theta_vals = linspace(0, pi, num_bins); % only 0 to pi due to symmetry
% Loop over angular bins
for i = 1:num_bins
angle_start = (i - 1) * pi / num_bins;
angle_end = i * pi / num_bins;
angle_mask = (Theta >= angle_start) & (Theta < angle_end);
bin_mask = radial_mask & angle_mask;
fft_angle = IMGFFT .* bin_mask;
S_theta(i) = sum(sum(abs(fft_angle).^2));
end
% Optional smoothing
if exist('sigma', 'var') && ~isempty(sigma)
% Gaussian smoothing
half_width = ceil(3 * sigma);
x = -half_width:half_width;
gauss_kernel = exp(-x.^2 / (2 * sigma^2));
gauss_kernel = gauss_kernel / sum(gauss_kernel);
% Circular convolution
S_theta = conv([S_theta(end - half_width + 1:end), S_theta, S_theta(1:half_width)], ...
gauss_kernel, 'same');
S_theta = S_theta(half_width + 1:end - half_width);
elseif exist('windowSize', 'var') && ~isempty(windowSize)
% Moving average smoothing
pad = floor(windowSize / 2);
kernel = ones(1, windowSize) / windowSize;
S_theta = conv([S_theta(end - pad + 1:end), S_theta, S_theta(1:pad)], kernel, 'same');
S_theta = S_theta(pad + 1:end - pad);
end
end
function contrast = computeRadialSpectralContrast(k_rho_vals, S_k_smoothed, k_min, k_max)
% Computes the ratio of the peak in S_k_smoothed within [k_min, k_max]
% to the value at (or near) k = 0.
% Ensure inputs are column vectors
k_rho_vals = k_rho_vals(:);
S_k_smoothed = S_k_smoothed(:);
% Step 1: Find index of k ≈ 0
[~, idx_k0] = min(abs(k_rho_vals)); % Closest to zero
S_k0 = S_k_smoothed(idx_k0);
% Step 2: Find indices in specified k-range
in_range = (k_rho_vals >= k_min) & (k_rho_vals <= k_max);
if ~any(in_range)
warning('No values found in the specified k-range. Returning NaN.');
contrast = NaN;
return;
end
% Step 3: Find peak value in the specified k-range
S_k_peak = max(S_k_smoothed(in_range));
% Step 4: Compute contrast
contrast = S_k_peak / S_k0;
end
function drawPSOverlays(kx, ky, r_min, r_max)
% drawFFTOverlays - Draw overlays on existing FFT plot:
% - Radial lines every 30°
% - Annular highlight with white (upper half) and gray (lower half) circles between r_min and r_max
% - Horizontal white bands at ky=0 in annulus region
% - Scale ticks and labels every 1 μm⁻¹ along each radial line
%
% Inputs:
% kx, ky - reciprocal space vectors (μm⁻¹)
% r_min - inner annulus radius offset index (integer)
% r_max - outer annulus radius offset index (integer)
%
% Example:
% hold on;
% drawFFTOverlays(kx, ky, 10, 30);
hold on
% === Overlay Radial Lines + Scales ===
[kx_grid, ky_grid] = meshgrid(kx, ky);
[~, kr_grid] = cart2pol(kx_grid, ky_grid); % kr_grid in μm⁻¹
max_kx = max(kx);
max_ky = max(ky);
for angle = 0 : pi/6 : pi
x_line = [0, max_kx] * cos(angle);
y_line = [0, max_ky] * sin(angle);
% Plot radial lines
plot(x_line, y_line, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.2);
plot(x_line, -y_line, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.2);
% Draw scale ticks along positive radial line
drawTicksAlongLine(0, 0, x_line(2), y_line(2));
% Draw scale ticks along negative radial line (reflect y)
drawTicksAlongLine(0, 0, x_line(2), -y_line(2));
end
% === Overlay Annular Highlight: White (r_min to r_max), Gray elsewhere ===
theta_full = linspace(0, 2*pi, 500);
center_x = ceil(size(kr_grid, 2) / 2);
center_y = ceil(size(kr_grid, 1) / 2);
k_min = kr_grid(center_y, center_x + r_min);
k_max = kr_grid(center_y, center_x + r_max);
% Upper half: white dashed circles
x1_upper = k_min * cos(theta_full(theta_full <= pi));
y1_upper = k_min * sin(theta_full(theta_full <= pi));
x2_upper = k_max * cos(theta_full(theta_full <= pi));
y2_upper = k_max * sin(theta_full(theta_full <= pi));
plot(x1_upper, y1_upper, 'k--', 'LineWidth', 1.2);
plot(x2_upper, y2_upper, 'k--', 'LineWidth', 1.2);
% Lower half: gray dashed circles
x1_lower = k_min * cos(theta_full(theta_full > pi));
y1_lower = k_min * sin(theta_full(theta_full > pi));
x2_lower = k_max * cos(theta_full(theta_full > pi));
y2_lower = k_max * sin(theta_full(theta_full > pi));
plot(x1_lower, y1_lower, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.0);
plot(x2_lower, y2_lower, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.0);
% === Highlight horizontal band across k_y = 0 ===
x_vals = kx;
xW1 = x_vals((x_vals >= -k_max) & (x_vals < -k_min));
xW2 = x_vals((x_vals > k_min) & (x_vals <= k_max));
plot(xW1, zeros(size(xW1)), 'k--', 'LineWidth', 1.2);
plot(xW2, zeros(size(xW2)), 'k--', 'LineWidth', 1.2);
hold off
% --- Nested helper function to draw ticks along a radial line ---
function drawTicksAlongLine(x_start, y_start, x_end, y_end)
% Tick parameters
tick_spacing = 1; % spacing between ticks in μm⁻¹
tick_length = 0.05 * sqrt((x_end - x_start)^2 + (y_end - y_start)^2); % relative tick length
line_color = [0.5 0.5 0.5];
tick_color = [0.5 0.5 0.5];
font_size = 8;
% Vector along the line
dx = x_end - x_start;
dy = y_end - y_start;
L = sqrt(dx^2 + dy^2);
ux = dx / L;
uy = dy / L;
% Perpendicular vector for ticks
perp_ux = -uy;
perp_uy = ux;
% Number of ticks (from 0 up to max length)
n_ticks = floor(L / tick_spacing);
for i = 1:n_ticks
% Position of tick along the line
xt = x_start + i * tick_spacing * ux;
yt = y_start + i * tick_spacing * uy;
% Tick endpoints
xt1 = xt - 0.5 * tick_length * perp_ux;
yt1 = yt - 0.5 * tick_length * perp_uy;
xt2 = xt + 0.5 * tick_length * perp_ux;
yt2 = yt + 0.5 * tick_length * perp_uy;
% Draw tick
plot([xt1 xt2], [yt1 yt2], '-', 'Color', tick_color, 'LineWidth', 1);
% Label with distance (integer)
text(xt, yt, sprintf('%d', i), ...
'Color', tick_color, ...
'FontSize', font_size, ...
'HorizontalAlignment', 'center', ...
'VerticalAlignment', 'bottom', ...
'Rotation', atan2d(dy, dx));
end
end
end