371 lines
16 KiB
Matlab
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
|
|
|
|
|