Files
2026-05-11 08:55:02 +02:00

371 lines
16 KiB
Matlab

function combinedSpectrum = nappCombineSpectra(DataTable,varargin)
%NAPPCOMBINESPECTRA Combine multiple PE or NEXAFS spectra through averaging or summation.
% This function combines selected photoelectron (PE) or X-ray absorption
% spectroscopy (NEXAFS) spectra into a single spectrum by averaging or
% summing. Data are automatically interpolated onto a common energy axis
% to handle slight variations in energy sampling between acquisitions.
%
% The output format is controlled by the 'output' parameter:
% - 'simple' (default): Returns a numeric matrix [energy, intensity, (std)]
% suitable for quick plotting and analysis. Requires custom code for
% plotting and further processing.
% - 'complete': Returns a MATLAB table (same format as input DataTable) with
% energy axis and combined spectrum plus metadata. Critically, this allows
% all existing nappXXX functions to operate directly on the combined result
% without conversion. Output table is named '<originalName>_combined.h5'
% where originalName is from the first selected row's name field. The
% mean of pass energy, sample temperature, ice temperature, pressure
% are added to the output.
%
% SYNTAX
% ------
% combinedSpectrum = NAPPCOMBINESPECTRA(DataTable)
%
% combinedSpectrum = NAPPCOMBINESPECTRA(DataTable, 'Name', Value)
%
% EXAMPLES
% --------
% % Average normalized C1s spectra with binding energy scale
% selection = contains(DataTable.name, 'C1s');
% avgSpectrum = nappCombineSpectra(DataTable, ...
% 'tableSelection', selection, ...
% 'tableVariable', 'spectrum_countsNorm', ...
% 'energyScale', 'binding', ...
% 'mode', 'average');
%
% % Sum multiple low-signal acquisitions to improve statistics
% sumSpectrum = nappCombineSpectra(DataTable, ...
% 'tableSelection', weakSignalRuns, ...
% 'tableVariable', 'spectrum_countsPerSecond', ...
% 'mode', 'sum');
%
% % Average NEXAFS spectra from multiple measurements
% nexafsAvg = nappCombineSpectra(DataTable, ...
% 'tableSelection', nexafsSelection, ...
% 'tableVariable', 'nexafsSpectrum', ...
% 'mode', 'average');
%
% % Combine using second spectrum's energy grid as reference
% combined = nappCombineSpectra(DataTable, ...
% 'tableSelection', mySelection, ...
% 'tableRow', 2, ...
% 'tableVariable', 'spectrum_counts');
%
% % Work with pre-normalized NEXAFS data
% preNorm = nappCombineSpectra(DataTable, ...
% 'tableVariable', 'nexafsSpectrumPreNorm', ...
% 'mode', 'average');
%
% INPUT ARGUMENTS
% ---------------
% DataTable table
% Data table containing spectroscopy spectra and corresponding energy
% scales. Must contain at least one spectrum variable and matching
% energy axis variable.
%
% NAME-VALUE PAIR ARGUMENTS (optional)
% ------------------------------------
% 'tableSelection' logical array (default: all rows)
% Logical index to filter which rows of DataTable to combine.
% Example: contains(DataTable.name, 'C1s') to select C1s spectra
% Example: DataTable.quality > 0.8 to select high-quality measurements
%
% 'tableVariable' char vector (default: 'spectrum_counts')
% Select which spectrum type to combine. Must be one of:
%
% PE spectra:
% - 'spectrum_counts'
% - 'spectrum_countsPerSecond'
% - 'spectrum_countsNorm'
%
% NEXAFS spectra:
% - 'nexafsSpectrum'
% - 'nexafsSpectrumPreBackground'
% - 'nexafsSpectrumPreNorm'
% - 'nexafsSpectrumPreGasPhase'
% - 'nexafsSpectrumPreI0'
% - 'nexafsSpectrumPreAdvCarb'
%
% 'tableRow' numeric scalar (default: 1)
% Index of the spectrum whose energy axis is used as reference for
% interpolation (index within the filtered table defined by
% 'tableSelection'). The combined spectrum's common energy grid
% is extracted from this row. Range: 1 to number of selected rows.
%
% 'mode' char vector (default: 'sum')
% Method for combining spectra:
% - 'sum': add all spectra together (increases signal)
% - 'average': calculate mean and standard deviation across spectra
% - 'subtract': subtract spectra (not yet implemented)
%
% 'energyScale' char vector (default: 'kinetic')
% Energy axis to use for PE spectra (ignored for NEXAFS):
% - 'kinetic': kinetic energy scale (eV) - uses scientaEkin_eV
% - 'binding': binding energy scale (eV) - uses bindingEnergy_eV
% - 'xRay': photon energy scale (eV) - uses xRayEkin_eV
%
% 'output' char vector (default: 'simple')
% Output format:
% - 'simple': return matrix with energy and combined intensity
% - 'complete': return table with full metadata and sorting info
%
% OUTPUT ARGUMENTS
% ----------------
% combinedSpectrum numeric matrix or table
% Combined spectroscopy data. Format depends on 'output' parameter:
%
% For 'simple' output (default):
% - Column 1: Energy values (eV) on common interpolated axis
% - Column 2: Combined intensity (sum or mean depending on mode)
% - Column 3: Standard deviation (only when mode = 'average')
%
% For 'complete' output:
% - Returns table with energy axis and spectrum(s) plus metadata columns
% (name, folder, creationDate, analysisDir, lastModifiedDatestr,
% lastModifiedDatenum) and is sorted by modification date
%
% NOTES
% -----
% - The function automatically determines the overlapping energy range across
% all selected spectra and interpolates to a common grid defined by the
% reference spectrum ('tableRow' parameter).
% - For binding energy scale, the overlap is computed using <= comparisons
% (reversed from kinetic energy behavior).
% - Missing or NaN values are excluded from calculations using 'omitnan' flag.
% - For 'average' mode, standard deviation quantifies variability between
% acquisitions and indicates measurement uncertainty.
% - Energy axes must be monotonic (strictly increasing or decreasing) for
% interpolation to work correctly.
% - All selected spectra must have compatible energy ranges with sufficient
% overlap (at least one common point).
%
% SEE ALSO
% --------
% interp1, nappPlot, nappBackground, mean, std, outerjoinRowToTable
expectedTableVariable = {'nexafsSpectrum','spectrum_counts','spectrum_countsPerSecond',...
'spectrum_countsNorm','nexafsSpectrumPreBackground','nexafsSpectrumPreNorm',...
'nexafsSpectrumPreGasPhase','nexafsSpectrumPreI0','nexafsSpectrumPreAdvCarb'};
defaultTableSelection = true(size(DataTable,1));
defaultTableRow = 1;
defaultMode = 'sum';
expectedMode = {'sum','average','subtract'};
defaultEnergyScale = 'kinetic';
expectedEnergyScale = {'kinetic','binding','xRay'};
defaultOutput = 'simple';
expectedOutput = {'simple','complete'};
% Parse input
p = inputParser;
addRequired(p,'DataTable',@istable);
addParameter(p,'tableSelection',defaultTableSelection,@islogical);
addParameter(p,'tableRow',defaultTableRow,@isnumeric);
addParameter(p,'tableVariable',...
@(x) any(validatestring(x,expectedTableVariable)));
addParameter(p,'mode',defaultMode,...
@(x) any(validatestring(x,expectedMode)));
addParameter(p,'energyScale',defaultEnergyScale,...
@(x) any(validatestring(x,expectedEnergyScale)));
addParameter(p,'output',defaultOutput,...
@(x) any(validatestring(x,expectedOutput)));
parse(p,DataTable,varargin{:});
% check input (all elements of input variable need to have non-zero size)
if ~all(cell2mat(cellfun(@size,p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable),'UniformOutput',false)),'all')
disp('The table variable appears to be empty. I quit now')
error('The table variable appears to be empty.')
end
if (contains(p.Results.tableVariable,'counts')) && (contains(p.Results.energyScale,'kinetic'))
tmpX = p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV;
elseif (contains(p.Results.tableVariable,'counts')) && (contains(p.Results.energyScale,'binding'))
tmpX = p.Results.DataTable(p.Results.tableSelection,:).bindingEnergy_eV;
elseif contains(p.Results.tableVariable,'counts') && (contains(p.Results.energyScale,'xRay'))
tmpX = p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV;
end
if contains(p.Results.energyScale,'binding')
tmpStart = min(cellfun(@(x) x(1), tmpX));
tmpEnd = max(cellfun(@(x) x(end), tmpX));
tmpStartIndex = find(tmpX{p.Results.tableRow} <= tmpStart,1);
tmpEndIndex = find(tmpX{p.Results.tableRow} <= tmpEnd,1);
else
tmpStart = max(cellfun(@(x) x(1), tmpX));
tmpEnd = min(cellfun(@(x) x(end), tmpX));
tmpStartIndex = find(tmpX{p.Results.tableRow} >= tmpStart,1);
tmpEndIndex = find(tmpX{p.Results.tableRow} >= tmpEnd,1);
end
% check if x has different scaling or size
% for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1)
% testX(:,ii) = tmpX{ii};
% end
%if all(all(testX == testX(1,:)))
% xq = tmpX{1};
% for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1)
% vq(:,ii) = p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii};
% end
%else
xq = tmpX{p.Results.tableRow}(tmpStartIndex:tmpEndIndex);
% repeat this for all columns in input.
for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1)
x = tmpX{ii};
v = p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii};
vq(:,ii) = interp1(x,v,xq);
end
% end
if contains(p.Results.output,'simple')
combinedSpectrum(:,1) = xq;
if contains(p.Results.mode,'sum')
combinedSpectrum(:,2) = sum(vq,2,'omitnan');
elseif contains(p.Results.mode,'average')
combinedSpectrum(:,2) = mean(vq,2,'omitnan');
combinedSpectrum(:,3) = std(vq,0,2,'omitnan');
elseif contains(p.Results.mode,'subtract')
disp('Not supported yet') % FIXME: subtraction is not implemented yet.
end
else
if contains(p.Results.energyScale,'binding')
TmpFileList.bindingEnergy_eV = xq;
elseif contains(p.Results.energyScale,'kinetic')
TmpFileList.scientaEkin_eV = xq;
else
TmpFileList.xRayEkin_eV = xq;
end
if contains(p.Results.mode,'sum')
TmpFileList.spectrum_counts = sum(vq,2,'omitnan');
elseif contains(p.Results.mode,'average') && ~contains(p.Results.energyScale,'xRay')
TmpFileList.(p.Results.tableVariable) = mean(vq,2,'omitnan');
elseif contains(p.Results.mode,'average') && contains(p.Results.energyScale,'xRay')
TmpFileList.(p.Results.tableVariable) = mean(vq,2,'omitnan');
elseif contains(p.Results.mode,'subtract')
disp('Not supported yet') % FIXME: subtraction is not implemented yet.
end
% copy essential information
if contains(p.Results.energyScale,'xRay')
TmpFileList.name = [p.Results.DataTable(p.Results.tableSelection,:).name{p.Results.tableRow} '_combined.h5'];
else
TmpFileList.name = [p.Results.DataTable(p.Results.tableSelection,:).name{p.Results.tableRow} '_combined.ibw'];
end
TmpFileList.folder = p.Results.DataTable(p.Results.tableSelection,:).folder(1);
TmpFileList.creationDate = p.Results.DataTable(p.Results.tableSelection,:).creationDate(1,:);
TmpFileList.analysisDir = p.Results.DataTable(p.Results.tableSelection,:).analysisDir(1);
TmpFileList.date = datestr(now);
TmpFileList.datenum = now;
% add processed spectra
% Ensure all columns exist with appropriate sizing
if ~ismember('logCombineDataOriginStr', p.Results.DataTable.Properties.VariableNames)
DataTable.logCombineDataOriginStr = cellstr(repmat(" ",size(p.Results.DataTable,1),1));
end
if ~ismember('logCombineDataOrigin', p.Results.DataTable.Properties.VariableNames)
DataTable.logDataSource = cell(size(p.Results.DataTable, 1), 1);
end
tmpRow = p.Results.DataTable(p.Results.tableSelection,:);
% Store an array of the processed spectrum names wrapped in a cell
% so you can easily loop through it (e.g., for name = DataTable.logDataSource{i})
TmpFileList.logCombineDataOrigin = {string(tmpRow.name')};
allNames = "";
for ii = 1:size(tmpRow,1)
thisDateStr = datestr(tmpRow.lastModifiedDatenum(ii),'yyyymmdd_HHMM');
thisName = tmpRow.name{ii};
token = thisDateStr + "_" + thisName(1:4);
if ii == 1
allNames = token;
else
allNames = allNames + " | " + token;
end
end
TmpFileList.logCombineDataOriginStr = allNames;
if ismember('scientaPassEnergy_eV', p.Results.DataTable.Properties.VariableNames)
TmpFileList.scientaPassEnergy_eV = mean(p.Results.DataTable(p.Results.tableSelection,:).scientaPassEnergy_eV,1,'omitnan');
end
if ismember('sampleTemp_dC', p.Results.DataTable.Properties.VariableNames)
TmpFileList.sampleTemp_dC = mean(p.Results.DataTable(p.Results.tableSelection,:).sampleTemp_dC,1,'omitnan');
end
if ismember('iceTemp_dC', p.Results.DataTable.Properties.VariableNames)
TmpFileList.iceTemp_dC = mean(p.Results.DataTable(p.Results.tableSelection,:).iceTemp_dC,1,'omitnan');
end
if ismember('cellPressure_mbar', p.Results.DataTable.Properties.VariableNames)
TmpFileList.cellPressure_mbar = mean(p.Results.DataTable(p.Results.tableSelection,:).cellPressure_mbar,1,'omitnan');
end
if ismember('smplX_mm', p.Results.DataTable.Properties.VariableNames)
TmpFileList.smplX_mm = mean(p.Results.DataTable(p.Results.tableSelection,:).smplX_mm,1,'omitnan');
end
if ismember('smplY_mm', p.Results.DataTable.Properties.VariableNames)
TmpFileList.smplY_mm = mean(p.Results.DataTable(p.Results.tableSelection,:).smplY_mm,1,'omitnan');
end
if ismember('smplZ_mm', p.Results.DataTable.Properties.VariableNames)
TmpFileList.smplZ_mm = mean(p.Results.DataTable(p.Results.tableSelection,:).smplZ_mm,1,'omitnan');
end
if ismember('sealingTemp', p.Results.DataTable.Properties.VariableNames)
TmpFileList.sealingTemp = mean(p.Results.DataTable(p.Results.tableSelection,:).sealingTemp,1,'omitnan');
end
%% Sort by date
% rename date fields and delete those fields not needed
[TmpFileList.lastModifiedDatestr] = TmpFileList.date;
[TmpFileList.lastModifiedDatenum] = TmpFileList.datenum;
% -------------------------------------------------------------------------
% ADAPTED UNIVERSAL LOGGING BLOCK (For newly created rows)
% -------------------------------------------------------------------------
thisFuncName = mfilename;
colDate = sprintf('log%sDate', thisFuncName);
colCaller = sprintf('log%sCaller', thisFuncName);
% Get current time as a string
currentTimeStr = string(datetime('now', 'Format', 'yyyy-MM-dd HH:mm:ss'));
% Determine the calling script using the call stack
stackInfo = dbstack;
if length(stackInfo) > 1
callerScript = string(stackInfo(2).file); % The script that called this function
else
callerScript = " "; % Called directly from the command window
end
% Add the log data directly to the new struct
TmpFileList.(colDate) = currentTimeStr;
TmpFileList.(colCaller) = callerScript;
% -------------------------------------------------------------------------
% First, we need to convert into a table, because structures can not be sorted:
TmpFileList = struct2table(TmpFileList,'AsArray',true);
% Then, we sort the table based on the time (6th column, datenum)
TmpFileList = sortrows(TmpFileList,'datenum');
% Then, remove un-needed columns
TmpFileList = removevars(TmpFileList,{'date','datenum'});
% merge tables (append imported data to previous table)
if exist("DataTable",'var')
TmpFileList = outerjoinRowToTable(TmpFileList,DataTable);
end
% Then, we sort the table based on the time (datenum)
combinedSpectrum = sortrows(TmpFileList,'lastModifiedDatenum');
end
end