diff --git a/Data-Analyzer/analyzeFolder.m b/Data-Analyzer/analyzeFolder.m new file mode 100644 index 0000000..cf4b1ca --- /dev/null +++ b/Data-Analyzer/analyzeFolder.m @@ -0,0 +1,701 @@ +function results = analyzeFolder(options) + % Ensure required fields are defined in options + arguments + options.scan_parameter (1,:) char + options.scan_groups (1,:) double + options.cam (1,1) double + options.angle (1,1) double + options.center (1,2) double + options.span (1,2) double + options.fraction (1,2) double + options.ImagingMode (1,:) char + options.PulseDuration (1,1) double + options.removeFringes (1,1) logical + options.skipUnshuffling (1,1) logical + options.pixel_size (1,1) double + options.magnification (1,1) double + options.zoom_size (1,1) double + options.r_min (1,1) double + options.r_max (1,1) double + options.N_angular_bins (1,1) double + options.Angular_Threshold (1,1) double + options.Angular_Sigma (1,1) double + options.Angular_WindowSize (1,1) double + options.theta_min (1,1) double + options.theta_max (1,1) double + options.N_radial_bins (1,1) double + options.Radial_Sigma (1,1) double + options.Radial_WindowSize (1,1) double + options.k_min (1,1) double + options.k_max (1,1) double + options.skipPreprocessing (1,1) logical + options.skipMasking (1,1) logical + options.skipIntensityThresholding (1,1) logical + options.skipBinarization (1,1) logical + options.folderPath (1,:) char + end + + % Assign variables from options + scan_parameter = options.scan_parameter; + scan_groups = options.scan_groups; + folderPath = options.folderPath; + center = options.center; + span = options.span; + fraction = options.fraction; + ImagingMode = options.ImagingMode; + PulseDuration = options.PulseDuration; + removeFringes = options.removeFringes; + skipUnshuffling = options.skipUnshuffling; + pixel_size = options.pixel_size; + magnification = options.magnification; + zoom_size = options.zoom_size; + r_min = options.r_min; + r_max = options.r_max; + N_angular_bins = options.N_angular_bins; + Angular_Threshold = options.Angular_Threshold; + Angular_Sigma = options.Angular_Sigma; + Angular_WindowSize = options.Angular_WindowSize; + theta_min = options.theta_min; + theta_max = options.theta_max; + N_radial_bins = options.N_radial_bins; + Radial_Sigma = options.Radial_Sigma; + Radial_WindowSize = options.Radial_WindowSize; + k_min = options.k_min; + k_max = options.k_max; + skipPreprocessing = options.skipPreprocessing; + skipMasking = options.skipMasking; + skipIntensityThresholding = options.skipIntensityThresholding; + skipBinarization = options.skipBinarization; + cam = options.cam; + angle = options.angle; + + % Load images and analyze them (keep using the cleaned body of your original function) + % Fix the incorrect usage of 'cam' and 'angle' not defined locally + 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"]; + + 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, '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 + + % ===== Unshuffle if necessary to do so ===== + + if ~skipUnshuffling + n_values = length(scan_groups); + n_total = length(scan_parameter_values); + + % Infer number of repetitions + n_reps = n_total / n_values; + + % Preallocate ordered arrays + ordered_scan_values = zeros(1, n_total); + ordered_od_imgs = cell(1, n_total); + + counter = 1; + + for rep = 1:n_reps + for val = scan_groups + % Find the next unused match for this val + idx = find(scan_parameter_values == val, 1, 'first'); + + % Assign and remove from list to avoid duplicates + ordered_scan_values(counter) = scan_parameter_values(idx); + ordered_od_imgs{counter} = od_imgs{idx}; + + % Mark as used by removing + scan_parameter_values(idx) = NaN; % NaN is safe since original values are 0:5:45 + od_imgs{idx} = []; % empty cell so it won't be matched again + + counter = counter + 1; + end + end + + % Now assign back + scan_parameter_values = ordered_scan_values; + od_imgs = ordered_od_imgs; + end + % Extract quantities + fft_imgs = cell(1, nimgs); + spectral_distribution = cell(1, nimgs); + theta_values = cell(1, nimgs); + radial_spectral_contrast = zeros(1, nimgs); + angular_spectral_weight = zeros(1, nimgs); + N_shots = length(od_imgs); + + for k = 1:N_shots + IMG = od_imgs{k}; + [IMGFFT, ~] = 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, []); + [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 + 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); + end + + % Assuming scan_parameter_values and spectral_weight are column vectors (or row vectors of same length) + [unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); + + % Preallocate arrays + mean_rsc = zeros(size(unique_scan_parameter_values)); + stderr_rsc = zeros(size(unique_scan_parameter_values)); + + % Loop through each unique theta and compute mean and standard error + for i = 1:length(unique_scan_parameter_values) + group_vals = radial_spectral_contrast(idx == i); + mean_rsc(i) = mean(group_vals); + stderr_rsc(i) = std(group_vals) / sqrt(length(group_vals)); % standard error = std / sqrt(N) + end + + % Preallocate arrays + mean_asw = zeros(size(unique_scan_parameter_values)); + stderr_asw = zeros(size(unique_scan_parameter_values)); + + % Loop through each unique theta and compute mean and standard error + for i = 1:length(unique_scan_parameter_values) + group_vals = angular_spectral_weight(idx == i); + mean_asw(i) = mean(group_vals); + stderr_asw(i) = std(group_vals) / sqrt(length(group_vals)); % standard error = std / sqrt(N) + end + + % 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, :) = spectral_distribution{k}; + end + + % Group by scan parameter values (e.g., alpha, angle, etc.) + [unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); + N_params = length(unique_scan_parameter_values); + + % 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); + std_error_g2_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_idx = find(idx == i); + group_data = delta_nkr_all(group_idx, :); + 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 0–60° 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'); + n_i = numel(g2_all_per_group{i}); % Number of repetitions for this param + std_error_g2_values(i) = sqrt(var_max_g2_values(i) / n_i); + end + + results.folderPath = folderPath; + results.scan_parameter = scan_parameter; + results.scan_groups = scan_groups; + + results.mean_max_g2_values = mean_max_g2_values; + results.std_error_g2_values = std_error_g2_values; + results.mean_max_g2_angle = mean_max_g2_angle_values; + results.radial_spectral_contrast= mean_rsc; + results.angular_spectral_weight = mean_asw; +end + +%% 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 [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, 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 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 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 diff --git a/Data-Analyzer/compareAngularCorrelation.m b/Data-Analyzer/compareAngularCorrelation.m new file mode 100644 index 0000000..a2574a0 --- /dev/null +++ b/Data-Analyzer/compareAngularCorrelation.m @@ -0,0 +1,39 @@ +%% Track spectral weight across the transition + +set(0,'defaulttextInterpreter','latex') +set(groot, 'defaultAxesTickLabelInterpreter','latex'); set(groot, 'defaultLegendInterpreter','latex'); + +format long + +font = 'Bahnschrift'; + +% Load data +Data = load('C:/Users/Karthik/Documents/GitRepositories/Calculations/Data-Analyzer/Comparison/Max_g2_DropletsToStripes.mat', 'unique_scan_parameter_values', 'mean_max_g2_values', 'std_error_g2_values'); + +dts_scan_parameter_values = Data.unique_scan_parameter_values; +dts_mean_mg2 = Data.mean_max_g2_values; +dts_stderr_mg2 = Data.std_error_g2_values; + +Data = load('C:/Users/Karthik/Documents/GitRepositories/Calculations/Data-Analyzer/Comparison/Max_g2_StripesToDroplets.mat', 'unique_scan_parameter_values', 'mean_max_g2_values', 'std_error_g2_values'); + +std_scan_parameter_values = Data.unique_scan_parameter_values; +std_mean_mg2 = Data.mean_max_g2_values; +std_stderr_mg2 = Data.std_error_g2_values; + +figure(1); +set(gcf,'Position',[100 100 950 750]) +errorbar(dts_scan_parameter_values, dts_mean_mg2, dts_stderr_mg2, 'o--', ... + 'LineWidth', 1.5, 'MarkerSize', 6, 'CapSize', 5, 'DisplayName' , 'Droplets to Stripes'); +hold on +errorbar(std_scan_parameter_values, std_mean_mg2, std_stderr_mg2, 'o--', ... + 'LineWidth', 1.5, 'MarkerSize', 6, 'CapSize', 5, 'DisplayName', 'Stripes to Droplets'); +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('B = 2.42 G', '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 +%% \ No newline at end of file diff --git a/Data-Analyzer/conductSpectralAnalysis.m b/Data-Analyzer/conductSpectralAnalysis.m index 83f3c2f..d31ff8c 100644 --- a/Data-Analyzer/conductSpectralAnalysis.m +++ b/Data-Analyzer/conductSpectralAnalysis.m @@ -1,18 +1,18 @@ -%% ===== Settings ===== +%% ===== D-S 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 = "//DyLabNAS/Data/TwoDGas/2025/06/23/"; -run = '0016'; +run = '0300'; folderPath = strcat(folderPath, run); cam = 5; angle = 0; -center = [1430, 2040]; +center = [1410, 2030]; span = [200, 200]; fraction = [0.1, 0.1]; @@ -43,29 +43,100 @@ 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 = '; +scan_parameter = 'ps_rot_mag_fin_pol_angle'; +% scan_parameter = 'rot_mag_field'; -savefolderPath = 'E:/Results - Experiment/B2.35G/'; -savefileName = 'Droplets'; +savefileName = 'DropletsToStripes'; font = 'Bahnschrift'; -skipUnshuffling = true; if strcmp(savefileName, 'DropletsToStripes') - scan_groups = 0:5:45; + scan_groups = 0:5:45; + titleString = 'Droplets to Stripes'; elseif strcmp(savefileName, 'StripesToDroplets') - scan_groups = 45:-5:0; + scan_groups = 45:-5:0; + titleString = 'Stripes to Droplets'; end % Flags +skipNormalization = false; +skipUnshuffling = true; skipPreprocessing = true; skipMasking = true; skipIntensityThresholding = true; skipBinarization = true; skipMovieRender = true; skipSaveFigures = false; +skipSaveOD = false; + +%% ===== S-D 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 = "//DyLabNAS/Data/TwoDGas/2025/06/24/"; + +run = '0001'; + +folderPath = strcat(folderPath, run); + +cam = 5; + +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; + +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'; + +savefileName = 'StripesToDroplets'; +font = 'Bahnschrift'; + +if strcmp(savefileName, 'DropletsToStripes') + scan_groups = 0:5:45; + titleString = 'Droplets to Stripes'; +elseif strcmp(savefileName, 'StripesToDroplets') + scan_groups = 45:-5:0; + titleString = 'Stripes to Droplets'; +end + +% Flags +skipNormalization = true; +skipUnshuffling = false; +skipPreprocessing = true; +skipMasking = true; +skipIntensityThresholding = true; +skipBinarization = true; +skipMovieRender = true; +skipSaveFigures = false; +skipSaveOD = 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. @@ -89,7 +160,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); @@ -108,7 +179,7 @@ else end end -%% ===== Get rotation angles ===== +% ===== Get rotation angles ===== scan_parameter_values = zeros(1, length(files)); % Get information about the '/globals' group @@ -127,7 +198,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); @@ -166,17 +237,10 @@ end %% ===== Run Fourier analysis over images ===== -fft_imgs = cell(1, nimgs); -spectral_contrast = zeros(1, nimgs); -spectral_weight = zeros(1, nimgs); -N_shots = length(od_imgs); - -avg_ps_accum = 0; -avg_S_k_accum = 0; -avg_S_theta_accum = 0; - -% Pre-allocate once sizes are known (after first run) -fft_size_known = false; +fft_imgs = cell(1, nimgs); +radial_spectral_contrast = zeros(1, nimgs); +angular_spectral_weight = zeros(1, nimgs); +N_shots = length(od_imgs); if ~skipMovieRender % Create VideoWriter object for movie @@ -194,7 +258,10 @@ if ~skipSaveFigures end end -% Display the cropped image +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 + for k = 1:N_shots IMG = od_imgs{k}; [IMGFFT, IMGPR] = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization); @@ -235,8 +302,13 @@ for k = 1:N_shots [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 - spectral_contrast(k) = computeSpectralContrast(fft_imgs{k}, r_min, r_max, Angular_Threshold); - spectral_weight(k) = trapz(theta_vals, S_theta); + radial_spectral_contrast(k) = computeRadialSpectralContrast(fft_imgs{k}, r_min, r_max, Angular_Threshold); + S_theta_norm = S_theta / max(S_theta); % Normalize to 1 + angular_spectral_weight(k) = trapz(theta_vals, S_theta_norm); + + 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 figure(1); clf @@ -268,14 +340,21 @@ for k = 1:N_shots ylabel('y (\mum)', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font); title('OD Image', 'FontSize', 16, 'FontWeight', 'bold', 'Interpreter', 'tex', 'FontName', font); - text(0.975, 0.975, [scan_parameter_text, num2str(scan_parameter_values(k), '%.2f'), ' G'], ... - 'Color', 'white', 'FontWeight', 'bold', 'FontSize', 14, ... - 'Interpreter', 'tex', 'Units', 'normalized', ... - 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top'); + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + text(0.975, 0.975, [num2str(scan_parameter_values(k), '%.1f^\\circ')], ... + 'Color', 'white', 'FontWeight', 'bold', 'FontSize', 14, ... + 'Interpreter', 'tex', 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top'); + else + text(0.975, 0.975, [num2str(scan_parameter_values(k), '%.2f'), ' G'], ... + 'Color', 'white', 'FontWeight', 'bold', 'FontSize', 14, ... + 'Interpreter', 'tex', 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top'); + end % ======= FFT POWER SPECTRUM (reciprocal space) ======= ax2 = nexttile; - imagesc(kx, ky, log(1 + abs(fft_imgs{k}).^2)); + imagesc(kx, ky, log(1 + ps_list{k})); axis image; set(gca, 'FontSize', 14, 'YDir', 'normal') xlabel('k_x [\mum^{-1}]', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font); @@ -299,8 +378,13 @@ for k = 1:N_shots % ======= ANGULAR DISTRIBUTION (S(θ)) ======= nexttile; - plot(theta_vals/pi, S_theta, 'LineWidth', 2); - set(gca, 'FontSize', 14, 'YScale', 'log', 'YLim', [1E4, 1E7]); + if ~skipNormalization + plot(theta_vals/pi, S_theta_norm, 'LineWidth', 2); + set(gca, 'FontSize', 14, 'YLim', [0, 1]); + else + plot(theta_vals/pi, S_theta, 'LineWidth', 2); + set(gca, 'FontSize', 14, 'YScale', 'log', 'YLim', [1E4, 1E7]); + end 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', ... @@ -316,20 +400,6 @@ for k = 1:N_shots drawnow; - if ~fft_size_known - fft_sz = size(fft_imgs{k}); - N_radial_bins_used = length(S_k_smoothed); - N_angular_bins_used = length(S_theta); - avg_ps_accum = zeros(fft_sz); - avg_S_k_accum = zeros(1, N_radial_bins_used); - avg_S_theta_accum = zeros(1, N_angular_bins_used); - fft_size_known = true; - end - - avg_ps_accum = avg_ps_accum + abs(fft_imgs{k}).^2; - avg_S_k_accum = avg_S_k_accum + S_k_smoothed; - avg_S_theta_accum = avg_S_theta_accum + S_theta; - if ~skipMovieRender % Capture the current frame and write it to the video frame = getframe(gcf); % Capture the current figure as a frame @@ -342,6 +412,14 @@ for k = 1:N_shots % Save current figure as PNG with high resolution print(gcf, fileNamePNG, '-dpng', '-r100'); % 300 dpi for high quality end + if ~skipSaveOD + odDataStruct = struct(); + odDataStruct.IMG = IMG; + odDataStruct.x = x; + odDataStruct.y = y; + odDataStruct.scan_parameter_value = scan_parameter_values(k); + save(fullfile(saveFolder, sprintf('od_image_%03d.mat', k)), '-struct', 'odDataStruct'); + end if skipMovieRender & skipSaveFigures pause(0.5); end @@ -352,46 +430,398 @@ if ~skipMovieRender close(videoFile); end -%% ===== Final Averages ===== -avg_ps = avg_ps_accum / N_shots; -avg_S_k = avg_S_k_accum / N_shots; -avg_S_theta = avg_S_theta_accum / N_shots; +%% Track across the transition -% Generate figure with 3 subplots -figure('Name', 'Average Spectral Analysis', 'Position', [400 200 1200 400]); -tavg = tiledlayout(1, 3, 'TileSpacing', 'compact', 'Padding', 'compact'); +% Assuming scan_parameter_values and spectral_weight are column vectors (or row vectors of same length) +[unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); -% ==== 1. Average FFT Power Spectrum ==== -nexttile; -imagesc(kx, ky, log(1 + avg_ps)); -axis image; -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('Average Power Spectrum', 'FontSize', 16, 'FontWeight', 'bold'); -colorbar; -colormap(Colormaps.coolwarm()); +% Preallocate arrays +mean_sc = zeros(size(unique_scan_parameter_values)); +stderr_sc = zeros(size(unique_scan_parameter_values)); -% ==== 2. Average Radial Spectral Distribution ==== -nexttile; -plot(k_rho_vals, avg_S_k, 'LineWidth', 2); -xlabel('k_\rho [\mum^{-1}]', 'Interpreter', 'tex', 'FontSize', 14); -ylabel('Magnitude (a.u.)', 'Interpreter', 'tex', 'FontSize', 14); -title('Average S(k_\rho)', 'FontSize', 16, 'FontWeight', 'bold'); -set(gca, 'FontSize', 14, 'YScale', 'log', 'XLim', [min(k_rho_vals), max(k_rho_vals)]); +% Loop through each unique theta and compute mean and standard error +for i = 1:length(unique_scan_parameter_values) + group_vals = radial_spectral_contrast(idx == i); + mean_sc(i) = mean(group_vals); + stderr_sc(i) = std(group_vals) / sqrt(length(group_vals)); % standard error = std / sqrt(N) +end + +figure(2); +set(gcf,'Position',[100 100 950 750]) +errorbar(unique_scan_parameter_values, mean_sc, stderr_sc, 'o--', ... + 'LineWidth', 1.5, 'MarkerSize', 6, 'CapSize', 5); +set(gca, 'FontSize', 14); % For tick labels only +hXLabel = xlabel('\alpha (degrees)', 'Interpreter', 'tex'); +hYLabel = ylabel('Radial Spectral Contrast', 'Interpreter', 'tex'); +hTitle = title(titleString, '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 + + +% Preallocate arrays +mean_sw = zeros(size(unique_scan_parameter_values)); +stderr_sw = zeros(size(unique_scan_parameter_values)); + +% Loop through each unique theta and compute mean and standard error +for i = 1:length(unique_scan_parameter_values) + group_vals = angular_spectral_weight(idx == i); + mean_sw(i) = mean(group_vals); + stderr_sw(i) = std(group_vals) / sqrt(length(group_vals)); % standard error = std / sqrt(N) +end + +figure(3); +set(gcf,'Position',[100 100 950 750]) +errorbar(unique_scan_parameter_values, mean_sw, stderr_sw, 'o--', ... + 'LineWidth', 1.5, 'MarkerSize', 6, 'CapSize', 5); +set(gca, 'FontSize', 14); % For tick labels only +hXLabel = xlabel('\alpha (degrees)', 'Interpreter', 'tex'); +hYLabel = ylabel('Angular Spectral Weight', 'Interpreter', 'tex'); +hTitle = title(titleString, '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; -% ==== 3. Average Angular Spectral Distribution ==== -nexttile; -plot(theta_vals/pi, avg_S_theta, 'LineWidth', 2); -xlabel('\theta/\pi [rad]', 'Interpreter', 'tex', 'FontSize', 14); -ylabel('Magnitude (a.u.)', 'Interpreter', 'tex', 'FontSize', 14); -title('Average S(\theta)', 'FontSize', 16, 'FontWeight', 'bold'); -set(gca, 'FontSize', 14, 'YScale', 'log'); -grid on; -ax = gca; -ax.XMinorGrid = 'on'; -ax.YMinorGrid = 'on'; +%% Plot Averages + +% Group by scan parameter values +[unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); +N_params = numel(unique_scan_parameter_values); + +if ~skipSaveFigures + % Define folder for saving images + saveFolder = [savefileName '_SavedFigures']; + if ~exist(saveFolder, 'dir') + mkdir(saveFolder); + end +end + +% Loop over each unique parameter value +for p = 1:N_params + current_param = unique_scan_parameter_values(p); + indices = find(idx == p); % Indices of shots for this parameter + N_shots = numel(indices); + + % Initialize accumulators + avg_ps = 0; + avg_S_k = 0; + avg_S_theta = 0; + + % Accumulate values + for j = 1:N_shots + avg_ps = avg_ps + ps_list{indices(j)}; + avg_S_k = avg_S_k + s_k_list{indices(j)}; + avg_S_theta = avg_S_theta + s_theta_list{indices(j)}; + end + + % Average over repetitions + avg_ps = avg_ps / N_shots; + avg_S_k = avg_S_k / N_shots; + avg_S_theta = avg_S_theta / N_shots; + + % ==== Plot ==== + figure(3); + set(gcf,'Position',[400 200 1200 400]) + tavg = tiledlayout(1, 3, 'TileSpacing', 'compact', 'Padding', 'compact'); + + % 1. Power Spectrum + nexttile; + imagesc(kx, ky, log(1 + avg_ps)); + axis image; + 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('Average Power Spectrum', 'FontSize', 16, 'FontWeight', 'bold'); + colorbar; + colormap(Colormaps.coolwarm()); + + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + text(0.975, 0.975, [num2str(current_param, '%.1f^\\circ')], ... + 'Color', 'white', 'FontWeight', 'bold', 'FontSize', 14, ... + 'Interpreter', 'tex', 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top'); + else + text(0.975, 0.975, [num2str(current_param, '%.2f'), ' G'], ... + 'Color', 'white', 'FontWeight', 'bold', 'FontSize', 14, ... + 'Interpreter', 'tex', 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top'); + end + + % 2. Radial Spectrum + nexttile; + plot(k_rho_vals, avg_S_k, 'LineWidth', 2); + xlabel('k_\rho [\mum^{-1}]', 'Interpreter', 'tex', 'FontSize', 14); + ylabel('Magnitude (a.u.)', 'Interpreter', 'tex', 'FontSize', 14); + title('Average S(k_\rho)', 'FontSize', 16, 'FontWeight', 'bold'); + set(gca, 'FontSize', 14, 'YScale', 'log', ... + 'XLim', [min(k_rho_vals), max(k_rho_vals)]); + grid on; + + % 3. Angular Spectrum + nexttile; + plot(theta_vals/pi, avg_S_theta, 'LineWidth', 2); + xlabel('\theta/\pi [rad]', 'Interpreter', 'tex', 'FontSize', 14); + ylabel('Magnitude (a.u.)', 'Interpreter', 'tex', 'FontSize', 14); + title('Average S(\theta)', 'FontSize', 16, 'FontWeight', 'bold'); + set(gca, 'FontSize', 14, 'YScale', 'log', ... + 'YLim', [1E4, 1E7]); + grid on; + ax = gca; + ax.XMinorGrid = 'on'; + ax.YMinorGrid = 'on'; + + drawnow; + + % ==== Save Figure ==== + if ~skipSaveFigures + % Create a filename for each averaged plot + fileNamePNG = fullfile(saveFolder, sprintf('fft_avg_analysis_param_%03d.png', p)); + + % Save current figure as PNG with high resolution + print(gcf, fileNamePNG, '-dpng', '-r300'); % 300 dpi for high quality + else + pause(0.5) + end +end + +%% ========= Replot OD images ========== + +% Settings +filePattern = fullfile(saveFolder, 'od_image_*.mat'); +files = dir(filePattern); +colormapName = 'inferno'; +showText = true; +showOverlay = true; +font = 'Bahnschrift'; + +% Load and organize all OD images by parameter and repetition +nFiles = length(files); +if nFiles == 0 + error('No .mat OD image files found in folder: %s', saveFolder); +end + +% Load all data and extract parameter values +scan_values = zeros(1, nFiles); +allData = cell(1, nFiles); + +for k = 1:nFiles + S = load(fullfile(files(k).folder, files(k).name)); + scan_values(k) = S.scan_parameter_value; + allData{k} = S; +end + +% Get unique parameter values +unique_params = unique(scan_values); +nParams = numel(unique_params); + +% Group images: paramData{i} = [rep1, rep2, ...] + +if strcmp(savefileName, 'StripesToDroplets') + unique_params = fliplr(unique_params); +end + +paramData = cell(1, nParams); +for i = 1:nParams + idxs = find(scan_values == unique_params(i)); + paramData{i} = allData(idxs); +end + +% Get number of repetitions (assumes all same) +nReps = max(cellfun(@numel, paramData)); + +% Initialize figure with one row, nParams columns +figure(100); clf; +% Set number of columns (e.g., 4 or auto-compute from nParams) +nCols = min(4, nParams); +nRows = ceil(nParams / nCols); + +% Create tiled layout with multiple rows +t = tiledlayout(nRows, nCols, 'TileSpacing', 'compact', 'Padding', 'compact'); + +% Adjust figure size accordingly +set(gcf, 'Position', [100 100 300*nCols 300*nRows]); + +% Pre-create image handles +axesArray = gobjects(1, nParams); +imgArray = gobjects(1, nParams); +textArray = gobjects(1, nParams); + +for i = 1:nParams + S = paramData{i}{1}; % First repetition to initialize + + ax = nexttile(i); + axesArray(i) = ax; + + imgArray(i) = imagesc(S.x, S.y, S.IMG); + axis equal tight; + set(ax, 'YDir', 'normal'); + colormap(ax, Colormaps.(colormapName)()); + colorbar; + + xlabel('x [\mum]', 'Interpreter', 'tex', 'FontSize', 12, 'FontName', font); + ylabel('y [\mum]', 'Interpreter', 'tex', 'FontSize', 12, 'FontName', font); + + if showOverlay + hold on; + drawODOverlays(S.x(1), S.y(1), S.x(end), S.y(end)); + drawODOverlays(S.x(end), S.y(1), S.x(1), S.y(end)); + hold off; + end + + % Add initial label + if showText + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + labelStr = sprintf('%.1f^\\circ', S.scan_parameter_value); + else + labelStr = sprintf('%.2f G', S.scan_parameter_value); + end + textArray(i) = text(ax, 0.975, 0.975, labelStr, ... + 'Color', 'white', 'FontWeight', 'bold', ... + 'FontSize', 12, 'Interpreter', 'tex', ... + 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', ... + 'VerticalAlignment', 'top'); + end +end + +% 🔁 Loop over repetitions +for rep = 1:nReps + for i = 1:nParams + repsForParam = paramData{i}; + if rep <= numel(repsForParam) + S = repsForParam{rep}; + imgArray(i).CData = S.IMG; + + % Update text if needed (optional) + if showText + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + labelStr = sprintf('%.1f^\\circ', S.scan_parameter_value); + else + labelStr = sprintf('%.2f G', S.scan_parameter_value); + end + textArray(i).String = labelStr; + end + end + end + drawnow; % Update figure + + % Optional: pause or save frame + pause(0.2); +end + +%% ========= Replot OD images in chunks by parameter ========== + +% Settings +filePattern = fullfile(saveFolder, 'od_image_*.mat'); +files = dir(filePattern); +colormapName = 'inferno'; +showText = true; +showOverlay = true; +font = 'Bahnschrift'; +paramStep = 2; % Show every paramStep-th parameter +pauseTime = 0.2; % Seconds between repetitions + +% Load and organize all OD images +nFiles = numel(files); +scan_values = zeros(1, nFiles); +allData = cell(1, nFiles); + +for k = 1:nFiles + S = load(fullfile(files(k).folder, files(k).name)); + scan_values(k) = S.scan_parameter_value; + allData{k} = S; +end + +% Sort and group by unique parameter values +[unique_params, ~, ic] = unique(scan_values); +nParams = numel(unique_params); + +paramGroups = cell(1, nParams); +for i = 1:nParams + paramGroups{i} = allData(ic == i); +end + +if strcmp(savefileName, 'StripesToDroplets') + unique_params = fliplr(unique_params); + paramGroups = fliplr(paramGroups); +end + +% Select a subset of parameters +selectedIdx = 1:paramStep:nParams; +nDisplayParams = numel(selectedIdx); +selectedGroups = paramGroups(selectedIdx); + +% Get max number of repetitions +nReps = max(cellfun(@numel, selectedGroups)); + +% Initialize figure +figure(101); clf; +tiledlayout(1, nDisplayParams, 'TileSpacing', 'compact', 'Padding', 'compact'); +set(gcf, 'Position', [100 100 300*nDisplayParams 300]); + +imgArray = gobjects(1, nDisplayParams); +textArray = gobjects(1, nDisplayParams); + +% Initial plot (repetition 1) +for j = 1:nDisplayParams + ax = nexttile; + group = selectedGroups{j}; + S = group{1}; + + imgArray(j) = imagesc(S.x, S.y, S.IMG); + axis equal tight; + set(ax, 'YDir', 'normal'); + colormap(ax, Colormaps.(colormapName)()); + colorbar; + + xlabel('x [\mum]', 'Interpreter', 'tex', 'FontSize', 12, 'FontName', font); + ylabel('y [\mum]', 'Interpreter', 'tex', 'FontSize', 12, 'FontName', font); + + if showOverlay + hold on; + drawODOverlays(S.x(1), S.y(1), S.x(end), S.y(end)); + drawODOverlays(S.x(end), S.y(1), S.x(1), S.y(end)); + hold off; + end + + if showText + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + labelStr = sprintf('%.1f^\\circ', S.scan_parameter_value); + else + labelStr = sprintf('%.2f G', S.scan_parameter_value); + end + textArray(j) = text(0.975, 0.975, labelStr, ... + 'Color', 'white', 'FontWeight', 'bold', ... + 'FontSize', 12, 'Interpreter', 'tex', ... + 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', ... + 'VerticalAlignment', 'top'); + end +end + +% Loop through repetitions +for rep = 1:nReps + for j = 1:nDisplayParams + group = selectedGroups{j}; + if rep <= numel(group) + S = group{rep}; + imgArray(j).CData = S.IMG; + + if showText + if strcmp(scan_parameter, 'ps_rot_mag_fin_pol_angle') + textArray(j).String = sprintf('%.1f^\\circ', S.scan_parameter_value); + else + textArray(j).String = sprintf('%.2f G', S.scan_parameter_value); + end + end + end + end + drawnow; + pause(pauseTime); +end + %% Helper Functions function [IMGFFT, IMGPR] = computeFourierTransform(I, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization) @@ -548,7 +978,7 @@ function [theta_vals, S_theta] = computeAngularSpectralDistribution(IMGFFT, r_mi end end -function contrast = computeSpectralContrast(IMGFFT, r_min, r_max, threshold) +function contrast = computeRadialSpectralContrast(IMGFFT, r_min, r_max, threshold) % Apply threshold to isolate strong peaks IMGFFT(IMGFFT < threshold) = 0; diff --git a/Data-Analyzer/extractAutocorrelation.m b/Data-Analyzer/extractAutocorrelation.m index 4db9b0e..477f812 100644 --- a/Data-Analyzer/extractAutocorrelation.m +++ b/Data-Analyzer/extractAutocorrelation.m @@ -3,16 +3,16 @@ groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1 "/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); cam = 5; angle = 0; -center = [1430, 2040]; +center = [1410, 2030]; span = [200, 200]; fraction = [0.1, 0.1]; @@ -43,29 +43,30 @@ 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 = '; +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'; +savefileName = 'DropletsToStripes'; font = 'Bahnschrift'; -skipUnshuffling = true; if strcmp(savefileName, 'DropletsToStripes') - scan_groups = 0:5:45; + scan_groups = 0:5:45; + titleString = 'Droplets to Stripes'; elseif strcmp(savefileName, 'StripesToDroplets') - scan_groups = 45:-5:0; + scan_groups = 45:-5:0; + titleString = 'Stripes to Droplets'; end % Flags +skipUnshuffling = true; skipPreprocessing = true; skipMasking = true; skipIntensityThresholding = true; skipBinarization = true; skipMovieRender = true; -skipSaveFigures = false; +skipSaveFigures = true; %% ===== 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. @@ -172,9 +173,8 @@ for k = 1:N_shots 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, []); + [theta_values, 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) @@ -186,15 +186,15 @@ end % Grouping by scan parameter value (e.g., alpha) [unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); -% Number of unique alpha values -N_alpha = length(unique_scan_parameter_values); +% Number of unique parameter values +N_params = length(unique_scan_parameter_values); % Preallocate result arrays -g2_all = zeros(N_alpha, N_angular_bins); -g2_error_all = zeros(N_alpha, N_angular_bins); +g2_all = zeros(N_params, N_angular_bins); +g2_error_all = zeros(N_params, N_angular_bins); % Compute g2 -for i = 1:N_alpha +for i = 1:N_params group_idx = find(idx == i); group_data = delta_nkr_all(group_idx, :); @@ -214,23 +214,20 @@ for i = 1:N_alpha end end -% Reconstruct theta axis from any one of the stored values -theta_vals = theta_values{1}; % assuming it's in radians - -% Number of unique alpha values -nAlpha = size(g2_all, 1); +% Number of unique parameter values +nParams = size(g2_all, 1); % Generate a colormap with enough unique colors -cmap = sky(nAlpha); % You can also try 'jet', 'turbo', 'hot', etc. +cmap = sky(nParams); % You can also try 'jet', 'turbo', 'hot', etc. figure(1); clf; set(gcf,'Position',[100 100 950 750]) hold on; -legend_entries = cell(nAlpha, 1); +legend_entries = cell(nParams, 1); -for i = 1:nAlpha - errorbar(theta_vals/pi, g2_all(i, :), g2_error_all(i, :), ... +for i = 1:nParams + errorbar(theta_values/pi, g2_all(i, :), g2_error_all(i, :), ... 'o', 'Color', cmap(i,:), ... 'MarkerSize', 3, 'MarkerFaceColor', cmap(i,:), ... 'CapSize', 4); @@ -241,15 +238,15 @@ for i = 1:nAlpha end end -ylim([-1.5 3.0]); % Set y-axis limits here +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('Change across transition', 'Interpreter', 'tex'); +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 +set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold'); % Set font and size for title grid on; %% Helper Functions diff --git a/Data-Analyzer/extractCustomCorrelation.m b/Data-Analyzer/extractCustomCorrelation.m index f0db2e9..66ca5a4 100644 --- a/Data-Analyzer/extractCustomCorrelation.m +++ b/Data-Analyzer/extractCustomCorrelation.m @@ -3,16 +3,16 @@ groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1 "/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); cam = 5; angle = 0; -center = [1430, 2040]; +center = [1410, 2030]; span = [200, 200]; fraction = [0.1, 0.1]; @@ -43,29 +43,30 @@ 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 = '; +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'; +savefileName = 'DropletsToStripes'; font = 'Bahnschrift'; -skipUnshuffling = true; if strcmp(savefileName, 'DropletsToStripes') - scan_groups = 0:5:45; + scan_groups = 0:5:45; + titleString = 'Droplets to Stripes'; elseif strcmp(savefileName, 'StripesToDroplets') - scan_groups = 45:-5:0; + scan_groups = 45:-5:0; + titleString = 'Stripes to Droplets'; end % Flags +skipUnshuffling = true; skipPreprocessing = true; skipMasking = true; skipIntensityThresholding = true; skipBinarization = true; skipMovieRender = true; -skipSaveFigures = false; +skipSaveFigures = true; %% ===== 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. @@ -135,103 +136,94 @@ theta_values = cell(1, nimgs); N_shots = length(od_imgs); -% Compute FFT +% Compute FFT for all images 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⁻¹ + + kx_full = 2 * pi * vx * 1E-6; 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) +% 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, :) = spectral_distribution{k}; end -% Grouping by scan parameter value (e.g., alpha) +% Group by scan parameter values (e.g., alpha, angle, etc.) [unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values); +N_params = length(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 +% Define angular range and conversion +angle_range = 180; angle_per_bin = angle_range / N_angular_bins; - -max_peak_angle = 60; +max_peak_angle = 180; max_peak_bin = round(max_peak_angle / angle_per_bin); +% Parameters for search window_size = 10; angle_threshold = 100; -ref_peak_angles = []; -angle_at_max_g2 = []; -g2_values = []; +% 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_idx = find(idx == i); - group_data = delta_nkr_all(group_idx, :); + group_idx = find(idx == i); + group_data = delta_nkr_all(group_idx, :); + N_reps = size(group_data, 1); - for j = 1: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 for peak only in 0° to 90° + + % Restrict search to 0–60° for highest peak 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 + peak_angle = (peak_idx - 1) * angle_per_bin; - % 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)); @@ -240,25 +232,65 @@ for i = 1:N_params 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; + angles(k) = mod((peak_idx - 1 + offsets(k)) * angle_per_bin, angle_range); end [max_corr, max_idx] = max(correlations); - g2_values(end+1) = max_corr; - angle_at_max_g2(end+1) = angles(max_idx); + 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 histograms within 0-180 degrees only +%% ── Mean ± Std vs. scan parameter ────────────────────────────────────── +% Compute standard error instead of standard deviation +std_error_g2_values = zeros(1, N_params); +for i = 1:N_params + n_i = numel(g2_all_per_group{i}); % Number of repetitions for this param + std_error_g2_values(i) = sqrt(var_max_g2_values(i) / n_i); +end + +% Plot mean ± SEM +figure(1); +set(gcf,'Position',[100 100 950 750]) +set(gca, 'FontSize', 14); % For tick labels only +errorbar(unique_scan_parameter_values, ... % x-axis + mean_max_g2_values, ... % y-axis (mean) + std_error_g2_values, ... % ± SEM + '--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; + +% Define folder for saving images +saveFolder = [savefileName '_SavedFigures']; +if ~exist(saveFolder, 'dir') + mkdir(saveFolder); +end +save([saveFolder savefileName '.mat'], 'unique_scan_parameter_values', 'mean_max_g2_values', 'std_error_g2_values'); + +%{ +%% Plot histograms within 0-180 degrees only figure(1); hold on; @@ -324,7 +356,7 @@ text(mode_ref, yl(2)*0.9, sprintf('%.1f°', mode_ref), 'HorizontalAlignment', 'c % 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) diff --git a/Data-Analyzer/extractQuantities.m b/Data-Analyzer/extractQuantities.m new file mode 100644 index 0000000..248de98 --- /dev/null +++ b/Data-Analyzer/extractQuantities.m @@ -0,0 +1,118 @@ +%% Parameters + +% === Define folders and settings === + +% === Define folders and settings === + +baseFolder = '//DyLabNAS/Data/TwoDGas/2025/04/'; + +dates = ["01", "02"]; % Example: three folders +runs = { + ["0059", "0060", "0061"], + ["0007", "0008", "0009", "0010", "0011"] +}; + +options.scan_parameter = 'rot_mag_fin_pol_angle'; +options.scan_groups = 0:10:50; +options.cam = 5; + +% Image cropping and alignment +options.angle = 0; +options.center = [1285, 2100]; +options.span = [200, 200]; +options.fraction = [0.1, 0.1]; + +% Imaging and calibration parameters +options.pixel_size = 5.86e-6; % in meters +options.magnification = 23.94; +options.removeFringes = false; +options.ImagingMode = 'HighIntensity'; +options.PulseDuration = 5e-6; + +% Fourier analysis: Radial +options.theta_min = deg2rad(0); +options.theta_max = deg2rad(180); +options.N_radial_bins = 500; +options.Radial_Sigma = 2; +options.Radial_WindowSize = 5; % Must be odd + +% Fourier analysis: Angular +options.r_min = 10; +options.r_max = 20; +options.k_min = 1.2; % in μm⁻¹ +options.k_max = 2.2; % in μm⁻¹ +options.N_angular_bins = 180; +options.Angular_Threshold = 75; +options.Angular_Sigma = 2; +options.Angular_WindowSize = 5; + +% Optional visualization / zooming +options.zoom_size = 50; + +% Optional flags or settings struct +options.skipUnshuffling = false; +options.skipPreprocessing = true; +options.skipMasking = true; +options.skipIntensityThresholding = true; +options.skipBinarization = true; + +% === Loop through folders and collect results === + +results_all = []; + +assert(length(dates) == length(runs), ... + 'Each entry in `dates` must correspond to a cell in `runs`.'); + +for i = 1:length(dates) + currentDate = dates(i); + currentRuns = runs{i}; + + for j = 1:length(currentRuns) + runID = currentRuns(j); + folderPath = fullfile(baseFolder, currentDate, runID); + + if ~endsWith(folderPath, filesep) + options.folderPath = [char(folderPath) filesep]; + else + options.folderPath = char(folderPath); + end + + try + % Unpack options struct into name-value pairs + args = [fieldnames(options), struct2cell(options)]'; + args = args(:)'; + + results = analyzeFolder(args{:}); + results_all = [results_all; results]; + catch ME + warning("Error processing %s/%s: %s", currentDate, runID, ME.message); + end + end +end + + +%% Plotting heatmap of mean_max_g2_values + +N_x = length(options.scan_groups); +N_y = length(results_all) / N_x; + +% Preallocate +g2_matrix = zeros(N_y, N_x); +angle_matrix = zeros(N_y, N_x); + +for i = 1:length(results_all) + row = ceil(i / N_x); % outer parameter (e.g., date) + col = mod(i-1, N_x) + 1; % inner scan parameter + g2_matrix(row, col) = results_all(i).mean_max_g2_values(col); + angle_matrix(row, col) = results_all(i).mean_max_g2_angle(col); +end + +% Plot heatmap +figure; +imagesc(options.scan_groups, 1:N_y, g2_matrix); +xlabel('Scan Parameter (e.g. Angle)'); +ylabel('Scan Set Index'); +title('Mean g_2 Value Heatmap'); +colorbar; + + diff --git a/Data-Analyzer/plotImages.m b/Data-Analyzer/plotImages.m index 5a6fead..e445517 100644 --- a/Data-Analyzer/plotImages.m +++ b/Data-Analyzer/plotImages.m @@ -4,29 +4,42 @@ groupList = ["/images/MOT_3D_Camera/in_situ_absorption", "/images/ODT_1_Axi "/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/05/22/"; +folderPath = "//DyLabNAS/Data/TwoDGas/2025/04/01/"; -run = '0078'; +run = '0059'; folderPath = strcat(folderPath, run); cam = 5; angle = 0; -center = [1375, 2020]; +center = [1285, 2100]; span = [200, 200]; fraction = [0.1, 0.1]; -pixel_size = 5.86e-6; +pixel_size = 5.86e-6; % in meters +magnification = 23.94; removeFringes = false; -%% Compute OD image, rotate and extract ROI for analysis +ImagingMode = 'HighIntensity'; +PulseDuration = 5e-6; + +% Plotting and saving +scan_parameter = 'rot_mag_fin_pol_angle'; +scan_groups = 0:10:50; +savefileName = 'DropletsToStripes'; +font = 'Bahnschrift'; + +% Flags +skipUnshuffling = 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)); +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; @@ -34,16 +47,15 @@ for k = 1 : length(files) fprintf(1, 'Now reading %s\n', fullFileName); - atm_img = double(imrotate(h5read(fullFileName, append(groupList(cam), "/atoms")), angle)); % im2double rescales values to between [0, 1], use double instead + 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), 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); @@ -61,69 +73,121 @@ else 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, '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 + +% ===== Unshuffle if necessary to do so ===== + +if ~skipUnshuffling + n_values = length(scan_groups); + n_total = length(scan_parameter_values); + + % Infer number of repetitions + n_reps = n_total / n_values; + + % Preallocate ordered arrays + ordered_scan_values = zeros(1, n_total); + ordered_od_imgs = cell(1, n_total); + + counter = 1; + + for rep = 1:n_reps + for val = scan_groups + % Find the next unused match for this val + idx = find(scan_parameter_values == val, 1, 'first'); + + % Assign and remove from list to avoid duplicates + ordered_scan_values(counter) = scan_parameter_values(idx); + ordered_od_imgs{counter} = od_imgs{idx}; + + % Mark as used by removing + scan_parameter_values(idx) = NaN; % NaN is safe since original values are 0:5:45 + od_imgs{idx} = []; % empty cell so it won't be matched again + + counter = counter + 1; + end + end + + % Now assign back + scan_parameter_values = ordered_scan_values; + od_imgs = ordered_od_imgs; +end + %% Display Images figure(1) clf set(gcf,'Position',[50 50 950 750]) -% 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; +% Get image size in pixels +[Ny, Nx] = size(od_imgs{1}); -% 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)); +% Define pixel size and magnification (if not already defined earlier) +dx = pixel_size / magnification; % e.g. in meters +dy = dx; % assuming square pixels + +% Define x and y axes in μm (centered at image center) +x = ((1:Nx) - ceil(Nx/2)) * dx * 1E6; % micrometers +y = ((1:Ny) - ceil(Ny/2)) * dy * 1E6; % Display the cropped image for k = 1 : length(od_imgs) - imagesc(x_range, y_range, od_imgs{k}) - axis equal tight; - hcb = colorbar; - hL = ylabel(hcb, 'Optical Density', 'FontSize', 16); - set(hL,'Rotation',-90); - colormap jet; - % set(gca,'CLim',[0 0.4]); - 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 - xlabel('Horizontal', 'Interpreter', 'tex','FontSize',16); - ylabel('Vertical', 'Interpreter', 'tex','FontSize',16); + imagesc(x, y, od_imgs{k}); + hold on; - drawnow - pause(0.5) + % Convert pixel grid to µm (already done: x and y axes) + % Draw ↘ diagonal (top-left to bottom-right) + drawODOverlays(x(1), y(1), x(end), y(end)); + + % Draw ↙ diagonal (top-right to bottom-left) + drawODOverlays(x(end), y(1), x(1), y(end)); + + hold off; + axis equal tight; + colormap(Colormaps.inferno()); + set(gca, 'FontSize', 14, 'YDir', 'normal'); + + if strcmp(scan_parameter, 'rot_mag_fin_pol_angle') + text(0.975, 0.975, [num2str(scan_parameter_values(k), '%.1f^\\circ')], ... + 'Color', 'white', 'FontWeight', 'bold', 'FontSize', 24, ... + 'Interpreter', 'tex', 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top'); + else + text(0.975, 0.975, [num2str(scan_parameter_values(k), '%.2f'), ' G'], ... + 'Color', 'white', 'FontWeight', 'bold', 'FontSize', 24, ... + 'Interpreter', 'tex', 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top'); + end + + colorbarHandle = colorbar; + ylabel(colorbarHandle, 'Optical Density', 'Rotation', -90, 'FontSize', 14, 'FontName', font); + + xlabel('x (\mum)', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font); + ylabel('y (\mum)', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font); + title('OD Image', 'FontSize', 16, 'FontWeight', 'bold', 'Interpreter', 'tex', 'FontName', font); + + drawnow; + pause(0.5); end -%% Overlay images - -% image_below = ; -% image_top = ; - -% Display the first image (opaque) -figure (2); -clf -set(gcf,'Position',[50 50 950 750]) - -imagesc(x_range, y_range, image_below); -hold on; % Allow overlaying of the second image -h = imagesc(x_range, y_range, image_top); % Display the second image (translucent) -set(h, 'AlphaData', 0.6); % Adjust transparency: 0 is fully transparent, 1 is fully opaque -axis equal tight; -hcb = colorbar; -hL = ylabel(hcb, 'Optical Density', 'FontSize', 16); -set(hL,'Rotation',-90); -colormap jet; -set(gca,'CLim',[0 1.0]); -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 -xlabel('X', 'Interpreter', 'tex','FontSize',16); -ylabel('Y', 'Interpreter', 'tex','FontSize',16); -hold off; - %% Helper Functions function ret = getBkgOffsetFromCorners(img, x_fraction, y_fraction) @@ -174,27 +238,109 @@ 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 drawODOverlays(x1, y1, x2, y2) + + % Parameters + tick_spacing = 10; % µm between ticks + tick_length = 2; % µm tick mark length + line_color = [0.5 0.5 0.5]; + tick_color = [0.5 0.5 0.5]; + font_size = 10; + + % Vector from start to end + dx = x2 - x1; + dy = y2 - y1; + L = sqrt(dx^2 + dy^2); + + % Unit direction vector along diagonal + ux = dx / L; + uy = dy / L; + + % Perpendicular unit vector for ticks + perp_ux = -uy; + perp_uy = ux; + + % Midpoint (center) + xc = (x1 + x2) / 2; + yc = (y1 + y2) / 2; + + % Number of positive and negative ticks + n_ticks = floor(L / (2 * tick_spacing)); + + % Draw main diagonal line + plot([x1 x2], [y1 y2], '--', 'Color', line_color, 'LineWidth', 1.2); + + for i = -n_ticks:n_ticks + d = i * tick_spacing; + xt = xc + d * ux; + yt = yc + d * uy; + + % Tick line 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: centered at tick, offset slightly along diagonal + if d ~= 0 + text(xt, yt, sprintf('%+d', d), ... + 'Color', tick_color, ... + 'FontSize', font_size, ... + 'HorizontalAlignment', 'center', ... + 'VerticalAlignment', 'bottom', ... + 'Rotation', atan2d(dy, dx)); + end + end end