* Transitioned nappNorm, nappDetermineShift, and nappCopyShift to use modern MATLAB arguments blocks, removing their legacy functionSignatures.json entries.
* Refactored nappNorm to rely on prior peak fitting results (e.g., from nappFitGaussXps) for normalization rather than performing the fitting internally.
* Decoupled energy calibration: nappDetermineShift now solely calculates internal binding energy shifts, and nappCopyShift distributes them using a unified matching engine ('earlier', 'after', 'spectrumNumber').
* Implemented a Universal Logging Block to track data provenance, automatically appending execution timestamps and caller script names to processed rows.
* Updated nappCombineSpectra to output a logDataSource cell array and cleanly name combined spectra.
* Created nappSaveDataTable for automatic, script-named .mat file saving, complete with a Live Editor interceptor.
* Added a publish-mode silencer to nappFitGaussXps to prevent optimizer spam in generated PDFs.
* Updated readme.md to document the new arguments structure, the 3-step calibration workflow, the matching engine, and the new table variables.
249 lines
8.4 KiB
Matlab
249 lines
8.4 KiB
Matlab
function nappAddFitToPlot(DataTable, varargin)
|
|
% nappAddFitToPlot - Plot fitted peaks with proper decomposition
|
|
% FIXED: Correct pseudo-Voigt normalization and FWHM extraction
|
|
|
|
% Parse input arguments
|
|
p = inputParser;
|
|
addOptional(p, 'tableSelection', []);
|
|
addOptional(p, 'plotHandle', gca);
|
|
addOptional(p, 'peakWidthMode', 'FWHM');
|
|
addOptional(p, 'energyScale', 'kinetic');
|
|
parse(p, varargin{:});
|
|
|
|
% Get data from table
|
|
if isempty(p.Results.tableSelection)
|
|
fitData = DataTable(1, :);
|
|
else
|
|
fitData = DataTable(p.Results.tableSelection, :);
|
|
end
|
|
|
|
% Get energy range from current figure xlim
|
|
ax = p.Results.plotHandle;
|
|
axes(ax);
|
|
xlims = xlim(ax);
|
|
energyMin = xlims(1);
|
|
energyMax = xlims(2);
|
|
|
|
% Create energy range with 0.2 eV step
|
|
energyStep = 0.2;
|
|
energyRange = (energyMin:energyStep:energyMax)';
|
|
|
|
% =====================================================================
|
|
% COUNT ONLY NON-NaN PEAKS
|
|
% =====================================================================
|
|
peakArea_vals = fitData.peakArea; % Extract from cell
|
|
validPeaks = ~isnan(peakArea_vals); % Find non-NaN values
|
|
nPeaks = sum(validPeaks); % Count valid peaks
|
|
|
|
fprintf('Found %d valid peaks (non-NaN)\n', nPeaks);
|
|
|
|
if isempty(energyRange) || nPeaks == 0
|
|
warning('No valid fit data to plot');
|
|
return;
|
|
end
|
|
|
|
% Initialize output
|
|
peakCurves = zeros(length(energyRange), nPeaks);
|
|
totalFit = zeros(size(energyRange));
|
|
peakTypes = cell(nPeaks, 1);
|
|
|
|
% Extract peak parameters (cell arrays)
|
|
peakPos_vals = fitData.peakPosition;
|
|
peakFWHM_vals = fitData.peakFWHM;
|
|
|
|
% Check for peak eta parameter (pseudo-Voigt indicator)
|
|
hasPeakEta = ismember('peakEta', fitData.Properties.VariableNames) && ...
|
|
~all(isnan(fitData.peakEta));
|
|
|
|
if hasPeakEta
|
|
peakEta_vals = fitData.peakEta;
|
|
|
|
end
|
|
|
|
% =====================================================================
|
|
% MAIN LOOP: Process each VALID peak
|
|
% =====================================================================
|
|
validIdx = find(validPeaks); % Get indices of valid peaks
|
|
for i = 1:nPeaks
|
|
iPeak = validIdx(i); % Actual peak index in array
|
|
|
|
% Extract peak parameters
|
|
pos = peakPos_vals(iPeak);
|
|
A_total = peakArea_vals(iPeak);
|
|
FWHM = peakFWHM_vals(iPeak);
|
|
|
|
% Skip if FWHM is NaN or invalid
|
|
if isnan(FWHM) || FWHM <= 0
|
|
fprintf(' WARNING: Peak %d has invalid FWHM = %g, skipping\n', iPeak, FWHM);
|
|
continue;
|
|
end
|
|
|
|
% Check if this is a pseudo-Voigt peak
|
|
if hasPeakEta && ~isnan(peakEta_vals(iPeak))
|
|
% ============================================================
|
|
% PSEUDO-VOIGT: Gaussian + Lorentzian blend
|
|
% eta = Gaussian fraction (0 = pure Lorentzian, 1 = pure Gaussian)
|
|
% ============================================================
|
|
eta = peakEta_vals(iPeak);
|
|
|
|
% === GAUSSIAN COMPONENT (FWHM-based) ===
|
|
% G(E) = A_gauss * sqrt(4*ln(2)/pi) / FWHM * exp(-4*ln(2)*(E-E0)^2/FWHM^2)
|
|
% This is normalized so integral = A_gauss
|
|
A_gauss = eta * A_total; % Gaussian gets eta fraction of total area
|
|
gauss_norm = sqrt(4*log(2)/pi) / FWHM;
|
|
peakG = A_gauss .* gauss_norm .* ...
|
|
exp(-4*log(2) * ((energyRange - pos)./FWHM).^2);
|
|
|
|
% === LORENTZIAN COMPONENT (FWHM-based) ===
|
|
% L(E) = A_lorentz * (FWHM/2) / pi / ((E-E0)^2 + (FWHM/2)^2)
|
|
% This is normalized so integral = A_lorentz
|
|
A_lorentz = (1 - eta) * A_total; % Lorentzian gets (1-eta) fraction
|
|
gamma = FWHM / 2; % Half-width at half-maximum
|
|
peakL = A_lorentz .* (gamma / pi) ./ ...
|
|
((energyRange - pos).^2 + gamma.^2);
|
|
|
|
% === COMBINED PSEUDO-VOIGT ===
|
|
peak = peakG + peakL;
|
|
|
|
peakTypes{i} = sprintf('Pseudo-Voigt (η=%.3f)', eta);
|
|
|
|
else
|
|
% ============================================================
|
|
% PURE GAUSSIAN using FWHM
|
|
% G(E) = A * sqrt(4*ln(2)/pi) / FWHM * exp(-4*ln(2)*(E-E0)^2/FWHM^2)
|
|
% ============================================================
|
|
gauss_norm = sqrt(4*log(2)/pi) / FWHM;
|
|
peak = A_total .* gauss_norm .* ...
|
|
exp(-4*log(2) * ((energyRange - pos)./FWHM).^2);
|
|
peakTypes{i} = 'Gaussian';
|
|
end
|
|
|
|
peakCurves(:, i) = peak;
|
|
totalFit = totalFit + peak;
|
|
end
|
|
|
|
% =====================================================================
|
|
% ADD BACKGROUND
|
|
% =====================================================================
|
|
% Check for Shirley background parameters
|
|
hasShirleyK = ismember('peakShirleyK', fitData.Properties.VariableNames) && ...
|
|
~all(isnan(fitData.peakShirleyK));
|
|
|
|
|
|
if hasShirleyK
|
|
% SHIRLEY BACKGROUND RECONSTRUCTION
|
|
peakShirleyK_vals = fitData.peakShirleyK;
|
|
bkgrd = zeros(size(energyRange));
|
|
|
|
for i = 1:nPeaks
|
|
iPeak = validIdx(i);
|
|
|
|
% Extract parameters
|
|
area = peakArea_vals(iPeak);
|
|
FWHM = peakFWHM_vals(iPeak);
|
|
pos = peakPos_vals(iPeak);
|
|
if ismember('peakEta', fitData.Properties.VariableNames) && ~all(isnan(fitData.peakEta))
|
|
eta = peakEta_vals(iPeak);
|
|
else
|
|
eta = 1;
|
|
end
|
|
K = peakShirleyK_vals(iPeak);
|
|
|
|
% Calculate width parameters
|
|
w = FWHM / sqrt(log(4));
|
|
sigma = w / 2;
|
|
gamma = FWHM / 2;
|
|
|
|
% Cumulative Distribution Functions (CDFs) going from 0 to 1
|
|
% This represents the integral of the peak from -infinity to E
|
|
G_cdf = 0.5 * (1 + erf((energyRange - pos) / (sqrt(2) * sigma)));
|
|
L_cdf = 0.5 + (1/pi) * atan((energyRange - pos) / gamma);
|
|
|
|
% Combined Pseudo-Voigt CDF
|
|
integral_fraction = eta * G_cdf + (1 - eta) * L_cdf;
|
|
|
|
% Shirley Contribution
|
|
if matches(p.Results.energyScale, 'kinetic')
|
|
% For Kinetic Energy: background is higher at LOWER energies.
|
|
% Step goes from 1 (at E << pos) to 0 (at E >> pos)
|
|
shirleycontrib = K * area * (1 - integral_fraction);
|
|
else
|
|
% For Binding Energy: background is higher at HIGHER energies.
|
|
% Step goes from 0 (at E << pos) to 1 (at E >> pos)
|
|
shirleycontrib = K * area * integral_fraction;
|
|
end
|
|
|
|
bkgrd = bkgrd + shirleycontrib;
|
|
end
|
|
|
|
% Add constant offset C0
|
|
if ismember('peakBackground', fitData.Properties.VariableNames)
|
|
C0 = fitData.peakBackground(2); % Second element is C0
|
|
bkgrd = bkgrd + C0;
|
|
end
|
|
|
|
backgroundType = 'Shirley';
|
|
|
|
else
|
|
% LINEAR BACKGROUND (existing code)
|
|
if ismember('peakBackground', fitData.Properties.VariableNames)
|
|
bkgrd_vals = fitData.peakBackground;
|
|
if length(bkgrd_vals) >= 2
|
|
slope = bkgrd_vals(1);
|
|
intercept = bkgrd_vals(2);
|
|
else
|
|
slope = 0;
|
|
intercept = bkgrd_vals(1);
|
|
end
|
|
else
|
|
slope = 0;
|
|
intercept = 0;
|
|
end
|
|
|
|
bkgrd = slope .* energyRange + intercept;
|
|
backgroundType = sprintf('Linear (slope=%.4f, intercept=%.4f)', slope, intercept);
|
|
end
|
|
|
|
totalFit = totalFit + bkgrd;
|
|
|
|
|
|
% =====================================================================
|
|
% PLOT RESULTS
|
|
% =====================================================================
|
|
axes(ax);
|
|
hold on;
|
|
|
|
% Plot total fit (thick red line)
|
|
plot(energyRange, totalFit, '-', 'LineWidth', 2.5, 'Color', [0.8 0.2 0.2], 'DisplayName', 'Total Fit');
|
|
|
|
% Plot background (dotted gray line)
|
|
plot(energyRange, bkgrd, ':', 'LineWidth', 1.5, 'Color', [0.5 0.5 0.5], 'DisplayName', 'Background');
|
|
|
|
% Plot individual peaks (dashed colored lines)
|
|
colors = lines(nPeaks);
|
|
for i = 1:nPeaks
|
|
if any(peakCurves(:, i)) % Only plot if non-zero
|
|
plot(energyRange, peakCurves(:, i)+bkgrd, '--', 'LineWidth', 1.2, ...
|
|
'Color', colors(i,:), 'DisplayName', sprintf('Peak %d', i));
|
|
end
|
|
end
|
|
|
|
legend('show', 'Location', 'best');
|
|
hold off;
|
|
|
|
% % =====================================================================
|
|
% % DISPLAY INFORMATION
|
|
% % =====================================================================
|
|
% fprintf('\n=== FIT PLOTTED SUCCESSFULLY ===\n');
|
|
% fprintf('Valid peaks: %d\n', nPeaks);
|
|
% fprintf('Peak type(s): ');
|
|
% for i = 1:nPeaks
|
|
% fprintf('%s', peakTypes{i});
|
|
% if i < nPeaks, fprintf('; '); end
|
|
% end
|
|
% fprintf('\n');
|
|
% fprintf('Energy range: %.1f - %.1f eV\n', min(energyRange), max(energyRange));
|
|
% fprintf('=====================================\n\n');
|
|
% fprintf('Background: %s\n', backgroundType);
|
|
end
|