diff --git a/beamtime/nappNorm.m b/beamtime/nappNorm.m index 5d400ec..c263e7e 100644 --- a/beamtime/nappNorm.m +++ b/beamtime/nappNorm.m @@ -1,39 +1,46 @@ function DataTable = nappNorm(DataTable, processSelection, refSelection, options) % NAPPNORM Normalize XPS spectra based on provided reference selection or self-maximum. % -% DataTable = NAPPNORM(DataTable, processSelection, refSelection) normalizes -% the spectra where processSelection is true, dividing them by the peak area -% of matched spectra from the pool where refSelection is true. Both must -% be logical arrays matching the height of the DataTable. +% DataTable = NAPPNORM(DataTable, processSelection, refSelection) normalizes +% the spectra where processSelection is true, dividing them by the peak area +% of matched spectra from the pool where refSelection is true. Both must +% be logical arrays matching the height of the DataTable. % -% DataTable = NAPPNORM(DataTable, processSelection) scales each spectrum -% selected by the logical array by its own maximum value. +% DataTable = NAPPNORM(DataTable, processSelection) scales each spectrum +% selected by the logical array by its own maximum value. % -% Name-Value arguments: -% matchStrategy - 'earlier', 'after', 'spectrumNumber', 'internalMax' -% tableVariable - Target column ('spectrum_countsPerSecond' by default) -% timeVariable - Target time column ('lastModifiedDatenum' by default) -% peakNumber - Which peak area to use if multiple exist (1 by default) +% Name-Value arguments: +% matchStrategy - 'earlier', 'after', 'spectrumNumber', 'internal' +% tableVariable - Target column ('spectrum_countsPerSecond' by default) +% timeVariable - Target time column ('lastModifiedDatenum' by default) +% peakNumber - Which peak area to use if multiple exist (1 by default) +% mode - "max" per default, "area" as option +% range- energy range where max is found. first mist be first value, second +% second (low to high for kinetic, high to low for binding) arguments DataTable table processSelection logical {mustBeVector} - % refSelection is optional. If omitted, it defaults to empty. refSelection logical {mustBeVector} = false(0,1); - % Name-Value arguments options.matchStrategy char {mustBeMember(options.matchStrategy, ... - {'earlier', 'after', 'spectrumNumber', 'internalMax'})} = 'earlier' + {'earlier', 'after', 'spectrumNumber', 'internal'})} = 'earlier' options.tableVariable char {mustBeMember(options.tableVariable, ... {'spectrum_counts', 'spectrum_countsPerSecond', 'spectrum_countsNorm'})} = 'spectrum_countsPerSecond' options.timeVariable char {mustBeMember(options.timeVariable, ... {'lastModifiedDatenum', 'creationDate'})} = 'lastModifiedDatenum' options.peakNumber (1,1) double {mustBeInteger, mustBePositive} = 1 + options.mode char {mustBeMember(options.mode, ... + {'max', 'area'})} = 'max' + options.range (1,:) double {mustBePositive} =[ ]; + options.energyScale char {mustBeMember(options.energyScale, ... + {'kinetic', 'binding'})} = 'kinetic' end -numRowsTable = height(DataTable); + +numRowsTable = height(DataTable); % Validate length of the logical arrays if length(processSelection) ~= numRowsTable error('processSelection must have the exact same length as the DataTable height (%d).', numRowsTable); @@ -41,14 +48,14 @@ end if ~isempty(refSelection) && any(refSelection) && length(refSelection) ~= numRowsTable error('refSelection must have the exact same length as the DataTable height (%d).', numRowsTable); end - -% Auto-detect mode: If no valid reference array is passed OR 'internalMax' is chosen -if strcmpi(options.matchStrategy, 'internalMax') || isempty(refSelection) || ~any(refSelection) - mode = 'maximum'; +% Auto-detect mode: If no valid reference array is passed OR 'internal' is chosen +if strcmpi(options.matchStrategy, 'internal') && ( isempty(refSelection) || ~any(refSelection) ) && strcmpi(options.mode, 'max') + modus = 'internalMax'; +elseif strcmpi(options.mode, 'max') + modus = 'externalMax'; else - mode = 'referenceArea'; + modus = 'referenceArea'; end - % Initialize new columns if they don't exist if ~ismember('spectrum_countsNorm', DataTable.Properties.VariableNames) DataTable.spectrum_countsNorm = cell(numRowsTable, 1); @@ -59,11 +66,9 @@ end if ~ismember('lognappNormWith', DataTable.Properties.VariableNames) DataTable.lognappNormWith = cell(numRowsTable, 1); % New column for spectrum name end - %% Processing Loop processIdx = find(processSelection); refIdx = find(refSelection); - for i = 1:length(processIdx) currRow = processIdx(i); yData = DataTable.(options.tableVariable){currRow}; @@ -71,24 +76,17 @@ for i = 1:length(processIdx) refTimes = datenum(DataTable.(options.timeVariable)(refIdx, :)); currName = DataTable.name{currRow}; bestRefRow = NaN; - if isempty(yData) continue; end - - switch mode - case 'maximum' + switch modus + case 'internalMax' normFactor = max(yData); if normFactor == 0, normFactor = 1; end % Protect against /0 - DataTable.spectrum_countsNorm{currRow} = yData ./ normFactor; - DataTable.logNormMode{currRow} = 'maximum'; + DataTable.logNormMode{currRow} = 'maximum'; DataTable.logNormWith{currRow} = num2str(normFactor); % Log internal maximum - case 'referenceArea' - - - % Internal Matching Engine using Code Words switch options.matchStrategy case 'earlier' @@ -98,7 +96,6 @@ for i = 1:length(processIdx) [~, minIdx] = min(timeDiffs); bestRefRow = validRefs(minIdx); end - case 'after' validRefs = refIdx(refTimes >= currTime); if ~isempty(validRefs) @@ -106,11 +103,9 @@ for i = 1:length(processIdx) [~, minIdx] = min(timeDiffs); bestRefRow = validRefs(minIdx); end - case 'spectrumNumber' % Extract the 4-digit number at the start of the name (e.g., '0033') specNumStr = regexp(currName, '^\d{4}', 'match', 'once'); - if ~isempty(specNumStr) % Find references that start with 'XXXX_' searchPattern = [specNumStr, '_']; @@ -122,64 +117,251 @@ for i = 1:length(processIdx) warning('Row %d: Could not extract a 4-digit spectrum number from name "%s".', currRow, currName); end - - end + % Validate we found a match + if isnan(bestRefRow) + warning('Row %d (%s): No matching reference found for strategy "%s". Skipping.', ... + currRow, currName, options.matchStrategy); + continue; + end + % Extract area and validate + refAreas = DataTable.peakArea(bestRefRow,:); + if size(refAreas,2) < options.peakNumber + warning('Row %d: peakNumber %d exceeds available peaks in reference row %d. Skipping.', ... + currRow, options.peakNumber, bestRefRow); + continue; + end + normFactor = refAreas(options.peakNumber); + if normFactor == 0 + warning('Row %d: Reference area in row %d is zero. Skipping.', currRow, bestRefRow); + continue; + end + % Apply normalization and log the exact name of the reference + DataTable.spectrum_countsNorm{currRow} = yData ./ normFactor; + DataTable.lognappNormMode{currRow} = sprintf('referenceArea (Row %d)', bestRefRow); + DataTable.lognappNormWith{currRow} = DataTable.name{bestRefRow}; + % ------------------------------------------------------------------------- + % 1. Dynamically get this function's name + thisFuncName = mfilename; + colDate = sprintf('log%sDate', thisFuncName); + colCaller = sprintf('log%sCaller', thisFuncName); + % 2. Probe for column existence, create empty string arrays if missing + if ~ismember(colDate, DataTable.Properties.VariableNames) + DataTable.(colDate) = repmat("", numRowsTable, 1); + end + if ~ismember(colCaller, DataTable.Properties.VariableNames) + DataTable.(colCaller) = repmat("", numRowsTable, 1); + end + % 3. Get current time as a string + currentTimeStr = string(datetime('now', 'Format', 'yyyy-MM-dd HH:mm:ss')); + % 4. 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 + % 5. Write the log data only to the rows being processed + DataTable.(colDate)(processIdx) = currentTimeStr; + DataTable.(colCaller)(processIdx) = callerScript; - % Validate we found a match - if isnan(bestRefRow) - warning('Row %d (%s): No matching reference found for strategy "%s". Skipping.', ... - currRow, currName, options.matchStrategy); - continue; - end + end % ------------------------------------------------------------------------- + case 'externalMax' + % Internal Matching Engine using Code Words - % Extract area and validate - refAreas = DataTable.peakArea(bestRefRow,:); - if size(refAreas,2) < options.peakNumber - warning('Row %d: peakNumber %d exceeds available peaks in reference row %d. Skipping.', ... - currRow, options.peakNumber, bestRefRow); - continue; - end + switch options.matchStrategy + case 'earlier' + validRefs = refIdx(refTimes <= currTime); + if ~isempty(validRefs) + timeDiffs = currTime - refTimes(refTimes <= currTime); + [~, minIdx] = min(timeDiffs); + bestRefRow = validRefs(minIdx); - normFactor = refAreas(options.peakNumber); - if normFactor == 0 - warning('Row %d: Reference area in row %d is zero. Skipping.', currRow, bestRefRow); - continue; - end + if ~isempty(options.range) + if strcmpi(options.energyScale, 'kinetic') + % Extract the X and Y data for cleaner code + xData = DataTable.scientaEkin_eV{bestRefRow}; + yDataRef = DataTable.(options.tableVariable){bestRefRow}; - % Apply normalization and log the exact name of the reference - DataTable.spectrum_countsNorm{currRow} = yData ./ normFactor; - DataTable.lognappNormMode{currRow} = sprintf('referenceArea (Row %d)', bestRefRow); - DataTable.lognappNormWith{currRow} = DataTable.name{bestRefRow}; - % ------------------------------------------------------------------------- - % 1. Dynamically get this function's name - thisFuncName = mfilename; - colDate = sprintf('log%sDate', thisFuncName); - colCaller = sprintf('log%sCaller', thisFuncName); - - - % 2. Probe for column existence, create empty string arrays if missing - if ~ismember(colDate, DataTable.Properties.VariableNames) - DataTable.(colDate) = repmat("", numRowsTable, 1); + % Find all points within the range (safely handles ascending/descending) + minR = min(options.range); + maxR = max(options.range); + idxRange = (xData >= minR) & (xData <= maxR); + + % Extract the subset and find the max + searchY = yDataRef(idxRange); + [normFactor, relativeMaxIdx] = max(searchY); + + % Convert the relative index back to the absolute index to get the true energy + validIndices = find(idxRange); + absoluteMaxIndex = validIndices(relativeMaxIdx); + + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, xData(absoluteMaxIndex),normFactor); + else + % Extract the X and Y data for cleaner code + xData = DataTable.bindingEnergy_eV{bestRefRow}; + yDataRef = DataTable.(options.tableVariable){bestRefRow}; + + % Find all points within the range (safely handles ascending/descending) + minR = min(options.range); + maxR = max(options.range); + idxRange = (xData >= minR) & (xData <= maxR); + + % Extract the subset and find the max + searchY = yDataRef(idxRange); + [normFactor, relativeMaxIdx] = max(searchY); + + % Convert the relative index back to the absolute index to get the true energy + validIndices = find(idxRange); + absoluteMaxIndex = validIndices(relativeMaxIdx); + + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, xData(absoluteMaxIndex),normFactor); + + end + + else + [normFactor, MaxIdx] = max(DataTable.(options.tableVariable){bestRefRow}); + if strcmpi(options.energyScale, 'kinetic') + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, DataTable.scientaEkin_eV{bestRefRow}(MaxIdx),normFactor ); + else + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, DataTable.bindingEnergy_eV{bestRefRow}(MaxIdx),normFactor ); + end + end + + if normFactor == 0, normFactor = 1; end % Protect against /0 + DataTable.spectrum_countsNorm{currRow} = yData ./ normFactor; + DataTable.logNormMode{currRow} = 'external maximum'; + DataTable.logNormWith{currRow} = num2str(normFactor); % Log internal maximum + end + case 'after' + validRefs = refIdx(refTimes >= currTime); + if ~isempty(validRefs) + timeDiffs = refTimes(refTimes >= currTime) - currTime; + [~, minIdx] = min(timeDiffs); + bestRefRow = validRefs(minIdx); + if ~isempty(options.range) + if strcmpi(options.energyScale, 'kinetic') + % Extract the X and Y data for cleaner code + xData = DataTable.scientaEkin_eV{bestRefRow}; + yDataRef = DataTable.(options.tableVariable){bestRefRow}; + + % Find all points within the range (safely handles ascending/descending) + minR = min(options.range); + maxR = max(options.range); + idxRange = (xData >= minR) & (xData <= maxR); + + % Extract the subset and find the max + searchY = yDataRef(idxRange); + [normFactor, relativeMaxIdx] = max(searchY); + + % Convert the relative index back to the absolute index to get the true energy + validIndices = find(idxRange); + absoluteMaxIndex = validIndices(relativeMaxIdx); + + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, xData(absoluteMaxIndex),normFactor); + + else + % Extract the X and Y data for cleaner code + xData = DataTable.bindingEnergy_eV{bestRefRow}; + yDataRef = DataTable.(options.tableVariable){bestRefRow}; + + % Find all points within the range (safely handles ascending/descending) + minR = min(options.range); + maxR = max(options.range); + idxRange = (xData >= minR) & (xData <= maxR); + + % Extract the subset and find the max + searchY = yDataRef(idxRange); + [normFactor, relativeMaxIdx] = max(searchY); + + % Convert the relative index back to the absolute index to get the true energy + validIndices = find(idxRange); + absoluteMaxIndex = validIndices(relativeMaxIdx); + + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, xData(absoluteMaxIndex),normFactor); + + end + + else + [normFactor, MaxIdx] = max(DataTable.(options.tableVariable){bestRefRow}); + if strcmpi(options.energyScale, 'kinetic') + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, DataTable.scientaEkin_eV{bestRefRow}(MaxIdx),normFactor ); + else + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, DataTable.bindingEnergy_eV{bestRefRow}(MaxIdx),normFactor ); + end + end + if normFactor == 0, normFactor = 1; end % Protect against /0 + DataTable.spectrum_countsNorm{currRow} = yData ./ normFactor; + DataTable.logNormMode{currRow} = 'external maximum'; + DataTable.logNormWith{currRow} = num2str(normFactor); % Log internal maximum + end + case 'spectrumNumber' + % Extract the 4-digit number at the start of the name (e.g., '0033') + specNumStr = regexp(currName, '^\d{4}', 'match', 'once'); + if ~isempty(specNumStr) + % Find references that start with 'XXXX_' + searchPattern = [specNumStr, '_']; + validRefs = refIdx(startsWith(DataTable.name(refIdx), searchPattern)); + if ~isempty(validRefs) + bestRefRow = validRefs(1); % Take the first exact match + end + else + warning('Row %d: Could not extract a 4-digit spectrum number from name "%s".', currRow, currName); + end + if ~isempty(options.range) + if strcmpi(options.energyScale, 'kinetic') + % Extract the X and Y data for cleaner code + xData = DataTable.scientaEkin_eV{bestRefRow}; + yDataRef = DataTable.(options.tableVariable){bestRefRow}; + + % Find all points within the range (safely handles ascending/descending) + minR = min(options.range); + maxR = max(options.range); + idxRange = (xData >= minR) & (xData <= maxR); + + % Extract the subset and find the max + searchY = yDataRef(idxRange); + [normFactor, relativeMaxIdx] = max(searchY); + + % Convert the relative index back to the absolute index to get the true energy + validIndices = find(idxRange); + absoluteMaxIndex = validIndices(relativeMaxIdx); + + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, xData(absoluteMaxIndex),normFactor); + + else + % Extract the X and Y data for cleaner code + xData = DataTable.bindingEnergy_eV{bestRefRow}; + yDataRef = DataTable.(options.tableVariable){bestRefRow}; + + % Find all points within the range (safely handles ascending/descending) + minR = min(options.range); + maxR = max(options.range); + idxRange = (xData >= minR) & (xData <= maxR); + + % Extract the subset and find the max + searchY = yDataRef(idxRange); + [normFactor, relativeMaxIdx] = max(searchY); + + % Convert the relative index back to the absolute index to get the true energy + validIndices = find(idxRange); + absoluteMaxIndex = validIndices(relativeMaxIdx); + + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, xData(absoluteMaxIndex),normFactor); + + end + + else + [normFactor, MaxIdx] = max(DataTable.(options.tableVariable){bestRefRow}); + if strcmpi(options.energyScale, 'kinetic') + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, DataTable.scientaEkin_eV{bestRefRow}(MaxIdx),normFactor ); + else + fprintf('Norming %s with %s at %.1f eV: %.1e\n', DataTable.name{currRow}, DataTable.name{bestRefRow}, DataTable.bindingEnergy_eV{bestRefRow}(MaxIdx),normFactor ); + end + end + if normFactor == 0, normFactor = 1; end % Protect against /0 + DataTable.spectrum_countsNorm{currRow} = yData ./ normFactor; + DataTable.logNormMode{currRow} = 'external maximum'; + DataTable.logNormWith{currRow} = num2str(normFactor); % Log internal maximum + end end - if ~ismember(colCaller, DataTable.Properties.VariableNames) - DataTable.(colCaller) = repmat("", numRowsTable, 1); - end - - % 3. Get current time as a string - currentTimeStr = string(datetime('now', 'Format', 'yyyy-MM-dd HH:mm:ss')); - - % 4. 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 - - % 5. Write the log data only to the rows being processed - DataTable.(colDate)(processIdx) = currentTimeStr; - DataTable.(colCaller)(processIdx) = callerScript; - % ------------------------------------------------------------------------- - end -end end \ No newline at end of file