From 43ac9c00bbfee25d1ddbf047a7bcd8d2bd1f6982 Mon Sep 17 00:00:00 2001 From: ThorBarRa Date: Sat, 9 May 2026 07:31:44 +0200 Subject: [PATCH] small bug fixes. CopyShift now works with any filename, not just numbers as before --- beamtime/nappCopyShift.m | 213 ++++++++++++++++--------------- beamtime/nappDetermineShiftXps.m | 25 ++-- 2 files changed, 126 insertions(+), 112 deletions(-) diff --git a/beamtime/nappCopyShift.m b/beamtime/nappCopyShift.m index 2ce64dd..af6b95d 100644 --- a/beamtime/nappCopyShift.m +++ b/beamtime/nappCopyShift.m @@ -1,118 +1,123 @@ function DataTable = nappCopyShift(DataTable, processSelection, refSelection, options) % NAPPCOPYSHIFT Copy binding energy shift from reference spectra to target spectra. % -% DataTable = NAPPCOPYSHIFT(DataTable, processSelection, refSelection) -% copies the calculated binding energy shift from a pool of reference spectra -% (where refSelection is true) to the target spectra (where processSelection +% DataTable = NAPPCOPYSHIFT(DataTable, processSelection, refSelection) +% copies the calculated binding energy shift from a pool of reference spectra +% (where refSelection is true) to the target spectra (where processSelection % is true) using a matching strategy. % -% NOTE: This function requires that nappDetermineShift has already been +% NOTE: This function requires that nappDetermineShift has already been % run on the reference spectra to populate the 'bindingEnergyShift' column. % % Name-Value arguments: % matchStrategy - 'earlier' (default), 'after', 'spectrumNumber' % timeVariable - Target time column ('lastModifiedDatenum' by default) +% spectrumNumberPrefixLength - Number of leading characters used for name +% matching in 'spectrumNumber' strategy (default 7) - arguments - DataTable table - processSelection logical {mustBeVector} - refSelection logical {mustBeVector} - - % Name-Value arguments - options.matchStrategy char {mustBeMember(options.matchStrategy, ... - {'earlier', 'after', 'spectrumNumber'})} = 'earlier' - options.timeVariable char {mustBeMember(options.timeVariable, ... - {'lastModifiedDatenum', 'creationDate'})} = 'lastModifiedDatenum' - end - - numRowsTable = height(DataTable); - - % Validate length of the logical arrays - if length(processSelection) ~= numRowsTable || length(refSelection) ~= numRowsTable - error('Logical selection arrays must have the exact same length as the DataTable height (%d).', numRowsTable); - end - - % Ensure shift data exists - if ~ismember('bindingEnergyShift', DataTable.Properties.VariableNames) - error('The column "bindingEnergyShift" does not exist. Please run nappDetermineShift on your reference spectra first.'); - end - - % Initialize source logging column if it doesn't exist - if ~ismember('lognappCopyShiftSource', DataTable.Properties.VariableNames) - DataTable.lognappCopyShiftSource = cell(numRowsTable, 1); - end - - %% Processing Loop - processIdx = find(processSelection); - refIdx = find(refSelection); - - for i = 1:length(processIdx) - currRow = processIdx(i); - currName = DataTable.name{currRow}; - - % Safely extract time (handles both single numbers and 1x6 datevecs) - currTime = datenum(DataTable.(options.timeVariable)(currRow, :)); - refTimes = datenum(DataTable.(options.timeVariable)(refIdx, :)); - - bestRefRow = NaN; - - % Internal Matching Engine - switch options.matchStrategy - case 'earlier' - validRefs = refIdx(refTimes <= currTime); + +arguments + DataTable table + processSelection logical {mustBeVector} + refSelection logical {mustBeVector} + + % Name-Value arguments + options.matchStrategy char {mustBeMember(options.matchStrategy, ... + {'earlier', 'after', 'spectrumNumber'})} = 'earlier' + options.timeVariable char {mustBeMember(options.timeVariable, ... + {'lastModifiedDatenum', 'creationDate'})} = 'lastModifiedDatenum' + options.spectrumNumberPrefixLength double {mustBePositive, mustBeInteger} = 7 +end + +numRowsTable = height(DataTable); + +% Validate length of the logical arrays +if length(processSelection) ~= numRowsTable || length(refSelection) ~= numRowsTable + error('Logical selection arrays must have the exact same length as the DataTable height (%d).', numRowsTable); +end + +% Ensure shift data exists +if ~ismember('bindingEnergyShift', DataTable.Properties.VariableNames) + error('The column "bindingEnergyShift" does not exist. Please run nappDetermineShift on your reference spectra first.'); +end + +% Initialize source logging column if it doesn't exist +if ~ismember('lognappCopyShiftSource', DataTable.Properties.VariableNames) + DataTable.lognappCopyShiftSource = cell(numRowsTable, 1); +end + +%% Processing Loop +processIdx = find(processSelection); +refIdx = find(refSelection); + +for i = 1:length(processIdx) + currRow = processIdx(i); + currName = DataTable.name{currRow}; + + % Safely extract time (handles both single numbers and 1x6 datevecs) + currTime = datenum(DataTable.(options.timeVariable)(currRow, :)); + refTimes = datenum(DataTable.(options.timeVariable)(refIdx, :)); + + bestRefRow = NaN; + + % Internal Matching Engine + switch options.matchStrategy + case 'earlier' + validRefs = refIdx(refTimes <= currTime); + if ~isempty(validRefs) + timeDiffs = currTime - refTimes(refTimes <= currTime); + [~, minIdx] = min(timeDiffs); + bestRefRow = validRefs(minIdx); + end + + case 'after' + validRefs = refIdx(refTimes >= currTime); + if ~isempty(validRefs) + timeDiffs = refTimes(refTimes >= currTime) - currTime; + [~, minIdx] = min(timeDiffs); + bestRefRow = validRefs(minIdx); + end + + case 'spectrumNumber' + % Use the first N characters of the name as the matching prefix + prefixLen = options.spectrumNumberPrefixLength; + if length(currName) >= prefixLen + searchPattern = currName(1:prefixLen); + validRefs = refIdx(startsWith(DataTable.name(refIdx), searchPattern)); if ~isempty(validRefs) - timeDiffs = currTime - refTimes(refTimes <= currTime); - [~, minIdx] = min(timeDiffs); - bestRefRow = validRefs(minIdx); + bestRefRow = validRefs(1); end - - case 'after' - validRefs = refIdx(refTimes >= currTime); - if ~isempty(validRefs) - timeDiffs = refTimes(refTimes >= currTime) - currTime; - [~, 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) - searchPattern = [specNumStr, '_']; - validRefs = refIdx(startsWith(DataTable.name(refIdx), searchPattern)); - if ~isempty(validRefs) - bestRefRow = validRefs(1); - end - else - 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 shift from reference row - referenceShift = DataTable.bindingEnergyShift(bestRefRow); - - if isnan(referenceShift) - warning('Row %d: Matched reference row %d (%s) does not have a valid calculated shift. Skipping.', ... - currRow, bestRefRow, DataTable.name{bestRefRow}); - continue; - end - - % Copy the shift and log the exact name of the reference used - DataTable.bindingEnergyShift(currRow) = referenceShift; - DataTable.lognappCopyShiftSource{currRow} = DataTable.name{bestRefRow}; - % 1. Dynamically get this function's name - thisFuncName = mfilename; + else + warning('Row %d: Name "%s" is shorter than the requested prefix length (%d). Skipping.', ... + currRow, currName, prefixLen); + 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 shift from reference row + referenceShift = DataTable.bindingEnergyShift(bestRefRow); + + if isnan(referenceShift) + warning('Row %d: Matched reference row %d (%s) does not have a valid calculated shift. Skipping.', ... + currRow, bestRefRow, DataTable.name{bestRefRow}); + continue; + end + + % Copy the shift and log the exact name of the reference used + DataTable.bindingEnergyShift(currRow) = referenceShift; + DataTable.lognappCopyShiftSource{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); @@ -120,10 +125,10 @@ function DataTable = nappCopyShift(DataTable, processSelection, refSelection, op 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 @@ -131,10 +136,10 @@ function DataTable = nappCopyShift(DataTable, processSelection, refSelection, op 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 diff --git a/beamtime/nappDetermineShiftXps.m b/beamtime/nappDetermineShiftXps.m index 887df04..a7b058d 100644 --- a/beamtime/nappDetermineShiftXps.m +++ b/beamtime/nappDetermineShiftXps.m @@ -1,4 +1,4 @@ -function DataTable = nappDetermineShift(DataTable, processSelection, options) +function DataTable = nappDetermineShiftXps(DataTable, processSelection, options) % NAPPDETERMINESHIFT Calculate binding energy axis shifts based on a spectrum's own fitted peak. % % DataTable = NAPPDETERMINESHIFT(DataTable, processSelection) calculates @@ -6,7 +6,7 @@ function DataTable = nappDetermineShift(DataTable, processSelection, options) % on their own fitted peak position (internal reference). % % NOTE: This function requires that peak fitting (e.g., nappFitGaussXps) -% has already been performed so that the 'peakPos' column exists. +% has already been performed so that the 'peakPosition' column exists. % % Name-Value arguments: % referenceBindingEnergy - The theoretical energy to align to (e.g., 285.0 for C1s) @@ -17,7 +17,7 @@ function DataTable = nappDetermineShift(DataTable, processSelection, options) processSelection logical {mustBeVector} % Name-Value arguments - options.referenceBindingEnergy (1,1) double = 285.0 % Default to C1s binding energy + options.referenceBindingEnergy (1,1) double = 284.8 % Default to C1s binding energy options.peakNumber (1,1) double {mustBeInteger, mustBePositive} = 1 end @@ -29,8 +29,8 @@ function DataTable = nappDetermineShift(DataTable, processSelection, options) end % Ensure peak positions exist (Fit must be run first!) - if ~ismember('peakPos', DataTable.Properties.VariableNames) - error('The column "peakPos" does not exist. Please run nappFitGaussXps before calculating shifts.'); + if ~ismember('peakPosition', DataTable.Properties.VariableNames) + error('The column "peakPosition" does not exist. Please run nappFitGaussXps before calculating shifts.'); end % Initialize new columns to store the shift data if they don't exist @@ -49,7 +49,7 @@ function DataTable = nappDetermineShift(DataTable, processSelection, options) currName = DataTable.name{currRow}; % Extract the peak position - refPositions = DataTable.peakPos(currRow, :); + refPositions = DataTable.peakPosition(currRow, :); if size(refPositions, 2) < options.peakNumber || isnan(refPositions(options.peakNumber)) warning('Row %d (%s): peakNumber %d does not contain a valid fitted position. Skipping.', ... currRow, currName, options.peakNumber); @@ -58,10 +58,19 @@ function DataTable = nappDetermineShift(DataTable, processSelection, options) % Calculate the shift: Shift = Reference Binding Energy - Measured Energy measuredEnergy = refPositions(options.peakNumber); - calculatedShift = options.referenceBindingEnergy - measuredEnergy; + + % Validate that xRayEkin_eV exists and contains a valid scalar for this row + if ~ismember('xRayEkin_eV', DataTable.Properties.VariableNames) + error('The column "xRayEkin_eV" does not exist in DataTable. Cannot compute binding energy shift.'); + end + xRayVal = DataTable.xRayEkin_eV{currRow}; + if isempty(xRayVal) || ~(isnumeric(xRayVal) && isscalar(xRayVal)) || isnan(xRayVal) + error('Row %d (%s): xRayEkin_eV is empty, non-scalar, or NaN. Aborting shift calculation.', currRow, currName); + end + calculatedShift = xRayVal - options.referenceBindingEnergy - measuredEnergy; % Log the shift and its source DataTable.bindingEnergyShift(currRow) = calculatedShift; - DataTable.energyShiftSource{currRow} = 'self'; + DataTable.energyShiftSource{currRow} = num2str(options.referenceBindingEnergy); end end \ No newline at end of file