Files
nappMATLAB-toolbox/beamtime/nappCopyShift.m
T
bartels-rausch 091cf00e77 Refactor energy calibration pipeline, introduce modern arguments blocks, and add universal logging
* 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.
2026-03-25 12:07:49 +01:00

140 lines
5.9 KiB
Matlab

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
% is true) using a matching strategy.
%
% 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)
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);
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'
% 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;
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;
% -------------------------------------------------------------------------
end
end