diff --git a/Data-Analyzer/conductSpectralAnalysis.m b/Data-Analyzer/conductSpectralAnalysis.m index f63571f..83f3c2f 100644 --- a/Data-Analyzer/conductSpectralAnalysis.m +++ b/Data-Analyzer/conductSpectralAnalysis.m @@ -1,27 +1,27 @@ -%% Settings -groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1_Axis_Camera/in_situ_absorption", ... - "/images/ODT_2_Axis_Camera/in_situ_absorption", "/images/Horizontal_Axis_Camera/in_situ_absorption", ... - "/images/Vertical_Axis_Camera/in_situ_absorption"]; +%% ===== Settings ===== +groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1_Axis_Camera/in_situ_absorption", ... + "/images/ODT_2_Axis_Camera/in_situ_absorption", "/images/Horizontal_Axis_Camera/in_situ_absorption", ... + "/images/Vertical_Axis_Camera/in_situ_absorption"]; -folderPath = "D:/Data - Experiment/2025/07/04/"; +folderPath = "D:/Data - Experiment/2025/07/04/"; -run = '0018'; +run = '0016'; -folderPath = strcat(folderPath, run); +folderPath = strcat(folderPath, run); -cam = 5; +cam = 5; -angle = 0; -center = [1430, 2040]; -span = [200, 200]; -fraction = [0.1, 0.1]; +angle = 0; +center = [1430, 2040]; +span = [200, 200]; +fraction = [0.1, 0.1]; -pixel_size = 5.86e-6; -magnification = 23.94; -removeFringes = false; +pixel_size = 5.86e-6; % in meters +magnification = 23.94; +removeFringes = false; -ImagingMode = 'HighIntensity'; -PulseDuration = 5e-6; % in s +ImagingMode = 'HighIntensity'; +PulseDuration = 5e-6; % in s % Fourier analysis settings @@ -42,6 +42,7 @@ Angular_WindowSize = 5; zoom_size = 50; % Zoomed-in region around center +% Plotting and saving % scan_parameter = 'ps_rot_mag_fin_pol_angle'; scan_parameter = 'rot_mag_field'; % scan_parameter_text = 'Angle = '; @@ -58,6 +59,7 @@ elseif strcmp(savefileName, 'StripesToDroplets') scan_groups = 45:-5:0; end +% Flags skipPreprocessing = true; skipMasking = true; skipIntensityThresholding = true; @@ -65,7 +67,7 @@ skipBinarization = true; skipMovieRender = true; skipSaveFigures = false; -%% Load and compute OD image, rotate and extract ROI for analysis +%% ===== Load and compute OD image, rotate and extract ROI for analysis ===== % Get a list of all files in the folder with the desired file name pattern. filePattern = fullfile(folderPath, '*.h5'); @@ -87,7 +89,7 @@ for k = 1 : length(files) absimages(:,:,k) = subtractBackgroundOffset(cropODImage(calculateODImage(atm_img, bkg_img, dark_img, ImagingMode, PulseDuration), center, span), fraction)'; end -% Fringe removal +%% ===== Fringe removal ===== if removeFringes optrefimages = removefringesInImage(absimages, refimages); @@ -106,7 +108,7 @@ else end end -%% Get rotation angles +%% ===== Get rotation angles ===== scan_parameter_values = zeros(1, length(files)); % Get information about the '/globals' group @@ -125,7 +127,7 @@ for k = 1 : length(files) end end -%% Unshuffle if necessary to do so +%% ===== Unshuffle if necessary to do so ===== if ~skipUnshuffling n_values = length(scan_groups); @@ -162,7 +164,7 @@ if ~skipUnshuffling od_imgs = ordered_od_imgs; end -%% Run Fourier analysis over images +%% ===== Run Fourier analysis over images ===== fft_imgs = cell(1, nimgs); spectral_contrast = zeros(1, nimgs); diff --git a/Data-Analyzer/execution_scripts.m b/Data-Analyzer/executionScripts.m similarity index 100% rename from Data-Analyzer/execution_scripts.m rename to Data-Analyzer/executionScripts.m diff --git a/Data-Analyzer/extractAutocorrelation.m b/Data-Analyzer/extractAutocorrelation.m index 903319c..4db9b0e 100644 --- a/Data-Analyzer/extractAutocorrelation.m +++ b/Data-Analyzer/extractAutocorrelation.m @@ -1,40 +1,73 @@ -%% Parameters +%% ===== Settings ===== +groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1_Axis_Camera/in_situ_absorption", ... + "/images/ODT_2_Axis_Camera/in_situ_absorption", "/images/Horizontal_Axis_Camera/in_situ_absorption", ... + "/images/Vertical_Axis_Camera/in_situ_absorption"]; -groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1_Axis_Camera/in_situ_absorption", ... - "/images/ODT_2_Axis_Camera/in_situ_absorption", "/images/Horizontal_Axis_Camera/in_situ_absorption", ... - "/images/Vertical_Axis_Camera/in_situ_absorption"]; +folderPath = "D:/Data - Experiment/2025/07/04/"; -folderPath = "//DyLabNAS/Data/TwoDGas/2025/06/23/"; +run = '0016'; -run = '0300'; +folderPath = strcat(folderPath, run); -folderPath = strcat(folderPath, run); +cam = 5; -cam = 5; +angle = 0; +center = [1430, 2040]; +span = [200, 200]; +fraction = [0.1, 0.1]; -angle = 0; -center = [1410, 2030]; -span = [200, 200]; -fraction = [0.1, 0.1]; +pixel_size = 5.86e-6; % in meters +magnification = 23.94; +removeFringes = false; -pixel_size = 5.86e-6; -removeFringes = false; +ImagingMode = 'HighIntensity'; +PulseDuration = 5e-6; % in s -scan_parameter = 'ps_rot_mag_fin_pol_angle'; -% scan_parameter = 'rot_mag_field'; -scan_parameter_text = 'Angle = '; -% scan_parameter_text = 'BField = '; +% Fourier analysis settings -savefolderPath = 'D:/Results - Experiment/B2.42G/'; -savefileName = 'DropletsToStripes.mat'; +% 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 +r_min = 10; +r_max = 20; +N_angular_bins = 180; +Angular_Threshold = 75; +Angular_Sigma = 2; +Angular_WindowSize = 5; + +zoom_size = 50; % Zoomed-in region around center + +% Plotting and saving +% scan_parameter = 'ps_rot_mag_fin_pol_angle'; +scan_parameter = 'rot_mag_field'; +% scan_parameter_text = 'Angle = '; +scan_parameter_text = 'BField = '; + +savefolderPath = 'E:/Results - Experiment/B2.35G/'; +savefileName = 'Droplets'; font = 'Bahnschrift'; +skipUnshuffling = true; +if strcmp(savefileName, 'DropletsToStripes') + scan_groups = 0:5:45; +elseif strcmp(savefileName, 'StripesToDroplets') + scan_groups = 45:-5:0; +end + +% Flags skipPreprocessing = true; skipMasking = true; skipIntensityThresholding = true; skipBinarization = true; +skipMovieRender = true; +skipSaveFigures = false; -%% Compute OD image, rotate and extract ROI for analysis +%% ===== Load and compute OD image, rotate and extract ROI for analysis ===== % Get a list of all files in the folder with the desired file name pattern. filePattern = fullfile(folderPath, '*.h5'); @@ -53,11 +86,10 @@ for k = 1 : length(files) dark_img = double(imrotate(h5read(fullFileName, append(groupList(cam), "/dark")), angle)); refimages(:,:,k) = subtractBackgroundOffset(cropODImage(bkg_img, center, span), fraction)'; - absimages(:,:,k) = subtractBackgroundOffset(cropODImage(calculateODImage(atm_img, bkg_img, dark_img), center, span), fraction)'; - + absimages(:,:,k) = subtractBackgroundOffset(cropODImage(calculateODImage(atm_img, bkg_img, dark_img, ImagingMode, PulseDuration), center, span), fraction)'; end -% Fringe removal +%% ===== Fringe removal ===== if removeFringes optrefimages = removefringesInImage(absimages, refimages); @@ -76,7 +108,7 @@ else end end -%% Get rotation angles +%% ===== Get rotation angles ===== scan_parameter_values = zeros(1, length(files)); % Get information about the '/globals' group @@ -86,7 +118,7 @@ for k = 1 : length(files) info = h5info(fullFileName, '/globals'); for i = 1:length(info.Attributes) if strcmp(info.Attributes(i).Name, scan_parameter) - if strcmp(scan_parameter, 'rot_mag_fin_pol_angle') + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') scan_parameter_values(k) = 180 - info.Attributes(i).Value; else scan_parameter_values(k) = info.Attributes(i).Value; @@ -95,45 +127,58 @@ for k = 1 : length(files) end end -%% Extract g2 from experiment data +%% ===== Extract g2 from experiment data ===== fft_imgs = cell(1, nimgs); spectral_distribution = cell(1, nimgs); theta_values = cell(1, nimgs); -N_bins = 32; -Threshold = 75; -Sigma = 2; N_shots = length(od_imgs); -% Display the cropped image +% Compute FFT for k = 1:N_shots - IMG = od_imgs{k}; - [IMGFFT, IMGPR] = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization); + IMG = od_imgs{k}; + [IMGFFT, IMGPR] = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization); - % Calculate the x and y limits for the cropped image - y_min = center(1) - span(2) / 2; - y_max = center(1) + span(2) / 2; - x_min = center(2) - span(1) / 2; - x_max = center(2) + span(1) / 2; + % Size of original image (in pixels) + [Ny, Nx] = size(IMG); - % Generate x and y arrays representing the original coordinates for each pixel - x_range = linspace(x_min, x_max, span(1)); - y_range = linspace(y_min, y_max, span(2)); + % Real-space pixel size in micrometers after magnification + dx = pixel_size / magnification; + dy = dx; % assuming square pixels - [rows, cols] = size(IMGFFT); - zoom_size = 50; % Zoomed-in region around center - mid_x = floor(cols/2); - mid_y = floor(rows/2); - fft_imgs{k} = IMGFFT(mid_y-zoom_size:mid_y+zoom_size, mid_x-zoom_size:mid_x+zoom_size); + % Real-space axes + x = ((1:Nx) - ceil(Nx/2)) * dx * 1E6; + y = ((1:Ny) - ceil(Ny/2)) * dy * 1E6; - [theta_vals, S_theta] = computeNormalizedAngularSpectralDistribution(fft_imgs{k}, 10, 20, N_bins, Threshold, Sigma); + % Reciprocal space increments (frequency domain, μm⁻¹) + dvx = 1 / (Nx * dx); + dvy = 1 / (Ny * dy); + + % Frequency axes + vx = (-floor(Nx/2):ceil(Nx/2)-1) * dvx; + vy = (-floor(Ny/2):ceil(Ny/2)-1) * dvy; + + % Wavenumber axes + kx_full = 2 * pi * vx * 1E-6; % μm⁻¹ + ky_full = 2 * pi * vy * 1E-6; + + % 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}, r_min, r_max, N_angular_bins, Angular_Threshold, Angular_Sigma, []); spectral_distribution{k} = S_theta; theta_values{k} = theta_vals; end -% Create matrix of shape (N_shots x N_bins) -delta_nkr_all = zeros(N_shots, N_bins); +% Create matrix of shape (N_shots x N_angular_bins) +delta_nkr_all = zeros(N_shots, N_angular_bins); for k = 1:N_shots delta_nkr_all(k, :) = spectral_distribution{k}; end @@ -142,26 +187,27 @@ end [unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); % Number of unique alpha values -N_alpha = length(unique_scan_parameter_values); +N_alpha = length(unique_scan_parameter_values); % Preallocate result arrays -g2_all = zeros(N_alpha, N_bins); -g2_error_all = zeros(N_alpha, N_bins); +g2_all = zeros(N_alpha, N_angular_bins); +g2_error_all = zeros(N_alpha, N_angular_bins); +% Compute g2 for i = 1:N_alpha - group_idx = find(idx == i); % Indices of 20 shots for this alpha - group_data = delta_nkr_all(group_idx, :); % (20 x N_bins) array + group_idx = find(idx == i); + group_data = delta_nkr_all(group_idx, :); - for dtheta = 0:N_bins-1 + for dtheta = 0:N_angular_bins-1 temp = zeros(length(group_idx), 1); for j = 1:length(group_idx) profile = group_data(j, :); profile_shifted = circshift(profile, -dtheta, 2); num = mean(profile .* profile_shifted); - denom = mean(profile)^2; + denom = mean(profile.^2); - temp(j) = num / denom - 1; + temp(j) = num / denom; end g2_all(i, dtheta+1) = mean(temp); g2_error_all(i, dtheta+1) = std(temp) / sqrt(length(group_idx)); % Standard error @@ -185,19 +231,25 @@ legend_entries = cell(nAlpha, 1); for i = 1:nAlpha errorbar(theta_vals/pi, g2_all(i, :), g2_error_all(i, :), ... - 'o-', 'Color', cmap(i,:), 'LineWidth', 1.2, ... - 'MarkerSize', 5, 'CapSize', 3); - legend_entries{i} = sprintf('$\\alpha = %g^\\circ$', unique_scan_parameter_values(i)); + 'o', 'Color', cmap(i,:), ... + 'MarkerSize', 3, 'MarkerFaceColor', cmap(i,:), ... + 'CapSize', 4); + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + legend_entries{i} = sprintf('$\\alpha = %g^\\circ$', unique_scan_parameter_values(i)); + elseif strcmp(scan_parameter, 'rot_mag_field') + legend_entries{i} = sprintf('B = %.2f G', unique_scan_parameter_values(i)); + end end + ylim([-1.5 3.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('Change across transition', 'Interpreter', 'tex'); +% hTitle = title('Change across transition', '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 +% set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold'); % Set font and size for title grid on; %% Helper Functions @@ -272,7 +324,7 @@ function [IMGFFT, IMGPR] = computeFourierTransform(I, skipPreprocessing, skipMas end end -function [theta_vals, S_theta] = computeNormalizedAngularSpectralDistribution(IMGFFT, r_min, r_max, num_bins, threshold, sigma) +function [theta_vals, S_theta] = computeAngularSpectralDistribution(IMGFFT, r_min, r_max, num_bins, threshold, sigma, windowSize) % Apply threshold to isolate strong peaks IMGFFT(IMGFFT < threshold) = 0; @@ -285,42 +337,40 @@ function [theta_vals, S_theta] = computeNormalizedAngularSpectralDistribution(IM Theta = atan2(Y - cy, X - cx); % range [-pi, pi] % Choose radial band - radial_mask = (R >= r_min) & (R <= r_max); + radial_mask = (R >= r_min) & (R <= r_max); - % Initialize the angular structure factor array - S_theta = zeros(1, num_bins); % Pre-allocate for 180 angle bins - % Define the angle values for the x-axis - theta_vals = linspace(0, pi, num_bins); + % Initialize angular structure factor + S_theta = zeros(1, num_bins); + theta_vals = linspace(0, pi, num_bins); - % Loop through each angle bin + % Loop through angle bins for i = 1:num_bins angle_start = (i-1) * pi / num_bins; angle_end = i * pi / num_bins; - - % Define a mask for the given angle range angle_mask = (Theta >= angle_start & Theta < angle_end); - bin_mask = radial_mask & angle_mask; - - % Extract the Fourier components for the given angle fft_angle = IMGFFT .* bin_mask; - - % Integrate the Fourier components over the radius at the angle - S_theta(i) = sum(sum(abs(fft_angle).^2)); % sum of squared magnitudes + S_theta(i) = sum(sum(abs(fft_angle).^2)); + end + + % Smooth using either Gaussian or moving average + if exist('sigma', 'var') && ~isempty(sigma) + % Gaussian convolution + 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 via convolution (circular) + 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 - - % Create a 1D Gaussian kernel - 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); % normalize - - % Apply convolution (circular padding to preserve periodicity) - 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); % crop back to original size - - % Normalize to 1 - S_theta = S_theta / max(S_theta); end function ret = getBkgOffsetFromCorners(img, x_fraction, y_fraction) @@ -371,28 +421,51 @@ function ret = cropODImage(img, center, span) ret = img(y_start:y_end, x_start:x_end); end -function ret = calculateODImage(imageAtom, imageBackground, imageDark) - % Calculate the OD image for absorption imaging. - % :param imageAtom: The image with atoms - % :type imageAtom: numpy array - % :param imageBackground: The image without atoms - % :type imageBackground: numpy array - % :param imageDark: The image without light - % :type imageDark: numpy array - % :return: The OD images - % :rtype: numpy array +function imageOD = calculateODImage(imageAtom, imageBackground, imageDark, mode, exposureTime) +%CALCULATEODIMAGE Calculates the optical density (OD) image for absorption imaging. +% +% imageOD = calculateODImage(imageAtom, imageBackground, imageDark, mode, exposureTime) +% +% Inputs: +% imageAtom - Image with atoms +% imageBackground - Image without atoms +% imageDark - Image without light +% mode - 'LowIntensity' (default) or 'HighIntensity' +% exposureTime - Required only for 'HighIntensity' [in seconds] +% +% Output: +% imageOD - Computed OD image +% + arguments + imageAtom (:,:) {mustBeNumeric} + imageBackground (:,:) {mustBeNumeric} + imageDark (:,:) {mustBeNumeric} + mode char {mustBeMember(mode, {'LowIntensity', 'HighIntensity'})} = 'LowIntensity' + exposureTime double = NaN + end + + % Compute numerator and denominator numerator = imageBackground - imageDark; denominator = imageAtom - imageDark; + % Avoid division by zero numerator(numerator == 0) = 1; denominator(denominator == 0) = 1; - - ret = -log(double(abs(denominator ./ numerator))); - if numel(ret) == 1 - ret = ret(1); + % Calculate OD based on mode + switch mode + case 'LowIntensity' + imageOD = -log(abs(denominator ./ numerator)); + + case 'HighIntensity' + if isnan(exposureTime) + error('Exposure time must be provided for HighIntensity mode.'); + end + imageOD = abs(denominator ./ numerator); + imageOD = -log(imageOD) + (numerator - denominator) ./ (7000 * (exposureTime / 5e-6)); end + end function [optrefimages] = removefringesInImage(absimages, refimages, bgmask) @@ -464,5 +537,4 @@ function [optrefimages] = removefringesInImage(absimages, refimages, bgmask) % Compute optimised reference image optrefimages(:,:,j)=reshape(R*c,[ydim xdim]); end -end - +end \ No newline at end of file diff --git a/Data-Analyzer/extractCustomCorrelation.m b/Data-Analyzer/extractCustomCorrelation.m new file mode 100644 index 0000000..e406087 --- /dev/null +++ b/Data-Analyzer/extractCustomCorrelation.m @@ -0,0 +1,615 @@ +%% ===== Settings ===== +groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1_Axis_Camera/in_situ_absorption", ... + "/images/ODT_2_Axis_Camera/in_situ_absorption", "/images/Horizontal_Axis_Camera/in_situ_absorption", ... + "/images/Vertical_Axis_Camera/in_situ_absorption"]; + +folderPath = "D:/Data - Experiment/2025/07/04/"; + +run = '0016'; + +folderPath = strcat(folderPath, run); + +cam = 5; + +angle = 0; +center = [1430, 2040]; +span = [200, 200]; +fraction = [0.1, 0.1]; + +pixel_size = 5.86e-6; % in meters +magnification = 23.94; +removeFringes = false; + +ImagingMode = 'HighIntensity'; +PulseDuration = 5e-6; % in s + +% 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 +r_min = 10; +r_max = 20; +N_angular_bins = 180; +Angular_Threshold = 75; +Angular_Sigma = 2; +Angular_WindowSize = 5; + +zoom_size = 50; % Zoomed-in region around center + +% Plotting and saving +% scan_parameter = 'ps_rot_mag_fin_pol_angle'; +scan_parameter = 'rot_mag_field'; +% scan_parameter_text = 'Angle = '; +scan_parameter_text = 'BField = '; + +savefolderPath = 'E:/Results - Experiment/B2.35G/'; +savefileName = 'Droplets'; +font = 'Bahnschrift'; + +skipUnshuffling = true; +if strcmp(savefileName, 'DropletsToStripes') + scan_groups = 0:5:45; +elseif strcmp(savefileName, 'StripesToDroplets') + scan_groups = 45:-5:0; +end + +% Flags +skipPreprocessing = true; +skipMasking = true; +skipIntensityThresholding = true; +skipBinarization = true; +skipMovieRender = true; +skipSaveFigures = false; + +%% ===== Load and compute OD image, rotate and extract ROI for analysis ===== +% Get a list of all files in the folder with the desired file name pattern. + +filePattern = fullfile(folderPath, '*.h5'); +files = dir(filePattern); +refimages = zeros(span(1) + 1, span(2) + 1, length(files)); +absimages = zeros(span(1) + 1, span(2) + 1, length(files)); + +for k = 1 : length(files) + baseFileName = files(k).name; + fullFileName = fullfile(files(k).folder, baseFileName); + + fprintf(1, 'Now reading %s\n', fullFileName); + + atm_img = double(imrotate(h5read(fullFileName, append(groupList(cam), "/atoms")), angle)); + bkg_img = double(imrotate(h5read(fullFileName, append(groupList(cam), "/background")), angle)); + dark_img = double(imrotate(h5read(fullFileName, append(groupList(cam), "/dark")), angle)); + + refimages(:,:,k) = subtractBackgroundOffset(cropODImage(bkg_img, center, span), fraction)'; + absimages(:,:,k) = subtractBackgroundOffset(cropODImage(calculateODImage(atm_img, bkg_img, dark_img, ImagingMode, PulseDuration), center, span), fraction)'; +end + +%% ===== Fringe removal ===== + +if removeFringes + optrefimages = removefringesInImage(absimages, refimages); + absimages_fringe_removed = absimages(:, :, :) - optrefimages(:, :, :); + + nimgs = size(absimages_fringe_removed,3); + od_imgs = cell(1, nimgs); + for i = 1:nimgs + od_imgs{i} = absimages_fringe_removed(:, :, i); + end +else + nimgs = size(absimages(:, :, :),3); + od_imgs = cell(1, nimgs); + for i = 1:nimgs + od_imgs{i} = absimages(:, :, i); + end +end + +%% ===== Get rotation angles ===== +scan_parameter_values = zeros(1, length(files)); + +% Get information about the '/globals' group +for k = 1 : length(files) + baseFileName = files(k).name; + fullFileName = fullfile(files(k).folder, baseFileName); + info = h5info(fullFileName, '/globals'); + for i = 1:length(info.Attributes) + if strcmp(info.Attributes(i).Name, scan_parameter) + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + scan_parameter_values(k) = 180 - info.Attributes(i).Value; + else + scan_parameter_values(k) = info.Attributes(i).Value; + end + end + end +end + +%% ===== Correlation of a single (highest) peak with a possible peak between 50-70 degrees from experiment data ===== + +fft_imgs = cell(1, nimgs); +spectral_distribution = cell(1, nimgs); +theta_values = cell(1, nimgs); + +N_shots = length(od_imgs); + +% Compute FFT +for k = 1:N_shots + IMG = od_imgs{k}; + [IMGFFT, IMGPR] = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization); + + % Size of original image (in pixels) + [Ny, Nx] = size(IMG); + + % Real-space pixel size in micrometers after magnification + dx = pixel_size / magnification; + dy = dx; % assuming square pixels + + % Real-space axes + x = ((1:Nx) - ceil(Nx/2)) * dx * 1E6; + y = ((1:Ny) - ceil(Ny/2)) * dy * 1E6; + + % Reciprocal space increments (frequency domain, μm⁻¹) + dvx = 1 / (Nx * dx); + dvy = 1 / (Ny * dy); + + % Frequency axes + vx = (-floor(Nx/2):ceil(Nx/2)-1) * dvx; + vy = (-floor(Ny/2):ceil(Ny/2)-1) * dvy; + + % Wavenumber axes + kx_full = 2 * pi * vx * 1E-6; % μm⁻¹ + ky_full = 2 * pi * vy * 1E-6; + + % 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}, r_min, r_max, N_angular_bins, Angular_Threshold, Angular_Sigma, []); + spectral_distribution{k} = S_theta; + theta_values{k} = theta_vals; +end + +% Create matrix of shape (N_shots x N_angular_bins) +delta_nkr_all = zeros(N_shots, N_angular_bins); +for k = 1:N_shots + delta_nkr_all(k, :) = spectral_distribution{k}; +end + +% Grouping by scan parameter value (e.g., alpha) +[unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); + +% Number of unique alpha values +N_params = length(unique_scan_parameter_values); + +% Define angular range and bins +angle_range = 180; % total angular span of the profile +angle_per_bin = angle_range / N_angular_bins; + +max_peak_angle = 60; +max_peak_bin = round(max_peak_angle / angle_per_bin); + +window_size = 10; +angle_threshold = 100; + +ref_peak_angles = []; +angle_at_max_g2 = []; +g2_values = []; + +for i = 1:N_params + group_idx = find(idx == i); + group_data = delta_nkr_all(group_idx, :); + + for j = 1:size(group_data, 1) + profile = group_data(j, :); + + % Restrict search for peak only in 0° to 90° + restricted_profile = profile(1:max_peak_bin); + [~, peak_idx_rel] = max(restricted_profile); + + % Convert relative peak index to global index in profile + peak_idx = peak_idx_rel; + + peak_angle = (peak_idx - 1) * angle_per_bin; % zero-based bin index to angle + + % Determine shift direction based on peak angle + 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 + + % Reference window around largest peak + ref_window = mod((peak_idx - window_size):(peak_idx + window_size) - 1, N_angular_bins) + 1; + ref = profile(ref_window); + + % Store reference peak angle + ref_peak_angles(end+1) = peak_angle; + + 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); + + % Calculate g2 correlation + num = mean(ref .* sec); + denom = mean(ref.^2); + g2 = num / denom; + + correlations(k) = g2; + + % Compute angle for this shifted window (map to 0-180 degrees) + angle_val = mod((peak_idx - 1 + offsets(k)) * angle_per_bin, angle_range); + angles(k) = angle_val; + end + + [max_corr, max_idx] = max(correlations); + g2_values(end+1) = max_corr; + angle_at_max_g2(end+1) = angles(max_idx); + end +end + +% Plot histograms within 0-180 degrees only +figure(1); +hold on; + +bin_edges = 0:10:180; + +h1 = histogram(ref_peak_angles, 'BinEdges', bin_edges, ... + 'FaceColor', [0.3 0.7 0.9], 'EdgeColor', 'none', 'FaceAlpha', 0.6); + +h2 = histogram(angle_at_max_g2, 'BinEdges', bin_edges, ... + 'FaceColor', [0.9 0.4 0.4], 'EdgeColor', 'none', 'FaceAlpha', 0.6); + +h1.Normalization = 'probability'; +h2.Normalization = 'probability'; + +xlabel('Angle (degrees)', 'FontSize', 12); +ylabel('Probability', 'FontSize', 12); +legend({'Reference Peak Angle', 'Angle at Max g₂'}, 'FontSize', 12); +title('Comparison of Reference Peak and Max g₂ Angles', 'FontSize', 14); +grid on; +xlim([0 180]); +hold off; + +% Assume ref_peak_angles and angle_at_max_g2 are row or column vectors of angles in [0,180] + +% Define fine angle grid for KDE evaluation +angle_grid = linspace(0, 180, 1000); + +% KDE for reference peak angles +[f_ref, xi_ref] = ksdensity(ref_peak_angles, angle_grid, 'Bandwidth', 5); + +% KDE for max g2 angles +[f_g2, xi_g2] = ksdensity(angle_at_max_g2, angle_grid, 'Bandwidth', 5); + +% Plot KDEs +figure(2); +plot(xi_ref, f_ref, 'LineWidth', 2, 'DisplayName', 'Reference Peak Angles'); +hold on; +plot(xi_g2, f_g2, 'LineWidth', 2, 'DisplayName', 'Max g_2 Angles'); +xlabel('Angle (degrees)'); +ylabel('Probability Density'); +title('KDE of Angle Distributions'); +legend; +grid on; + +% Find modes (angle at max KDE value) +[~, mode_idx_ref] = max(f_ref); +mode_ref = xi_ref(mode_idx_ref); + +[~, mode_idx_g2] = max(f_g2); +mode_g2 = xi_g2(mode_idx_g2); + +% Calculate difference in mode +mode_diff = abs(mode_ref - mode_g2); +fprintf('Mode difference between distributions: %.2f degrees\n', mode_diff); + +% Add vertical dashed lines at mode positions +yl = ylim; % get y-axis limits for text positioning + +% Reference peak mode line and label +xline(mode_ref, 'k--', 'LineWidth', 1.5, 'DisplayName', sprintf('Ref Mode: %.1f°', mode_ref)); +text(mode_ref, yl(2)*0.9, sprintf('%.1f°', mode_ref), 'HorizontalAlignment', 'center', 'VerticalAlignment', 'bottom', 'FontSize', 12, 'Color', 'k'); + +% Max g2 mode line and label +xline(mode_g2, 'r--', 'LineWidth', 1.5, 'DisplayName', sprintf('g_2 Mode: %.1f°', mode_g2)); +text(mode_g2, yl(2)*0.75, sprintf('%.1f°', mode_g2), 'HorizontalAlignment', 'center', 'VerticalAlignment', 'bottom', 'FontSize', 12, 'Color', 'r'); + + +%% Helper Functions +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 [theta_vals, S_theta] = computeAngularSpectralDistribution(IMGFFT, r_min, r_max, num_bins, threshold, sigma, windowSize) + % Apply threshold to isolate strong peaks + IMGFFT(IMGFFT < threshold) = 0; + + % Prepare polar coordinates + [ny, nx] = size(IMGFFT); + [X, Y] = meshgrid(1:nx, 1:ny); + cx = ceil(nx/2); + cy = ceil(ny/2); + R = sqrt((X - cx).^2 + (Y - cy).^2); + Theta = atan2(Y - cy, X - cx); % range [-pi, pi] + + % Choose radial band + radial_mask = (R >= r_min) & (R <= r_max); + + % Initialize angular structure factor + S_theta = zeros(1, num_bins); + theta_vals = linspace(0, pi, num_bins); + + % Loop through angle 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 + + % Smooth using either Gaussian or moving average + if exist('sigma', 'var') && ~isempty(sigma) + % Gaussian convolution + 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 via convolution (circular) + 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 ret = getBkgOffsetFromCorners(img, x_fraction, y_fraction) + % image must be a 2D numerical array + [dim1, dim2] = size(img); + + s1 = img(1:round(dim1 * y_fraction), 1:round(dim2 * x_fraction)); + s2 = img(1:round(dim1 * y_fraction), round(dim2 - dim2 * x_fraction):dim2); + s3 = img(round(dim1 - dim1 * y_fraction):dim1, 1:round(dim2 * x_fraction)); + s4 = img(round(dim1 - dim1 * y_fraction):dim1, round(dim2 - dim2 * x_fraction):dim2); + + ret = mean([mean(s1(:)), mean(s2(:)), mean(s3(:)), mean(s4(:))]); +end + +function ret = subtractBackgroundOffset(img, fraction) + % Remove the background from the image. + % :param dataArray: The image + % :type dataArray: xarray DataArray + % :param x_fraction: The fraction of the pixels used in x axis + % :type x_fraction: float + % :param y_fraction: The fraction of the pixels used in y axis + % :type y_fraction: float + % :return: The image after removing background + % :rtype: xarray DataArray + + x_fraction = fraction(1); + y_fraction = fraction(2); + offset = getBkgOffsetFromCorners(img, x_fraction, y_fraction); + ret = img - offset; +end + +function ret = cropODImage(img, center, span) + % Crop the image according to the region of interest (ROI). + % :param dataSet: The images + % :type dataSet: xarray DataArray or DataSet + % :param center: The center of region of interest (ROI) + % :type center: tuple + % :param span: The span of region of interest (ROI) + % :type span: tuple + % :return: The cropped images + % :rtype: xarray DataArray or DataSet + + x_start = floor(center(1) - span(1) / 2); + x_end = floor(center(1) + span(1) / 2); + y_start = floor(center(2) - span(2) / 2); + y_end = floor(center(2) + span(2) / 2); + + ret = img(y_start:y_end, x_start:x_end); +end + +function imageOD = calculateODImage(imageAtom, imageBackground, imageDark, mode, exposureTime) +%CALCULATEODIMAGE Calculates the optical density (OD) image for absorption imaging. +% +% imageOD = calculateODImage(imageAtom, imageBackground, imageDark, mode, exposureTime) +% +% Inputs: +% imageAtom - Image with atoms +% imageBackground - Image without atoms +% imageDark - Image without light +% mode - 'LowIntensity' (default) or 'HighIntensity' +% exposureTime - Required only for 'HighIntensity' [in seconds] +% +% Output: +% imageOD - Computed OD image +% + + arguments + imageAtom (:,:) {mustBeNumeric} + imageBackground (:,:) {mustBeNumeric} + imageDark (:,:) {mustBeNumeric} + mode char {mustBeMember(mode, {'LowIntensity', 'HighIntensity'})} = 'LowIntensity' + exposureTime double = NaN + end + + % Compute numerator and denominator + numerator = imageBackground - imageDark; + denominator = imageAtom - imageDark; + + % Avoid division by zero + numerator(numerator == 0) = 1; + denominator(denominator == 0) = 1; + + % Calculate OD based on mode + switch mode + case 'LowIntensity' + imageOD = -log(abs(denominator ./ numerator)); + + case 'HighIntensity' + if isnan(exposureTime) + error('Exposure time must be provided for HighIntensity mode.'); + end + imageOD = abs(denominator ./ numerator); + imageOD = -log(imageOD) + (numerator - denominator) ./ (7000 * (exposureTime / 5e-6)); + end + +end + +function [optrefimages] = removefringesInImage(absimages, refimages, bgmask) + % removefringesInImage - Fringe removal and noise reduction from absorption images. + % Creates an optimal reference image for each absorption image in a set as + % a linear combination of reference images, with coefficients chosen to + % minimize the least-squares residuals between each absorption image and + % the optimal reference image. The coefficients are obtained by solving a + % linear set of equations using matrix inverse by LU decomposition. + % + % Application of the algorithm is described in C. F. Ockeloen et al, Improved + % detection of small atom numbers through image processing, arXiv:1007.2136 (2010). + % + % Syntax: + % [optrefimages] = removefringesInImage(absimages,refimages,bgmask); + % + % Required inputs: + % absimages - Absorption image data, + % typically 16 bit grayscale images + % refimages - Raw reference image data + % absimages and refimages are both cell arrays containing + % 2D array data. The number of refimages can differ from the + % number of absimages. + % + % Optional inputs: + % bgmask - Array specifying background region used, + % 1=background, 0=data. Defaults to all ones. + % Outputs: + % optrefimages - Cell array of optimal reference images, + % equal in size to absimages. + % + + % Dependencies: none + % + % Authors: Shannon Whitlock, Caspar Ockeloen + % Reference: C. F. Ockeloen, A. F. Tauschinsky, R. J. C. Spreeuw, and + % S. Whitlock, Improved detection of small atom numbers through + % image processing, arXiv:1007.2136 + % Email: + % May 2009; Last revision: 11 August 2010 + + % Process inputs + + % Set variables, and flatten absorption and reference images + nimgs = size(absimages,3); + nimgsR = size(refimages,3); + xdim = size(absimages(:,:,1),2); + ydim = size(absimages(:,:,1),1); + + R = single(reshape(refimages,xdim*ydim,nimgsR)); + A = single(reshape(absimages,xdim*ydim,nimgs)); + optrefimages=zeros(size(absimages)); % preallocate + + if not(exist('bgmask','var')); bgmask=ones(ydim,xdim); end + k = find(bgmask(:)==1); % Index k specifying background region + + % Ensure there are no duplicate reference images + % R=unique(R','rows')'; % comment this line if you run out of memory + + % Decompose B = R*R' using singular value or LU decomposition + [L,U,p] = lu(R(k,:)'*R(k,:),'vector'); % LU decomposition + + for j=1:nimgs + b=R(k,:)'*A(k,j); + % Obtain coefficients c which minimise least-square residuals + lower.LT = true; upper.UT = true; + c = linsolve(U,linsolve(L,b(p,:),lower),upper); + + % Compute optimised reference image + optrefimages(:,:,j)=reshape(R*c,[ydim xdim]); + end +end \ No newline at end of file