Added a generic fit progress viewer and then scripts to run atom number and temperature extraction.

This commit is contained in:
Karthik 2025-09-13 00:29:08 +02:00
parent ad048ac2ef
commit 839d84035e
4 changed files with 683 additions and 83 deletions

View File

@ -0,0 +1,234 @@
function results = runFitProgressViewer(od_imgs, model, quantities, plotConfig, extraParams)
%% runFitProgressViewer
% Author: Karthik
% Date: 2025-09-12
% Version: 1.0
%
% Description:
% Generic batch fit viewer for 2D OD images using any model that
% implements guess() and fit(). Dynamically updates image, fit, residual,
% and optional bottom-row metrics (atom number, condensate fraction, temperature)
%
% Inputs:
% od_imgs : cell array of 2D images to fit
% model : object implementing guess() and fit()
% quantities : cell array of field names to compute/display
% plotConfig : struct controlling figure appearance and bottom row plots
% .fontName, .fontSize, .fontWeight, .colormapName, .scatterLineSpec
% .bottomRowLabels : corresponding y-axis labels
% .bottomRowUnits : scaling for display
% .bottomRowTitles : optional subplot titles (default = labels)
% extraParams : struct of optional parameters for the model (e.g., ToF)
%
% Notes:
% Optional notes, references.
arguments
od_imgs cell
model
quantities cell
plotConfig struct = struct()
extraParams struct = struct()
end
%% --- Default configuration ---
defaults = struct(...
'fontName', 'Bahnschrift', ...
'fontSize', 16, ...
'fontWeight', 'bold', ...
'colormapName', 'sky', ...
'scatterLineSpec', '-o', ...
'bottomRowFields', quantities, ... % <- derived from user input
'bottomRowLabels', {'Atom Number','# Condensed','Temp'}, ...
'bottomRowUnits', [1,1,1] ...
);
%% --- Merge user config with defaults ---
config = struct();
flds = fieldnames(plotConfig);
for k = 1:numel(flds)
config.(flds{k}) = plotConfig.(flds{k});
end
% Ensure bottomRowFields is set from quantities if not provided
if ~isfield(config,'bottomRowFields') || isempty(config.bottomRowFields)
config.bottomRowFields = quantities;
end
numImages = numel(od_imgs);
fprintf('\n[INFO] Starting processing of %d images...\n', numImages);
%% --- Preallocate results struct ---
results = repmat(struct('imageIndex',[],'fitResult',[],'gof',[], ...
'params',[],'fitData',[],'residuals',[],'rsquare',[],'status','Not processed'), ...
numImages,1);
for i = 1:numImages
results(i).imageIndex = i;
end
%% --- Create or reuse figure ---
hFig = findobj('Type','figure','Tag','FitProgressViewer');
if isempty(hFig)
hFig = figure('Position',[100,100,1450,850], ...
'NumberTitle','off', ...
'Name','Fit Progress Viewer', ...
'Tag','FitProgressViewer');
else
figure(hFig); clf;
end
t = tiledlayout(2,3,'TileSpacing','compact','Padding','compact');
%% --- Pre-create image/fit/residual axes ---
[axOriginal,hOriginal] = createImageAxis(nexttile(1), 'OD Image');
[axFit,hFit] = createImageAxis(nexttile(2), 'Fit');
[axResidual,hResidual] = createImageAxis(nexttile(3), 'Residual');
%% --- Pre-create bottom-row axes ---
axBottom = gobjects(numel(config.bottomRowFields),1);
scatterBottom = gobjects(numel(config.bottomRowFields),1);
for k = 1:numel(config.bottomRowFields)
axBottom(k) = nexttile(3+k);
scatterBottom(k) = plot(axBottom(k), nan, nan, config.scatterLineSpec);
hold(axBottom(k),'on'); grid(axBottom(k),'on');
xlabel(axBottom(k),'Image Index','FontName',config.fontName);
ylabel(axBottom(k),config.bottomRowLabels{k},'FontName',config.fontName);
% Use bottomRowTitles if provided, else fallback to labels
if isfield(config,'bottomRowTitles') && numel(config.bottomRowTitles) >= k
titleStr = config.bottomRowTitles{k};
else
titleStr = config.bottomRowLabels{k};
end
title(axBottom(k), titleStr, 'FontName',config.fontName, ...
'FontSize',config.fontSize, 'FontWeight',config.fontWeight);
end
%% --- Apply consistent font formatting ---
allAxes = [axOriginal, axFit, axResidual, axBottom(:)'];
for ax = allAxes, set(ax,'FontSize',config.fontSize,'FontName',config.fontName); end
%% --- Main batch loop ---
for i = 1:numImages
currentImg = od_imgs{i};
if isempty(currentImg) || ~isnumeric(currentImg) || all(isnan(currentImg(:)))
warning('Image %d empty or invalid. Skipping',i);
results(i).status = 'Invalid image'; continue;
end
[ny,nx] = size(currentImg); x = 1:nx; y = 1:ny;
%% --- Model guess and fit ---
params = model.guess(currentImg,x,y);
if isempty(fieldnames(extraParams))
[fitResult,gof] = model.fit(currentImg,x,y,'params',params);
else
args = reshape([fieldnames(extraParams)'; struct2cell(extraParams)'],1,[]);
[fitResult,gof] = model.fit(currentImg,x,y,'params',params,args{:});
end
[X,Y] = meshgrid(x,y);
xyData = [X(:),Y(:)];
fitData = reshape(fitResult(xyData), size(currentImg));
residuals = currentImg - fitData;
%% --- Store results ---
results(i).fitResult = fitResult;
results(i).gof = gof;
results(i).params = params;
results(i).fitData = fitData;
results(i).residuals = residuals;
results(i).rsquare = gof.rsquare;
results(i).status = 'Success';
%% --- Compute requested bottom-row fields only ---
for k = 1:numel(config.bottomRowFields)
fieldName = config.bottomRowFields{k};
switch fieldName
case 'atom_number'
if ismethod(model,'return_atom_number')
atomStruct = model.return_atom_number(X,Y,false);
results(i).atom_number = atomStruct.N_bec;
end
case 'condensate_fraction'
if isprop(model,'cond_frac')
results(i).condensate_fraction = model.cond_frac;
end
case 'temperature'
if ismethod(model,'return_temperature') && isfield(extraParams,'ToF')
results(i).temperature = model.return_temperature(extraParams.ToF,[],false);
end
end
end
%% --- Update plots dynamically ---
updatePlots(currentImg, fitData, residuals, i);
end
%% --- Display mean ± SEM after full batch loop (raw values) ---
for k = 1:numel(config.bottomRowFields)
fieldName = config.bottomRowFields{k};
% Collect raw values (no scaling)
vals = nan(numImages,1);
for i = 1:numImages
if isfield(results(i), fieldName)
vals(i) = results(i).(fieldName); % RAW, unscaled
end
end
meanVal = mean(vals,'omitnan');
semVal = std(vals,'omitnan')/sqrt(sum(~isnan(vals)));
% Place in bottom-right corner using normalized axes coordinates
ax = axBottom(k);
str = sprintf('%.2e ± %.2e', meanVal, semVal);
text(ax, 0.98, 0.02, str, ...
'Units','normalized', ...
'HorizontalAlignment','right', 'VerticalAlignment','bottom', ...
'FontName', config.fontName, ...
'FontSize', config.fontSize, ...
'FontWeight', config.fontWeight, ...
'BackgroundColor', 'w', ... % white box
'Margin', 4, ... % padding inside box
'EdgeColor', 'k'); % black border
end
fprintf('\n[INFO] Processing complete.\n');
%% --- Nested functions ---
function [ax,hImg] = createImageAxis(parentTile, titleStr)
ax = parentTile;
hImg = imagesc(ax, nan); axis(ax,'equal','tight');
colormap(ax, config.colormapName); colorbar(ax);
title(ax, titleStr, 'FontName', config.fontName, ...
'FontSize', config.fontSize, 'FontWeight', config.fontWeight);
end
function updatePlots(img, fitImg, residImg, idx)
% Update main image axes
set(hOriginal, 'CData', img);
set(hFit, 'CData', fitImg);
set(hResidual, 'CData', residImg);
% Update bottom-row plots
for k = 1:numel(config.bottomRowFields)
fieldName = config.bottomRowFields{k};
val = NaN;
if isfield(results(idx), fieldName)
val = results(idx).(fieldName) * config.bottomRowUnits(k);
end
if idx == 1
scatterBottom(k).XData = idx;
scatterBottom(k).YData = val;
else
scatterBottom(k).XData = [scatterBottom(k).XData, idx];
scatterBottom(k).YData = [scatterBottom(k).YData, val];
end
end
drawnow;
end
end

View File

@ -35,7 +35,6 @@ classdef DensityProfileBEC2DModel < handle
% Notes:
% - All static methods can be called independently without creating an object.
properties
% Conversion factors and default constants
fwhm_factor = 2*sqrt(2*log(2)); % FWHM factor for Gaussian
@ -137,7 +136,7 @@ classdef DensityProfileBEC2DModel < handle
center_pix = obj.calc_cen_pix(thresh);
center = obj.center_pix_conv(center_pix, x_1d, y_1d);
BEC_width_guess = obj.guess_BEC_width(thresh, center_pix);
if obj.is_debug
figure;
imagesc(x_1d, y_1d, thresh');
@ -153,11 +152,17 @@ classdef DensityProfileBEC2DModel < handle
% 1D slicing along the shorter axis
if BEC_width_guess(1) < BEC_width_guess(2)
if obj.is_debug
disp('x-axis is shorter, performing 1D fit along x-axis');
end
s_width_ind = 1;
x_fit = x_1d;
slice_range = round(center_pix(2) - BEC_width_guess(2)/2) : round(center_pix(2) + BEC_width_guess(2)/2);
X_guess = sum(data_2d(:, slice_range), 2) / length(slice_range);
else
if obj.is_debug
disp('y-axis is shorter, performing 1D fit along y-axis');
end
s_width_ind = 2;
x_fit = y_1d;
slice_range = round(center_pix(1) - BEC_width_guess(1)/2) : round(center_pix(1) + BEC_width_guess(1)/2);
@ -165,7 +170,7 @@ classdef DensityProfileBEC2DModel < handle
end
max_val = max(X_guess);
% Construct initial parameter struct
params_1d = struct();
params_1d.x0_bec.value = center(s_width_ind);
@ -184,20 +189,27 @@ classdef DensityProfileBEC2DModel < handle
params_1d.amp_th.min = 0;
params_1d.amp_th.max = 1.3 * max_val;
% params_1d.deltax.value = 3 * BEC_width_guess(s_width_ind);
% params_1d.deltax.min = 0;
% params_1d.deltax.max = max(x_width, y_width);
params_1d.sigma_bec.value = BEC_width_guess(s_width_ind) / 1.22;
params_1d.sigma_bec.min = 0;
params_1d.sigma_bec.max = 2 * BEC_width_guess(s_width_ind);
% params_1d.sigma_th.value = 3 * BEC_width_guess(1);
params_1d.sigma_th.value = 0.632 * params_1d.sigma_bec.value + 0.518 * 3 * BEC_width_guess(s_width_ind);
params_1d.sigma_th.min = 0;
params_1d.sigma_th.max = 3 * (0.632 * params_1d.sigma_bec.value + 0.518 * 3 * BEC_width_guess(s_width_ind));
% params_1d.sigma_th.expr = '0.632*sigma_bec + 0.518*deltax';
% Perform 1D bimodal fit
[fitResult_1d, gof_1d] = fit_1d_bimodal(x_fit, X_guess, params_1d);
% Extract fit coefficients
bval_1d = coeffvalues(fitResult_1d);
paramNames_1d = coeffnames(fitResult_1d);
for i = 1:length(paramNames_1d)
bval_1d_struct.(paramNames_1d{i}) = bval_1d(i);
end
@ -219,151 +231,373 @@ classdef DensityProfileBEC2DModel < handle
ylabel('Intensity');
end
% Final parameter preparation (pre_check logic omitted for brevity)
params = params_1d;
% Scale amplitudes
blurred_data = imgaussfilt(data_2d, 1);
amp_conv_1d_2d = max(blurred_data(:)) / (bval_1d_struct.amp_bec + bval_1d_struct.amp_th);
max_val = max(data_2d(:));
% Create parameter struct
params = struct();
% Pre-check: decide if image is pure BEC or pure thermal cloud based on 1D fit result
if bval_1d_struct.amp_th / bval_1d_struct.amp_bec > 7 && obj.pre_check
if obj.is_debug
disp('Image seems to be pure thermal cloud (based on 1D fit amplitudes)');
end
params.([obj.prefix 'amp_bec']).value = 0;
params.([obj.prefix 'amp_bec']).vary = false;
params.([obj.prefix 'amp_th']).value = amp_conv_1d_2d * bval_1d_struct.amp_th;
params.([obj.prefix 'amp_th']).max = 1.3 * max_val;
params.([obj.prefix 'amp_th']).vary = true;
params.([obj.prefix 'x0_bec']).value = 1;
params.([obj.prefix 'x0_bec']).vary = false;
params.([obj.prefix 'y0_bec']).value = 1;
params.([obj.prefix 'y0_bec']).vary = false;
params.([obj.prefix 'x0_th']).value = center(1);
params.([obj.prefix 'x0_th']).min = center(1) - 10;
params.([obj.prefix 'x0_th']).max = center(1) + 10;
params.([obj.prefix 'x0_th']).vary = true;
params.([obj.prefix 'y0_th']).value = center(2);
params.([obj.prefix 'y0_th']).min = center(2) - 10;
params.([obj.prefix 'y0_th']).max = center(2) + 10;
params.([obj.prefix 'y0_th']).vary = true;
params.([obj.prefix 'sigmax_bec']).value = 1;
params.([obj.prefix 'sigmax_bec']).vary = false;
params.([obj.prefix 'sigmay_bec']).value = 1;
params.([obj.prefix 'sigmay_bec']).vary = false;
params.([obj.prefix 'sigma_th']).value = bval_1d_struct.sigma_th;
params.([obj.prefix 'sigma_th']).min = 0;
params.([obj.prefix 'sigma_th']).max = max(x_width, y_width);
params.([obj.prefix 'sigma_th']).vary = true;
elseif bval_1d_struct.amp_bec / bval_1d_struct.amp_th > 10 && obj.pre_check
if obj.is_debug
disp('Image seems to be pure BEC (based on 1D fit amplitudes)');
end
params.([obj.prefix 'amp_bec']).value = amp_conv_1d_2d * bval_1d_struct.amp_bec;
params.([obj.prefix 'amp_bec']).max = 1.3 * max_val;
params.([obj.prefix 'amp_bec']).vary = true;
params.([obj.prefix 'amp_th']).value = 0;
params.([obj.prefix 'amp_th']).vary = false;
params.([obj.prefix 'x0_bec']).value = center(1);
params.([obj.prefix 'x0_bec']).min = center(1) - 10;
params.([obj.prefix 'x0_bec']).max = center(1) + 10;
params.([obj.prefix 'x0_bec']).vary = true;
params.([obj.prefix 'y0_bec']).value = center(2);
params.([obj.prefix 'y0_bec']).min = center(2) - 10;
params.([obj.prefix 'y0_bec']).max = center(2) + 10;
params.([obj.prefix 'y0_bec']).vary = true;
params.([obj.prefix 'x0_th']).value = 1;
params.([obj.prefix 'x0_th']).vary = false;
params.([obj.prefix 'y0_th']).value = 1;
params.([obj.prefix 'y0_th']).vary = false;
params.([obj.prefix 'sigma_th']).value = 1;
params.([obj.prefix 'sigma_th']).vary = false;
if s_width_ind == 1
params.([obj.prefix 'sigmax_bec']).value = bval_1d_struct.sigma_bec;
params.([obj.prefix 'sigmax_bec']).max = 2 * BEC_width_guess(1);
params.([obj.prefix 'sigmax_bec']).vary = true;
params.([obj.prefix 'sigmay_bec']).value = BEC_width_guess(2) / 1.22;
params.([obj.prefix 'sigmay_bec']).max = 2 * BEC_width_guess(2);
params.([obj.prefix 'sigmay_bec']).vary = true;
else
params.([obj.prefix 'sigmax_bec']).value = BEC_width_guess(1) / 1.22;
params.([obj.prefix 'sigmax_bec']).max = 2 * BEC_width_guess(1);
params.([obj.prefix 'sigmax_bec']).vary = true;
params.([obj.prefix 'sigmay_bec']).value = bval_1d_struct.sigma_bec;
params.([obj.prefix 'sigmay_bec']).max = 2 * BEC_width_guess(2);
params.([obj.prefix 'sigmay_bec']).vary = true;
end
else
% Normal bimodal fit parameters
params.([obj.prefix 'amp_bec']).value = amp_conv_1d_2d * bval_1d_struct.amp_bec;
params.([obj.prefix 'amp_bec']).min = 0;
params.([obj.prefix 'amp_bec']).max = 1.3 * max_val;
params.([obj.prefix 'amp_bec']).vary = true;
params.([obj.prefix 'amp_th']).value = amp_conv_1d_2d * bval_1d_struct.amp_th;
params.([obj.prefix 'amp_th']).min = 0;
params.([obj.prefix 'amp_th']).max = 1.3 * max_val;
params.([obj.prefix 'amp_th']).vary = true;
params.([obj.prefix 'x0_bec']).value = center(1);
params.([obj.prefix 'x0_bec']).min = center(1) - 10;
params.([obj.prefix 'x0_bec']).max = center(1) + 10;
params.([obj.prefix 'x0_bec']).vary = true;
params.([obj.prefix 'y0_bec']).value = center(2);
params.([obj.prefix 'y0_bec']).min = center(2) - 10;
params.([obj.prefix 'y0_bec']).max = center(2) + 10;
params.([obj.prefix 'y0_bec']).vary = true;
params.([obj.prefix 'x0_th']).value = center(1);
params.([obj.prefix 'x0_th']).min = center(1) - 10;
params.([obj.prefix 'x0_th']).max = center(1) + 10;
params.([obj.prefix 'x0_th']).vary = true;
params.([obj.prefix 'y0_th']).value = center(2);
params.([obj.prefix 'y0_th']).min = center(2) - 10;
params.([obj.prefix 'y0_th']).max = center(2) + 10;
params.([obj.prefix 'y0_th']).vary = true;
params.([obj.prefix 'sigma_th']).value = bval_1d_struct.sigma_th;
params.([obj.prefix 'sigma_th']).min = 0;
params.([obj.prefix 'sigma_th']).max = max(x_width, y_width);
params.([obj.prefix 'sigma_th']).vary = true;
if s_width_ind == 1
params.([obj.prefix 'sigmax_bec']).value = bval_1d_struct.sigma_bec;
params.([obj.prefix 'sigmax_bec']).min = 0;
params.([obj.prefix 'sigmax_bec']).max = 2 * BEC_width_guess(1);
params.([obj.prefix 'sigmax_bec']).vary = true;
params.([obj.prefix 'sigmay_bec']).value = BEC_width_guess(2) / 1.22;
params.([obj.prefix 'sigmay_bec']).min = 0;
params.([obj.prefix 'sigmay_bec']).max = 2 * BEC_width_guess(2);
params.([obj.prefix 'sigmay_bec']).vary = true;
else
params.([obj.prefix 'sigmax_bec']).value = BEC_width_guess(1) / 1.22;
params.([obj.prefix 'sigmax_bec']).min = 0;
params.([obj.prefix 'sigmax_bec']).max = 2 * BEC_width_guess(1);
params.([obj.prefix 'sigmax_bec']).vary = true;
params.([obj.prefix 'sigmay_bec']).value = bval_1d_struct.sigma_bec;
params.([obj.prefix 'sigmay_bec']).min = 0;
params.([obj.prefix 'sigmay_bec']).max = 2 * BEC_width_guess(2);
params.([obj.prefix 'sigmay_bec']).vary = true;
end
end
params.([obj.prefix 'rot_angle']).value = rot_angle;
params.([obj.prefix 'rot_angle']).min = rot_angle - 30;
params.([obj.prefix 'rot_angle']).max = rot_angle + 30;
params.([obj.prefix 'rot_angle']).vary = p.Results.vary_rot;
if obj.is_debug
disp('Initial parameters:');
disp(params);
end
obj.params = params;
end
function [fitResult, gof] = fit(obj, data, x, y, varargin)
% Perform 2D nonlinear least squares fit for BEC + thermal cloud.
% Optionally includes rotation of the cloud profile.
data = double(data);
figure
imagesc(data);
axis equal tight;
colorbar;
colormap('jet');
% Perform fitting
if isempty(obj.params)
obj.guess(data, x, y, varargin{:});
end
% Prepare grid and data
% Prepare fitting data
[X, Y] = meshgrid(x, y);
xyData = [X(:), Y(:)];
zData = double(data(:));
% Create fit options and fittype
% Create fit options
options = fitoptions('Method', 'NonlinearLeastSquares');
if obj.params.([obj.prefix 'rot_angle']).vary
% Include rotation parameter
% Define parameter order
paramOrder = {[obj.prefix 'amp_bec'], [obj.prefix 'amp_th'], ...
[obj.prefix 'x0_bec'], [obj.prefix 'y0_bec'], ...
[obj.prefix 'x0_th'], [obj.prefix 'y0_th'], ...
[obj.prefix 'sigmax_bec'], [obj.prefix 'sigmay_bec'], ...
[obj.prefix 'sigma_th'], [obj.prefix 'rot_angle']};
% Start point, lower and upper bounds
% Create StartPoint, Lower, and Upper vectors
startPoint = zeros(1, length(paramOrder));
lowerBounds = zeros(1, length(paramOrder));
upperBounds = zeros(1, length(paramOrder));
for i = 1:length(paramOrder)
paramName = paramOrder{i};
startPoint(i) = obj.params.(paramName).value;
lowerBounds(i) = obj.params.(paramName).min;
upperBounds(i) = obj.params.(paramName).max;
end
% Set fitting options
options.StartPoint = startPoint;
options.Lower = lowerBounds;
options.Upper = upperBounds;
% Define fit type
ft = fittype(@(amp_bec, amp_th, x0_bec, y0_bec, x0_th, y0_th, ...
sigmax_bec, sigmay_bec, sigma_th, rot_angle, x, y) ...
obj.density_profile_BEC_2d(x, y, amp_bec, amp_th, x0_bec, y0_bec, ...
x0_th, y0_th, sigmax_bec, sigmay_bec, sigma_th, rot_angle), ...
'independent', {'x', 'y'}, 'dependent', 'z');
else
% No rotation
% Define parameter order
paramOrder = {[obj.prefix 'amp_bec'], [obj.prefix 'amp_th'], ...
[obj.prefix 'x0_bec'], [obj.prefix 'y0_bec'], ...
[obj.prefix 'x0_th'], [obj.prefix 'y0_th'], ...
[obj.prefix 'sigmax_bec'], [obj.prefix 'sigmay_bec'], ...
[obj.prefix 'sigma_th']};
% Create StartPoint, Lower, and Upper vectors
startPoint = zeros(1, length(paramOrder));
lowerBounds = zeros(1, length(paramOrder));
upperBounds = zeros(1, length(paramOrder));
for i = 1:length(paramOrder)
paramName = paramOrder{i};
startPoint(i) = obj.params.(paramName).value;
lowerBounds(i) = obj.params.(paramName).min;
upperBounds(i) = obj.params.(paramName).max;
end
% Set fitting options
options.StartPoint = startPoint;
options.Lower = lowerBounds;
options.Upper = upperBounds;
% Define fit type
ft = fittype(@(amp_bec, amp_th, x0_bec, y0_bec, x0_th, y0_th, ...
sigmax_bec, sigmay_bec, sigma_th, x, y) ...
obj.density_profile_BEC_2d(x, y, amp_bec, amp_th, x0_bec, y0_bec, ...
x0_th, y0_th, sigmax_bec, sigmay_bec, sigma_th, 0), ...
'independent', {'x', 'y'}, 'dependent', 'z');
end
% Perform fitting
[obj.fitResult, obj.gof] = fit(xyData, zData, ft, options);
fitResult = obj.fitResult;
gof = obj.gof;
% Compute condensate fraction
% Post-processing check
if obj.post_check
bval = coeffvalues(obj.fitResult);
paramNames = coeffnames(obj.fitResult);
% Extract parameter values
for i = 1:length(paramNames)
eval([paramNames{i} ' = bval(i);']);
end
% Calculate number of atoms around the BEC
tf_fit = obj.ThomasFermi_2d(xyData(:,1), xyData(:,2), x0_bec, y0_bec, amp_bec, sigmax_bec, sigmay_bec);
tf_fit_2 = obj.ThomasFermi_2d(xyData(:,1), xyData(:,2), x0_bec, y0_bec, amp_bec, 1.5*sigmax_bec, 1.5*sigmay_bec);
mask = tf_fit > 0;
mask_2 = tf_fit_2 > 0;
N_c = sum(zData(mask & ~mask_2));
N_a = obj.atom_n_conv * N_c;
% If too few atoms are found around the BEC, refit (BEC only)
if N_a < 6615
if obj.is_debug
disp('No thermal cloud detected, performing BEC-only fit');
end
% Update parameters
obj.params.([obj.prefix 'amp_th']).value = 0;
obj.params.([obj.prefix 'amp_th']).vary = false;
obj.params.([obj.prefix 'x0_th']).value = 1;
obj.params.([obj.prefix 'x0_th']).vary = false;
obj.params.([obj.prefix 'y0_th']).value = 1;
obj.params.([obj.prefix 'y0_th']).vary = false;
obj.params.([obj.prefix 'sigma_th']).value = 1;
obj.params.([obj.prefix 'sigma_th']).vary = false;
% Refit
[obj.fitResult, obj.gof] = fit(xyData, zData, ft, options);
fitResult = obj.fitResult;
gof = obj.gof;
end
end
% Calculate condensate fraction
obj.cond_frac = obj.cal_cond_frac(X, Y);
end
function thresh = calc_thresh(obj, data, thresh_val, sigma)
% Binarize image for BEC detection
if nargin < 3, thresh_val = 0.3; end
if nargin < 4, sigma = 0.4; end
% Binarize image
if nargin < 3
thresh_val = 0.3;
end
if nargin < 4
sigma = 0.4;
end
blurred = imgaussfilt(data, sigma);
thresh = blurred < max(blurred(:)) * thresh_val;
thresh = double(~thresh);
thresh = double(~thresh); % Invert and convert to double precision
end
function center_pix = calc_cen_pix(obj, thresh)
% Compute center of binarized image
% Calculate the center of the binarized image
[X, Y] = size(thresh);
thresh = thresh / sum(thresh(:));
% Edge distributions
dx = sum(thresh, 2);
dy = sum(thresh, 1);
% Expectation values
center_pix = [sum(dx .* (1:X)'), sum(dy .* (1:Y))];
end
function center = center_pix_conv(obj, center_pix, x, y)
% Convert pixel indices to coordinate values
% Convert pixel center to coordinate center
center = [x(round(center_pix(1))), y(round(center_pix(2)))];
end
function BEC_width_guess = guess_BEC_width(obj, thresh, center)
% Estimate BEC width along x and y
% Guess BEC width
[X, Y] = size(thresh);
BEC_width_guess = [sum(thresh(:, round(center(2)))), sum(thresh(round(center(1)), :))];
BEC_width_guess(BEC_width_guess<=0) = 1;
for i = 1:2
if BEC_width_guess(i) <= 0
BEC_width_guess(i) = 1;
end
end
end
function cond_frac = cal_cond_frac(obj, X, Y)
% Compute condensate fraction from fitted BEC + thermal cloud profile
% Calculate condensate fraction
bval = coeffvalues(obj.fitResult);
paramNames = coeffnames(obj.fitResult);
% Extract fit parameters
% Extract parameter values
for i = 1:length(paramNames)
eval([paramNames{i} ' = bval(i);']);
end
if ~obj.params.([obj.prefix 'rot_angle']).vary
rot_angle = 0;
end
% Thomas-Fermi fit for condensate
tf_fit = obj.ThomasFermi_2d(X, Y, x0_bec, y0_bec, amp_bec, sigmax_bec, sigmay_bec);
% Total density profile (BEC + thermal cloud)
fit_total = obj.density_profile_BEC_2d(X, Y, amp_bec, amp_th, x0_bec, y0_bec, ...
x0_th, y0_th, sigmax_bec, sigmay_bec, sigma_th, rot_angle);
fit_total = obj.density_profile_BEC_2d(X, Y, amp_bec, amp_th, x0_bec, y0_bec, x0_th, y0_th, sigmax_bec, sigmay_bec, sigma_th, rot_angle);
N_bec = sum(tf_fit(:));
N_ges = sum(fit_total(:));
@ -371,7 +605,7 @@ classdef DensityProfileBEC2DModel < handle
end
function atom_n = return_atom_number(obj, X, Y, is_print)
% Compute total number of atoms, BEC atoms, thermal atoms, and condensate fraction
% Calculate atom number
if nargin < 4
is_print = true;
end
@ -379,7 +613,7 @@ classdef DensityProfileBEC2DModel < handle
bval = coeffvalues(obj.fitResult);
paramNames = coeffnames(obj.fitResult);
% Extract fit parameters
% Extract parameter values
for i = 1:length(paramNames)
eval([paramNames{i} ' = bval(i);']);
end
@ -404,7 +638,7 @@ classdef DensityProfileBEC2DModel < handle
end
function T = return_temperature(obj, tof, omg, is_print, eff_pix)
% Compute temperature from thermal cloud width and time-of-flight
% Calculate temperature
if nargin < 3
omg = [];
end
@ -418,42 +652,43 @@ classdef DensityProfileBEC2DModel < handle
bval = coeffvalues(obj.fitResult);
paramNames = coeffnames(obj.fitResult);
% Extract fit parameters
% Extract parameter values
for i = 1:length(paramNames)
eval([paramNames{i} ' = bval(i);']);
end
R_th = sigma_th * eff_pix * sqrt(2); % Thermal cloud radius
R_th = sigma_th * eff_pix * sqrt(2);
% Physical constants
u = 1.66053906660e-27; % Atomic mass unit [kg]
k = 1.380649e-23; % Boltzmann constant [J/K]
u = 1.66053906660e-27; % Atomic mass unit
k = 1.380649e-23; % Boltzmann constant
if isempty(omg)
% Free expansion
T = R_th^2 * 164 * u / k / tof^2;
else
% Trap expansion included
T = R_th^2 * 164 * u / k / (1/omg^2 + tof^2);
end
if is_print
fprintf('Temperature: %.2f nK\n', T * 1e9);
end
end
end
end
methods (Static)
function res = ThomasFermi_2d(x, y, centerx, centery, amplitude, sigmax, sigmay)
% 2D Thomas-Fermi distribution for a BEC (parabolic density profile)
res = (1 - ((x - centerx)/sigmax).^2 - ((y - centery)/sigmay).^2);
% Thomas-Fermi distribution function
% tiny = 1e-15;
res = (1 - ((x - centerx) / sigmax).^2 - ((y - centery) / sigmay).^2);
res(res < 0) = 0;
res = amplitude * res.^(3/2); % Standard TF 3/2 exponent
res = amplitude * res.^(3/2);
% res = amplitude * 5/(2*pi) / max(tiny, sigmax * sigmay) .* (res > 0) .* res;
end
function res = polylog(power, numerator)
% Approximate polylogarithm function using truncated series
order = 20; % Truncation order
% Polylogarithm function approximation
order = 20;
dataShape = size(numerator);
numerator = repmat(numerator(:), 1, order);
numerator = numerator .^ repmat(1:order, prod(dataShape), 1);
@ -466,31 +701,31 @@ classdef DensityProfileBEC2DModel < handle
end
function res = polylog2_2d(x, y, centerx, centery, amplitude, sigmax, sigmay)
% 2D thermal cloud distribution using polylog(2)
arg = exp(-((x - centerx).^2 / (2*sigmax^2)) - ((y - centery).^2 / (2*sigmay^2)));
res = amplitude / 1.643 * FitModels.DensityProfileBEC2DModel.polylog(2, arg);
% 2D polylogarithm function
% tiny = 1e-15;
arg = exp(-((x - centerx).^2 / (2 * sigmax^2)) - ((y - centery).^2 / (2 * sigmay^2)));
res = amplitude / (1.643) .* FitModels.DensityProfileBEC2DModel.polylog(2, arg);
end
function res = density_profile_BEC_2d(x, y, amp_bec, amp_th, x0_bec, y0_bec, ...
x0_th, y0_th, sigmax_bec, sigmay_bec, sigma_th, rot_angle)
% Combined 2D density profile: BEC (Thomas-Fermi) + thermal cloud (polylog)
function res = density_profile_BEC_2d(x, y, amp_bec, amp_th, x0_bec, y0_bec, x0_th, y0_th, sigmax_bec, sigmay_bec, sigma_th, rot_angle)
% BEC density profile function
if nargin < 12
rot_angle = 0;
end
% Rotate coordinates if needed
% Rotate coordinates (if needed)
if rot_angle ~= 0
rot_angle_rad = -rot_angle * pi/180; % Clockwise rotation
rot_angle_rad = -rot_angle * pi/180; % Negative sign means clockwise rotation
% Rotate coordinates
x_rot = x * cos(rot_angle_rad) + y * sin(rot_angle_rad);
y_rot = -x * sin(rot_angle_rad) + y * cos(rot_angle_rad);
% Rotate centers
% Rotate BEC center
x0_bec_rot = x0_bec * cos(rot_angle_rad) + y0_bec * sin(rot_angle_rad);
y0_bec_rot = -x0_bec * sin(rot_angle_rad) + y0_bec * cos(rot_angle_rad);
% Rotate thermal center
x0_th_rot = x0_th * cos(rot_angle_rad) + y0_th * sin(rot_angle_rad);
y0_th_rot = -x0_th * sin(rot_angle_rad) + y0_th * cos(rot_angle_rad);
@ -502,20 +737,20 @@ classdef DensityProfileBEC2DModel < handle
y0_th = y0_th_rot;
end
% Thomas-Fermi part (BEC)
% Calculate Thomas-Fermi part
TF_part = FitModels.DensityProfileBEC2DModel.ThomasFermi_2d(x, y, x0_bec, y0_bec, amp_bec, sigmax_bec, sigmay_bec);
% Polylog thermal part
% Calculate polylogarithm part
poly_part = FitModels.DensityProfileBEC2DModel.polylog2_2d(x, y, x0_th, y0_th, amp_th, sigma_th, sigma_th);
% Total density
% Total sum
res = TF_part + poly_part;
end
function res = density_1d(x, x0_bec, x0_th, amp_bec, amp_th, sigma_bec, sigma_th)
% 1D density profile (Thomas-Fermi + thermal polylog)
thermal_part = amp_th / 1.643 * polylog_int(exp(-0.5 * (x - x0_th).^2 / sigma_th^2));
TF_part = amp_bec * (1 - ((x - x0_bec)/sigma_bec).^2);
TF_part = amp_bec * (1 - ((x - x0_bec) / sigma_bec).^2);
TF_part(TF_part < 0) = 0;
TF_part = TF_part.^(3/2);
res = thermal_part + TF_part;
@ -524,8 +759,10 @@ classdef DensityProfileBEC2DModel < handle
end
% Helper function: polylogarithm interpolation
function res = polylog_int(x)
% Create interpolation table (simplified version)
x_int = linspace(0, 1.00001, 1000);
poly_tab = zeros(size(x_int));
@ -533,21 +770,32 @@ function res = polylog_int(x)
poly_tab(i) = sum((x_int(i).^(1:20)) ./ (1:20).^2);
end
% Linear interpolation
res = interp1(x_int, poly_tab, x, 'linear', 'extrap');
end
function [fitResult, gof] = fit_1d_bimodal(x, y, initialParams)
% 1D bimodal fitting function
% Input:
% x - independent variable data
% y - dependent variable data
% initialParams - structure of initial parameters
% Output:
% fitResult - fitting result
% gof - goodness-of-fit statistics
% Define 1D bimodal fitting function
bimodal1d = @(amp_bec, amp_th, x0_bec, x0_th, sigma_bec, sigma_th, x) ...
FitModels.DensityProfileBEC2DModel.density_1d(x, x0_bec, x0_th, amp_bec, amp_th, sigma_bec, sigma_th);
paramNames = {'amp_bec', 'amp_th', 'x0_bec', 'x0_th', 'sigma_bec', 'sigma_th'};
% Create fit type
ft = fittype(bimodal1d, 'independent', 'x', 'dependent', 'y');
% Set fit options
options = fitoptions(ft);
% paramNames = fieldnames(initialParams);
% Set initial parameters and bounds
startPoint = zeros(1, length(paramNames));
lowerBounds = zeros(1, length(paramNames));
upperBounds = zeros(1, length(paramNames));
@ -563,6 +811,6 @@ function [fitResult, gof] = fit_1d_bimodal(x, y, initialParams)
options.Lower = lowerBounds;
options.Upper = upperBounds;
% Perform fitting
[fitResult, gof] = fit(x(:), y(:), ft, options);
end

View File

@ -199,4 +199,5 @@ classdef TwoGaussian2DModel < handle
end
end
end
end

View File

@ -0,0 +1,117 @@
%% ===== BEC-Droplets Settings =====
% Specify data location to run analysis on
dataSources = {
struct('sequence', 'Evaporative_Cooling', ...
'date', '2025/08/13', ...
'runs', [30]) % specify run numbers as a string in "" or just as a numeric value
};
options = struct();
% File paths
options.baseDataFolder = '//DyLabNAS/Data';
options.FullODImagesFolder = 'E:/Data - Experiment/FullODImages/202508';
options.measurementName = 'BECToDroplets';
scriptFullPath = mfilename('fullpath');
options.saveDirectory = fileparts(scriptFullPath);
% Camera / imaging settings
options.cam = 3; % 1 - ODT_1_Axis_Camera; 2 - ODT_2_Axis_Camera; 3 - Horizontal_Axis_Camera;, 4 - Vertical_Axis_Camera;
options.angle = 0; % angle by which image will be rotated
options.center = [840, 972];
options.span = [100, 100];
options.fraction = [0.1, 0.1];
options.pixel_size = 5.86e-6; % in meters
options.magnification = 2.2218;
options.ImagingMode = 'LowIntensity';
options.PulseDuration = 25e-6; % in s
% Fourier analysis settings
options.theta_min = deg2rad(0);
options.theta_max = deg2rad(180);
options.N_radial_bins = 500;
options.Radial_Sigma = 2;
options.Radial_WindowSize = 5; % odd number
options.k_min = 1.2771; % μm¹
options.k_max = 2.5541; % μm¹
options.N_angular_bins = 180;
options.Angular_Threshold = 75;
options.Angular_Sigma = 2;
options.Angular_WindowSize = 5;
options.zoom_size = 50;
% Flags
options.skipUnshuffling = false;
options.skipNormalization = false;
options.skipFringeRemoval = true;
options.skipPreprocessing = true;
options.skipMasking = true;
options.skipIntensityThresholding = true;
options.skipBinarization = true;
options.skipFullODImagesFolderUse = true;
options.skipSaveData = false;
options.skipSaveFigures = true;
options.skipSaveProcessedOD = true;
options.skipLivePlot = true;
options.showProgressBar = true;
% Extras
options.font = 'Bahnschrift';
switch options.measurementName
case 'BECToDroplets'
options.scan_parameter = 'z_offset';
options.flipSortOrder = true;
options.scanParameterUnits = 'gauss';
options.titleString = 'BEC to Droplets';
case 'BECToStripes'
options.scan_parameter = 'rot_mag_field';
options.flipSortOrder = true;
options.scanParameterUnits = 'gauss';
options.titleString = 'BEC to Stripes';
case 'DropletsToStripes'
options.scan_parameter = 'ps_rot_mag_fin_pol_angle';
options.flipSortOrder = false;
options.scanParameterUnits = 'degrees';
options.titleString = 'Droplets to Stripes';
case 'StripesToDroplets'
options.scan_parameter = 'ps_rot_mag_fin_pol_angle';
options.flipSortOrder = false;
options.scanParameterUnits = 'degrees';
options.titleString = 'Stripes to Droplets';
end
%% ===== Collect Images and Launch Viewer =====
[options.selectedPath, options.folderPath] = Helper.selectDataSourcePath(dataSources, options);
[od_imgs, scan_parameter_values, scan_reference_values, file_list] = Helper.collectODImages(options);
%% Fit model and extract quantities
% --- Specify model and quantities to extract ---
model = FitModels.DensityProfileBEC2DModel();
quantities = {'atom_number','condensate_fraction','temperature'};
% --- Optional plotting configuration ---
plotConfig = struct();
plotConfig.fontName = 'Bahnschrift';
plotConfig.fontSize = 16;
plotConfig.fontWeight = 'bold';
plotConfig.colormapName = 'sky';
plotConfig.scatterLineSpec = '-o';
plotConfig.figureTag = 'BatchBECViewer';
plotConfig.bottomRowTitles = {'Condensed Atom Number','Condensate Fraction','Temperature'};
plotConfig.bottomRowLabels = {'# (\times 10^4)','# (%)','# (nK)'};
plotConfig.bottomRowUnits = [1e-4,1e2,1e9];
% --- Extra parameters ---
extraParams = struct();
extraParams.ToF = 20e-3;
% --- Run viewer ---
results = Analyzer.runFitProgressViewer(od_imgs, model, quantities, plotConfig, extraParams);