diff --git a/nappNorm.m b/nappNorm.m new file mode 100644 index 0000000..0e8c706 --- /dev/null +++ b/nappNorm.m @@ -0,0 +1,140 @@ +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) scales each spectrum +% selected by the logical array by its own maximum value. +% +% Name-Value arguments: +% matchStrategy - 'earlier', 'after', '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) + +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', 'internalMax'})} = '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 +end + +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); +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'; +else + mode = 'referenceArea'; +end + +% Initialize new columns if they don't exist +if ~ismember('spectrum_countsNorm', DataTable.Properties.VariableNames) + DataTable.spectrum_countsNorm = cell(numRowsTable, 1); +end +if ~ismember('logNormMode', DataTable.Properties.VariableNames) + DataTable.logNormMode = cell(numRowsTable, 1); +end +if ~ismember('logNormWith', DataTable.Properties.VariableNames) + DataTable.logNormWith = 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}; + currTime = datenum(DataTable.(options.timeVariable)(currRow, :)); + refTimes = datenum(DataTable.(options.timeVariable)(refIdx, :)); + currName = DataTable.name{currRow}; + bestRefRow = NaN; + + if isempty(yData) + continue; + end + + switch mode + case 'maximum' + normFactor = max(yData); + if normFactor == 0, normFactor = 1; end % Protect against /0 + + DataTable.spectrum_countsNorm{currRow} = yData ./ normFactor; + 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' + 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 + + + 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.logNormMode{currRow} = sprintf('referenceArea (Row %d)', bestRefRow); + DataTable.logNormWith{currRow} = DataTable.name{bestRefRow}; + end +end +end \ No newline at end of file diff --git a/oldVersions/nappDetermineShiftXps.m b/oldVersions/nappDetermineShiftXps.m new file mode 100644 index 0000000..69d3197 --- /dev/null +++ b/oldVersions/nappDetermineShiftXps.m @@ -0,0 +1,609 @@ +function DataTable = nappDetermineShiftXps(DataTable, varargin) +%NAPPDETERMINESHIFTXPS Determine energy shift calibration from XPS images or spectra. +% This function calculates the energy shift needed to calibrate XPS +% spectra to a binding energy reference or align all sequence iterations +% of the same file to a common kinetic energy. For spectra data, it +% computes absolute binding energy shifts relative to a reference binding +% energy value. For images, it computes relative kinetic energy shifts +% between sequence iterations. Shifts are determined by fitting Gaussian +% peaks or finding maxima in the spectra (simpler option using Matlab's +% findpeaks function). +% +% Needs X-ray energy in data table. Run addPhotonEnergy.m first. +% +% +% SYNTAX +% ------ +% DataTable = NAPPDETERMINESHIFTXPS(DataTable) +% +% DataTable = NAPPDETERMINESHIFTXPS(DataTable, eKinStart, eKinEnd, 'Name', Value) +% +% EXAMPLES +% -------- +% % Determine binding energy shift for C1s spectra using peak finding +% DataTable = nappDetermineShiftXps(DataTable, ... +% 'tableVariable', 'spectrum_countsPerSecond', ... +% 'binEnReference', 285.0, ... +% 'mode', 'findPeak'); +% +% % Determine kinetic energy shift for imaging data using the first +% iteration as reference +% DataTable = nappDetermineShiftXps(DataTable, 100, 200, ... +% 'tableVariable', 'image', ... +% 'kinEnReference', 1, ... +% 'showFigures', 'on'); +% +% % Determine binding energy shift using Gaussian fitting with custom +% parameters specified in tmpFitParameters +% FitPar.fitMode = 'position'; +% FitPar.refPeak = 1; +% DataTable = nappDetermineShiftXps(DataTable, 280, 290, ... +% 'tableVariable', 'spectrum_countsPerSecond', ... +% 'binEnReference', 284.8, ... +% 'mode', 'gaussFit', ... +% 'FitParameter', tmpFitParameters); +% +% tmpFitParameters.peakArea = [1e5 0.5 0.1 0.1 0.1] ; +% tmpFitParameters.peakWidth = 1.2; +% tmpFitParameters.peakPos = [198 1.6 10 10 10]; +% +% tmpFitParameters.peakAreaUpperBond = [Inf 0.5 0.1 0.1 0.1]; +% tmpFitParameters.peakWidthUpperBond = [3 NaN NaN NaN NaN]; +% tmpFitParameters.peakPosUpperBond = [200 1.6 10 10 10]; +% +% tmpFitParameters.peakAreaLowerBond = [100 10 0.5 0.1 0.1 0.1]; +% tmpFitParameters.peakWidthLowerBond = [1.2 NaN NaN NaN NaN]; +% tmpFitParameters.peakPosLowerBxxond = [195 1.3 0.1 0.1 0.1]; +% +% tmpFitParameters.backgroundRange = [6 5]; +% tmpFitParameters.weight = [198 200 0.1]; +% tmpFitParameters.fitMode = 'widthPosition'; +% +% PROCESSING OVERVIEW +% ------------------- +% The function operates differently depending on data type: +% +% FOR IMAGES (tableVariable = 'image' or 'imageRoh'): +% 1. For each image in tableSelection: +% - Extract kinetic energy axis (scientaEkin_eV or scientaEkinRoh_eV) +% - Fit 2-peak Gaussian model to each sequence iteration (WIP: change +% this to use nappFitGaussXPS.m) +% - Extract peak position from fitting results +% - Normalize shifts relative to reference iteration (kinEnReference) +% 2. Result: kineticEnergyShift column contains shifts relative to reference iteration +% (reference iteration has shift = 0) +% +% FOR SPECTRA (tableVariable = 'spectrum_counts', 'spectrum_countsPerSecond', or 'spectrum_countsNorm'): +% 1. For each spectrum in tableSelection: +% - Optional: restrict energy range using eKinStart/eKinEnd +% - Find peak position using either: +% * findPeak method: standard matlab peak detection +% * gaussFit method: Gaussian fitting with global optimization +% - Compute binding energy shift as: xRayPhotonEnergy - peakPosition - binEnReference +% 2. Result: bindingEnergyShift column contains absolute shifts relative to reference BE +% (positive shift means peak is at lower kinetic energy than expected) +% +% PEAK DETECTION METHODS +% ---------------------- +% Two methods are available for finding peak positions in spectral +% +% 1. 'findPeak' (default): Fast automated detection using MATLAB's findpeaks +% function with moving mean smoothing. Uses minPeakWidth, minPeakHeight, +% and movMeanWindow parameters to control sensitivity. Best for well-defined +% peaks with good signal-to-noise ratio. Accuracy: ±0.1-0.2 eV typical. +% +% 2. 'gaussFit': Slower but more accurate Gaussian fitting with global +% optimization using GlobalSearch. Fits background, Gaussian peaks, +% and extracts precise peak positions. Requires FitParameter structure. +% Best for noisy data or when high precision is required. Accuracy: ±0.05 eV typical. +% +% IMPORTANT LIMITATION: This function requires distinct photoelectron peaks +% to determine energy shifts. It will NOT work for background-only spectra +% or featureless data. The fitting algorithms assume at least one measurable +% peak is present in the specified energy range. +% +% INPUT ARGUMENTS +% --------------- +% DataTable table +% Data table containing XPS image or spectral data. Must contain: +% - scientaEkin_eV or scientaEkinRoh_eV: kinetic energy axis +% - tableVariable data: images or spectra +% - xRayEkin_eV: X-ray photon energy (eV) for each row +% +% eKinStart numeric scalar (optional) +% Lower kinetic energy bound for peak fitting (eV). If omitted, uses +% first energy point. Restricts the fitting region to improve accuracy. +% +% eKinEnd numeric scalar (optional) +% Upper kinetic energy bound for peak fitting (eV). If omitted, uses +% last energy point. Restricts the fitting region to improve accuracy. +% +% NAME-VALUE PAIR ARGUMENTS (optional) +% ------------------------------------ +% 'tableSelection' logical array (default: all rows) +% Logical index to filter which rows in DataTable to process. +% Example: contains(DataTable.name, 'C1s') to process only C1s spectra +% +% 'tableVariable' char vector (default: 'spectrum_countsPerSecond') +% Data type to process. Must be one of: +% +% Spectral data (compute binding energy shifts): +% - 'spectrum_counts' +% - 'spectrum_countsPerSecond' +% - 'spectrum_countsNorm' +% +% Imaging data (compute kinetic energy shifts): +% - 'image' +% - 'imageRoh' +% +% 'mode' char vector (default: 'findPeak') +% Peak detection method (spectral data only): +% - 'findPeak': automated peak detection using moving mean smoothing +% - 'gaussFit': 2-peak Gaussian fitting with global optimization +% +% 'binEnReference' numeric scalar (required for spectra) +% Reference binding energy value (eV) for spectral calibration. +% Typically the known BE of the primary peak (e.g., 285.0 eV for C1s C-H). +% Example: 'binEnReference', 285.0 +% +% 'kinEnReference' numeric scalar (default: 1) +% Reference iteration index for imaging data. All kinetic energy shifts +% are computed relative to this iteration. Reference iteration gets shift = 0. +% Range: 1 to number of image columns. +% +% 'mode' char vector (default: 'findPeak') +% Peak finding method for spectra: +% - 'findPeak': uses findpeaks with smoothing and height/width constraints +% - 'gaussFit': uses nappFitGaussXps with global optimization (slower, more accurate) +% +% 'minPeakWidth' numeric scalar (default: 0.1) +% Minimum peak width (eV) for findPeak method. Peaks narrower than this +% are rejected. Typical range: 0.05-0.5 eV depending on spectrometer resolution. +% +% 'minPeakHeight' numeric scalar (default: 0.1) +% Minimum peak height (intensity) for findPeak method. Peaks below this +% are rejected. Units match tableVariable data. +% +% 'movMeanWindow' numeric scalar (default: 1) +% Moving mean window width (number of points) for smoothing before peak +% detection in findPeak method. Reduces noise; typical range: 1-5 points. +% +% 'FitParameter' struct +% Structure containing Gaussian fitting parameters for gaussFit mode. +% Required fields: +% - fitMode: 'position', 'width', 'unconstrained', etc. +% - refPeak: peak number (1 or 2) whose position is used for shift calculation +% Example: FitPar.fitMode = 'position'; FitPar.refPeak = 1; +% +% 'refPeak' numeric scalar (default: 1) +% Peak number (1 or 2) whose position is used for binding energy shift +% calculation in gaussFit mode. Peak 1 is typically the main photoelectron peak. +% +% 'showFigures' char vector (default: 'off') +% Display fitting results: +% - 'on': show Gaussian fit plot, peak table, and peak finding results +% - 'off': suppress all figure output +% +% OUTPUT ARGUMENTS +% ---------------- +% DataTable table +% Updated data table with new columns: +% +% For imaging data (tableVariable = 'image' or 'imageRoh'): +% - kineticEnergyShift: column vector of relative kinetic energy shifts (eV) +% relative to kinEnReference iteration. Reference = 0. +% +% For spectral data (tableVariable = 'spectrum_*'): +% - bindingEnergyShift: column vector of absolute binding energy shifts (eV) +% relative to binEnReference. Typically small values (±0.5 eV range). +% +% For gaussFit mode on spectra, additional columns are added: +% - peakPosition: [peak1, peak2] positions (eV) for each spectrum +% - peakArea: [peak1, peak2] areas (eV·counts) for each spectrum +% - peakFWHM: [peak1, peak2] full-width half-maximum (eV) for each spectrum +% - peakBackground: linear background parameters [slope, intercept] +% +% NOTES +% ----- +% - PREREQUISITE: X-ray photon energy (xRayEkin_eV) must be present in DataTable. +% If empty, function returns with warning. +% - For imaging: Gaussian fitting assumes 2 peaks in each iteration. Adjust +% eKinStart/eKinEnd to isolate peak region if data is complex. +% - For spectra: findPeak method is fast but less accurate (±0.1-0.2 eV typical). +% gaussFit method is slower but more accurate (±0.05 eV typical). +% - Peak detection may fail if signal-to-noise is poor. Check showFigures output +% to diagnose fitting quality. +% - For imageRoh variant: uses scientaEkinRoh_eV if available; falls back to +% scientaEkin_eV if not populated. +% - Binding energy shifts can be negative (peak at higher BE than reference) or +% positive (peak at lower BE than reference). +% - Figure windows are automatically closed after processing to prevent clutter. +% +% SEE ALSO +% -------- +% nappCopyShift, nappBindingEnergyXps, nappFitGaussXps, nappAlignImages, +% findpeaks, GaussFitOnePeak +defaultKinEnReference = 1; +defaultTableVariable = 'spectrum_countsPerSecond'; +expectedTableVariable = {'spectrum_counts','spectrum_countsPerSecond','spectrum_countsNorm','image','imageRoh'}; +defaultTableSelection = true(size(DataTable,1)); +defaultMinPeakWidth = 0.1; +defaultMinPeakHeight = 0.1; +defaultMovMeanWindow = 1; +defaultRefPeak = 1; +defaultShowFigures = 'off'; +expectedShowFigures = {'on','off'}; +defaultMode = 'findPeak'; +expectedMode = {'gaussFit','findPeak'}; +% defaultFitMode = 'widthPosition'; +% expectedFitMode = {'unconstrained','widthPosition','position','width','unconstrainedAndWidthPosition'}; + + +%% + +% Parse input +p = inputParser; +addRequired(p,'DataTable',@istable); +addOptional(p,'eKinStart',@isnumeric) +addOptional(p,'eKinEnd',@isnumeric) +addParameter(p,'FitParameter',@isstruct); +addParameter(p,'binEnReference',@isnumeric) +addParameter(p,'kinEnReference',defaultKinEnReference,@isnumeric) +addParameter(p,'tableSelection',defaultTableSelection,@islogical); +addParameter(p,'minPeakWidth',defaultMinPeakWidth,@isnumeric); +addParameter(p,'minPeakHeight',defaultMinPeakHeight,@isnumeric); +addParameter(p,'movMeanWindow',defaultMovMeanWindow ,@isnumeric); +addParameter(p,'tableVariable',defaultTableVariable,... + @(x) any(validatestring(x,expectedTableVariable))); +addParameter(p,'showFigures',defaultShowFigures,... + @(x) any(validatestring(x,expectedShowFigures))); +addParameter(p,'mode',defaultMode, @(x) any(validatestring(x,expectedMode))); +% addParameter(p,'fitMode',defaultFitMode, @(x) any(validatestring(x,expectedFitMode))); +addParameter(p,'refPeak',defaultRefPeak,@isnumeric); +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 + +% add empty variable to table +if contains(p.Results.tableVariable,'image') + existColumn = strcmp('kineticEnergyShift',DataTable.Properties.VariableNames); + if ~any(existColumn) + DataTable.kineticEnergyShift = cell(size(DataTable,1),1); + end +else + existColumn = strcmp('bindingEnergyShift',DataTable.Properties.VariableNames); + if ~any(existColumn) + DataTable.bindingEnergyShift = NaN(size(DataTable,1),1); + end +end +%Colors; define PSI cooperate identity colours +PsiFarbe.hellgrau = [226 226 226]/255; +PsiFarbe.mittelgrau = [184 184 184]/255; +PsiFarbe.dunkelgrau = [78 78 77]/255; +PsiFarbe.blaugrau = [236 236 243]/255; % This is actually not a PSI colour +PsiFarbe.gelb = [239 200 37]/255; +PsiFarbe.hellorange = [227 164 31]/255; +PsiFarbe.dunkelorange = [207 92 22]/255; +PsiFarbe.hellrot = [164 36 23]/255; +PsiFarbe.dunkelbraun = [114 85 65]/255; +PsiFarbe.hellbraun = [127 119 47]/255; +PsiFarbe.mittelgruen = [135 150 59]/255; +PsiFarbe.hellgruen = [195 214 47]/255; +PsiFarbe.dunkelgruen = [72 130 33]/255; +PsiFarbe.dunkelrot = [104 47 86]/255; +PsiFarbe.dunkelblau = [37 65 108]/255; +PsiFarbe.mittelgruenblau = [61 164 168]/255; +PsiFarbe.hellblau = [58 162 190]/255; +PsiFarbe.mittelblau = [50 118 158]/255; + +% confirm that X-ray energy is set +if any(isnan(cell2mat(p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV))) + disp('Opps, X-ray energy is empty. Please add manually. Will stop now'); + return +end +if matches(p.Results.tableVariable,'image') + % repeat this for all columns in input. + for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1) + %% + try + if isnumeric(p.Results.eKinStart) + start = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}... + >= p.Results.eKinStart,1); + stop = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}... + >= p.Results.eKinEnd,1); + else + start = 1; + stop = length(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}); + end + catch + warning('There is something wrong with the eKinStart and/or eKinEnd parameters'); + disp('There is something wrong with the eKinStart and/or eKinEnd parameters'); + end + + for ij = 1:size(p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii},2) + [B,x1,y1] = GaussFitOnePeak([p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}(start:stop)',... + p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}(start:stop,ij)]); + tmp_1(ij) = p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}-B(3); + % plot result + if matches(p.Results.showFigures,'on') + % define equations to be plotted + Gauss = @(x,Hgt, Pos, Wdt) Hgt./(Wdt.*sqrt(pi/2))*exp(-2.*((x - Pos).^2/Wdt.^2)); + linear = @(x,slp,intrcpt) slp*x+intrcpt; + figure( 'Name', 'Gauss Multi Fit ') + tiledlayout(1,1) + nexttile; + %Define gauss peaks with hight, position, width from fitting results + yData_pk1= Gauss(x1 ,B(1), B(3), B(2)); + yData_pk2= Gauss(x1 ,B(4), B(6), B(5)); + bkgrd = linear(x1,B(7),B(8)); + % plot data + plot(x1,y1,'.') + hold on + % plot fit + h = plot(x1,yData_pk1+yData_pk2+bkgrd); + h.LineWidth=2; + h.Color = PsiFarbe.dunkelblau; + % plot each gauss peak + h = plot(x1,yData_pk1+bkgrd); + h.LineWidth=1.5; + h.Color = PsiFarbe.gelb; + h = plot(x1,yData_pk2+bkgrd); + h.LineWidth=1.5; + h.Color = PsiFarbe.gelb; + + legend OFF + xlabel('Kinetic Energy [eV]'); + ylabel('PE intensity [-]'); + grid on + + % List results in table + + disp('Results'); + disp(' ' ); + disp(cell2table({sprintf('%.1f',B(3)),... + sprintf('%.1f',B(2)*sqrt(log(4))),... + sprintf('%.1f',B(2)),... + sprintf('%7.0f',B(1))},... + 'VariableNames',{'position';'FWHW';'width';'area'},... + 'RowNames',{'pk 1'})); + end + end + %% + + + DataTable(p.Results.tableSelection,:).kineticEnergyShift{ii} = tmp_1-tmp_1(p.Results.kinEnReference); + clearvars tmp_1 + fh = findobj( 'Type', 'Figure', 'Name', 'Gauss Multi Fit '); + close(fh) + fh = findobj( 'Type', 'Figure', 'Name', 'GlobalSearch'); + close(fh) + end +elseif matches(p.Results.tableVariable,'imageRoh') + % repeat this for all columns in input. + for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1) + + %% + try + if isnumeric(p.Results.eKinStart) && isempty(DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}) + start = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}... + >= p.Results.eKinStart,1); + stop = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}... + >= p.Results.eKinEnd,1); + elseif isnumeric(p.Results.eKinStart) && ~isempty(DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}) + start = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}... + >= p.Results.eKinStart,1); + stop = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}... + >= p.Results.eKinEnd,1); + elseif isempty(DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}) + start = 1; + stop = length(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}); + else + start = 1; + stop = length(DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}); + end + catch + warning('There is something wrong with the eKinStart and/or eKinEnd parameters'); + disp('There is something wrong with the eKinStart and/or eKinEnd parameters'); + end + for ij = 1:size(p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii},2) + if isempty(DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}) + [B,x1,y1] = GaussFitOnePeak([p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}(start:stop)',... + p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}(start:stop,ij)]); + else + [B,x1,y1] = GaussFitOnePeak([DataTable(p.Results.tableSelection,:).scientaEkinRoh_eV{ii}(start:stop)',... + p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}(start:stop,ij)]); + end + + tmp_1(ij) = p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}-B(3); + % plot result + if matches(p.Results.showFigures,'on') + % define equations to be plotted + Gauss = @(x,Hgt, Pos, Wdt) Hgt./(Wdt.*sqrt(pi/2))*exp(-2.*((x - Pos).^2/Wdt.^2)); + linear = @(x,slp,intrcpt) slp*x+intrcpt; + figure( 'Name', 'Gauss Multi Fit ') + tiledlayout(1,1) + nexttile; + %Define gauss peaks with hight, position, width from fitting results + yData_pk1= Gauss(x1 ,B(1), B(3), B(2)); + yData_pk2= Gauss(x1 ,B(4), B(6), B(5)); + bkgrd = linear(x1,B(7),B(8)); + % plot data + plot(x1,y1,'.') + hold on + % plot fit + h = plot(x1,yData_pk1+yData_pk2+bkgrd); + h.LineWidth=2; + h.Color = PsiFarbe.dunkelblau; + % plot each gauss peak + h = plot(x1,yData_pk1+bkgrd); + h.LineWidth=1.5; + h.Color = PsiFarbe.gelb; + h = plot(x1,yData_pk2+bkgrd); + h.LineWidth=1.5; + h.Color = PsiFarbe.gelb; + + legend OFF + xlabel('Kinetic Energy [eV]'); + ylabel('PE intensity [-]'); + grid on + + % List results in table + + disp('Results'); + disp(' ' ); + disp(cell2table({sprintf('%.1f',B(3)),... + sprintf('%.1f',B(2)*sqrt(log(4))),... + sprintf('%.1f',B(2)),... + sprintf('%7.0f',B(1))},... + 'VariableNames',{'position';'FWHW';'width';'area'},... + 'RowNames',{'pk 1'})); + end + end + %% + + DataTable(p.Results.tableSelection,:).kineticEnergyShift{ii} = tmp_1-tmp_1(p.Results.kinEnReference); + clearvars tmp_1 + fh = findobj( 'Type', 'Figure', 'Name', 'Gauss Multi Fit '); + close(fh) + fh = findobj( 'Type', 'Figure', 'Name', 'GlobalSearch'); + close(fh) + end +else + % repeat this for all columns in input. + if matches(p.Results.mode,'findPeak') + for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1) + + %% + try + if isnumeric(p.Results.eKinStart) + start = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}... + >= p.Results.eKinStart,1); + stop = find(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}... + >= p.Results.eKinEnd,1); + else + start = 1; + stop = length(p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}); + end + catch + warning('There is something wrong with the eKinStart and/or eKinEnd parameters'); + disp('There is something wrong with the eKinStart and/or eKinEnd parameters'); + end + + if contains(p.Results.showFigures,'on') + figure + + findpeaks(movmean(p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}(start:stop),... + p.Results.movMeanWindow),... + p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}(start:stop),... + 'MinPeakWidth',p.Results.minPeakWidth,'MinPeakHeight',p.Results.minPeakHeight) + end + [pks,locs]=findpeaks(movmean(p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}(start:stop),... + p.Results.movMeanWindow),... + p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii}(start:stop),... + 'MinPeakWidth',p.Results.minPeakWidth,'MinPeakHeight',p.Results.minPeakHeight); + + %% + + [~,I] = max(pks); + tmp_1(ii,1) = p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}-locs(I)-p.Results.binEnReference; + clearvars tmp_2 + end + DataTable.bindingEnergyShift(p.Results.tableSelection) = tmp_1; + + elseif matches(p.Results.mode,'gaussFit') + tmpFitResults = nappFitGaussXps(DataTable,p.Results.FitParameter,p.Results.tableSelection,... + 'energyScale','kinetic','mode',p.Results.FitParameter.fitMode,'showFigures','on','tableVariable',p.Results.tableVariable); + + DataTable.bindingEnergyShift(p.Results.tableSelection) = tmpFitResults(p.Results.tableSelection,:).xRayEkinRange_eV(:,1) - tmpFitResults(p.Results.tableSelection,:).peakPosition(:,p.Results.FitParameter.refPeak) - repmat(p.Results.binEnReference,size(DataTable.bindingEnergyShift(p.Results.tableSelection))); + DataTable.peakPosition(p.Results.tableSelection,:) = tmpFitResults(p.Results.tableSelection,:).peakPosition; + DataTable.peakArea(p.Results.tableSelection,:) = tmpFitResults(p.Results.tableSelection,:).peakArea; + DataTable.peakFWHM(p.Results.tableSelection,:) = tmpFitResults(p.Results.tableSelection,:).peakFWHM; + % added 6.2025 tbr + DataTable.peakBackground(p.Results.tableSelection,:) = tmpFitResults(p.Results.tableSelection,:).peakBackground; + %% + + + + end + + +end +try + fh = findobj( 'Type', 'Figure', 'Name', 'Gauss Multi Fit '); + close(fh) +catch +end +try + fh = findobj( 'Type', 'Figure', 'Name', 'GlobalSearch'); + close(fh) +catch +end + +end +function [B,x1,y1] = GaussFitOnePeak(data1) + + +[x1, y1] = prepareCurveData(data1(:,1),data1(:,2)); + +% make data slimmer so that background makes sense +[~,I] = max(y1); +tmp1 = find(x1>=x1(I)-8,1); +tmp2 = find(x1>=x1(I)+5,1); +if isempty(tmp2) + y1 = y1(tmp1:end); + x1 = x1(tmp1:end); +else + y1 = y1(tmp1:tmp2); + x1 = x1(tmp1:tmp2); +end +[~,I] = max(y1); + +% Define equation to fit data +% I use the area Gauss equations here, which I took from the OriginPro +% documentation. https://www.originlab.com/doc/Origin-Help/Gauss-FitFunc +% +% Gauss = area/(width*sqrt(pi/2)) * exp(-2*(x-position)^2/width^2) +% +% where FWHM = width * sqrt(ln(4)) + +% Data set 1: +yfcn1 = @(b,x)... + b(1)/(b(2)*sqrt(pi/2))*exp(-2.*((x(:,1) - b(3)).^2/b(2).^2)) ... % Gauss peak 1: C-H, Ac- + + b(4)/(b(5)*sqrt(pi/2))*exp(-2.*((x(:,1)- b(6)).^2/b(5).^2))... % Gauss peak 2: C-OOH, Ac- + +b(7).*x(:,1)+b(8); % linear background + +B0 = [1e5 1.5 x1(I) ... + 1e5 1.0 x1(I)-4 ... + 1 0]; + + +% lower bonds +lb = [100 0.5 x1(I)-1. ... + 100 1.0 x1(I)-5 ... + -Inf -Inf]; + + +% upper bond +ub = [Inf 3 x1(I)+1 ... + Inf 5 x1(I)-1 ... + Inf Inf]; + + +% Fit equations: Gauss eq. 1 with data +yfcn = @(b,x) yfcn1(b,x); +% We find an optimal fit by minimising the residuals: +fitfcnw = @(b) sum((y1 - yfcn(b,x1)).^2); + +problem = createOptimProblem('fmincon', 'x0',B0, 'lb',lb,'ub',ub, 'objective',fitfcnw); +gs = GlobalSearch('PlotFcns',@gsplotbestf); +B = run(gs,problem); + + + + +end \ No newline at end of file diff --git a/oldVersions/nappScale.m b/oldVersions/nappScale.m new file mode 100644 index 0000000..e313769 --- /dev/null +++ b/oldVersions/nappScale.m @@ -0,0 +1,313 @@ +function DataTable = nappScale(DataTable,varargin) +%NAPPSCALE Normalize XPS and NEXAFS spectra. +% The peak of the same region or of a different region with identical +% SES file number (same sequence iteration) can be used for XPS data. +% NEXAFS spectra can be normed to their area or to the intensity +% at an energy range of choice. +% +% For XPS ('.ibw') data, NAPPSCALE divides spectra of one region +% (regionOld, default is C1s) by the fitted peak area taken from a +% spectrum of the same sequence but different region (regionNew, default +% is O1s). The same sequence iteration is identified by the SES file +% number and acquisition time. For NEXAFS ('.h5') data, NAPPSCALE +% normalizes spectra to the mean intensity or to the integrated area over +% a chosen photon-energy interval. +% +% DataTable = NAPPSCALE(DataTable) normalizes spectra in the input table +% using default options and returns the updated table with +% normalized spectra in the variable spectrum_countsNorm. +% +% DataTable = NAPPSCALE(DataTable, tableSelection) restricts +% normalization to rows where tableSelection is true. The length +% of tableSelection must match the height of DataTable. +% +% DataTable = NAPPSCALE(DataTable, tableSelection, energyStart, energyEnd) +% additionally specifies an energy interval used to normalize NEXAFS +% spectra ('.h5' data) +% +% DataTable = NAPPSCALE(..., Name, Value) customizes the source variable, +% normalization mode, line region mapping, and peak selection via +% Name-Value arguments (see below): +% + +% +% Required input +% -------------- +% DataTable +% Table containing spectroscopy data and metadata.[web:2] +% Depending on file type and options, the function uses: +% - name : file name, used to distinguish 'ibw' +% and 'h5' and to construct regionNew +% file names (XPS) +% - spectrum_counts : 1D spectrum (cell of numeric) +% - spectrum_countsPerSecond +% : 1D spectrum normalized to rate +% - spectrum_countsNorm : (created/overwritten) normalized +% spectrum +% - peakArea : numeric vector of fitted peak areas +% (XPS, one row per region) +% - lastModifiedDatenum : datenum time stamp used to find the +% matching regionNew spectrum +% - xRayEkin_eV : photon energy axis (NEXAFS) +% +% Optional positional input +% ------------------------- +% tableSelection +% Logical vector selecting rows in DataTable to normalize. +% Default: all rows true +% +% energyStart, energyEnd +% Photon-energy limits (in eV) that define the normalization +% interval for NEXAFS spectra ('.h5'). +% Type : numeric scalars +% Use : required for 'intensity' or 'area' mode on '.h5' data. +% +% Name-Value arguments +% -------------------- +% 'tableVariable' +% Name of the spectral variable to be normalized.[web:10] +% Type : char or string +% Values : 'spectrum_countsPerSecond' (default) +% 'spectrum_counts' +% 'spectrum_countsNorm' +% +% 'regionOld' +% XPS region name of the spectra to be normalized ('.ibw'). +% Used for file-name replacement when searching the reference +% region +% Type : char or string +% Values : 'C1s' (default) | 'O1s' | 'N1s' | 'Cl2p' +% +% 'regionNew' +% XPS region name that provides the reference peak area used for +% normalization. +% Type : char or string +% Values : 'O1s' (default) | 'C1s' | 'N1s' +% +% 'peakNumber' +% Index of the peak in the peakArea vector used as normalization +% reference for XPS data. +% Type : positive integer scalar +% Default: 1 +% +% 'mode' +% Normalization mode. +% Type : char or string +% Values : 'area' (default) +% 'intensity' +% +% Behaviour: +% - For '.ibw' (XPS) +% 'area' : divide selected spectra by the chosen peakArea +% of the corresponding regionNew spectrum. +% 'intensity' : currently not implemented (warning printed). +% - For '.h5' (NEXAFS) +% 'intensity' : divide spectra by the mean intensity over +% [energyStart, energyEnd]. +% 'area' : divide spectra by the integrated area under +% the curve over [energyStart, energyEnd] +% (trapz). +% +% Output +% ------ +% DataTable +% Updated input table with normalized spectra and log variables. +% The function ensures the following variables: +% +% spectrum_countsNorm +% Cell array containing the normalized spectrum for each +% processed row. Existing values are overwritten. +% +% logScaleRange +% Numeric matrix with two columns. For '.h5' rows, stores the +% [energyStart energyEnd] interval used for normalization. +% +% logScaleMode +% Cell array of character vectors with the mode ('area' or +% 'intensity') used for each normalized row. +% +% logScaleDate +% Cell array of character vectors containing the time stamp of +% the normalization (datestr(now)). +% +% Notes +% ----- +% - All selected entries in the chosen tableVariable must contain +% non-empty spectra; otherwise, the function throws an error. +% - For XPS data, regionOld and the tableSelection filter must refer to +% the same region; regionNew is found by name replacement and by +% matching date and time to locate the correct reference spectrum. +% - For NEXAFS data, the function supports both increasing and decreasing +% photon-energy axes when locating the normalization interval. + + + +% Define conditions and defaults of input +defaultTableVariable = 'spectrum_countsPerSecond'; +expectedTableVariable = {'spectrum_counts','spectrum_countsPerSecond','spectrum_countsNorm'}; +defaultMode = 'area'; +expectedMode = {'intensity','area'}; +defaultRegionOld = 'C1s'; +expectedRegionOld = {'O1s','C1s','N1s','Cl2p'}; +defaultRegionNew = 'O1s'; +expectedRegionNew = {'O1s','C1s','N1s'}; +defaultPeakNumber = 1; +defaultTableSelection = true(size(DataTable,1)); + + +% Parse input +p = inputParser; +addRequired(p,'DataTable',@istable); +addOptional(p,'tableSelection',defaultTableSelection,@islogical); +addOptional(p,'energyStart',@isnumeric) +addOptional(p,'energyEnd',@isnumeric) +addParameter(p,'tableVariable',defaultTableVariable,... + @(x) any(validatestring(x,expectedTableVariable))); +addParameter(p,'regionOld',defaultRegionOld,... + @(x) any(validatestring(x,expectedRegionOld))); +addParameter(p,'regionNew',defaultRegionNew,... + @(x) any(validatestring(x,expectedRegionNew))); +addParameter(p,'peakNumber',defaultPeakNumber); +addParameter(p,'mode',defaultMode,... + @(x) any(validatestring(x,expectedMode))); +parse(p,DataTable,varargin{:}); + +% add empty variable to table +existColumn = strcmp('spectrum_countsNorm',DataTable.Properties.VariableNames); +if ~any(existColumn) + DataTable.spectrum_countsNorm = cell(size(DataTable,1),1); +end + +existColumn = strcmp('logScaleRange',DataTable.Properties.VariableNames); +if ~any(existColumn) + DataTable.logBackgroundRange = NaN(size(DataTable,1),2); +end +existColumn = strcmp('logScaleMode',DataTable.Properties.VariableNames); +if ~any(existColumn) + DataTable.logBackgroundMode = cellstr(repmat(' ',size(p.Results.DataTable,1),1)); +end +existColumn = strcmp('logScaleDate',DataTable.Properties.VariableNames); +if ~any(existColumn) + DataTable.logBackgroundDate = cellstr(repmat(' ',size(p.Results.DataTable,1),1)); +end + +% 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 + + +% repeat this for all columns in input. +for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1) + if endsWith(p.Results.DataTable(p.Results.tableSelection,:).name{ii},'ibw') || endsWith(p.Results.DataTable(p.Results.tableSelection,:).name{ii},'xy') + % get name and date of input file and replace old region with new + % region. This will be the files where the peak area is taken from. + tmpName = replace(p.Results.DataTable(p.Results.tableSelection,:).name{ii}, ... + p.Results.regionOld,p.Results.regionNew); + tmpDate = p.Results.DataTable(p.Results.tableSelection,:).lastModifiedDatenum(ii); + % select spectra based on the new names and dates of original file + tmpDataSelection = contains(p.Results.DataTable.name,tmpName)... + & ismember(year(p.Results.DataTable.lastModifiedDatenum),year(tmpDate))... + & ismember(month(p.Results.DataTable.lastModifiedDatenum),month(tmpDate))... + & ismember(day(p.Results.DataTable.lastModifiedDatenum),day(tmpDate))... + & ismember(hour(p.Results.DataTable.lastModifiedDatenum),hour(tmpDate))... + & ismember(minute(p.Results.DataTable.lastModifiedDatenum),minute(tmpDate)); + % check peak area + existColumn = strcmp('peakArea',DataTable.Properties.VariableNames); + if any(existColumn) + if isnan(DataTable( tmpDataSelection,:).peakArea(p.Results.peakNumber)) + disp('The peak area appears to be empty. I quit now') + disp(DataTable( tmpDataSelection,:).name{ii}) + error('The peak area appears to be empty.') + end + else + disp('The data table appear to have not peak area column. I quit now') + error('No peak area column') + end + % do the norming + if contains(p.Results.mode,'area') + DataTable(p.Results.tableSelection,:).spectrum_countsNorm{ii} = ... + DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}... + ./ DataTable( tmpDataSelection,:).peakArea(p.Results.peakNumber); + elseif contains(p.Results.mode,'intensity') + disp('Intensity mode not yet programmed for XPS data') + end + + elseif endsWith(p.Results.DataTable(p.Results.tableSelection,:).name{ii},'h5') + % if ~exist(p.Results.energyStart) | ~exist(p.Results.energyEnd) + % disp('The energy region used for normalisation appears to be empty. I quit now') + % error('The energy region used to normalise appears to be empty.') + % end + if contains(p.Results.mode,'intensity') + if p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(1)... + < p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(end) + % prepare Data + [xData, yData] = prepareCurveData(p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}... + , p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}); + % MEAN + b_1 = find(xData >= p.Results.energyStart,1); + b_2 = find(xData >= p.Results.energyEnd,1); + + b_mean = mean(yData(b_1:b_2)); + elseif p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(1)... + > p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(end) + % prepare Data + [xData, yData] = prepareCurveData( p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}... + , p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}); + % MEAN + b_1 = find(xData <= p.Results.energyStart,1); + b_2 = find(xData <= p.Results.energyEnd,1); + + b_mean = mean(yData(b_2:b_1)); + else + disp('Warning: Issue with NEXAFS_col_scaling') + end + + tmp_3(:,1) = p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}... + ./b_mean; + + DataTable(p.Results.tableSelection,:).spectrum_countsNorm{ii} = tmp_3; + clearvars tmp_3 + elseif contains(p.Results.mode,'area') + if p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(1)... + < p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(end) + % prepare Data + [xData, yData] = prepareCurveData( p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}... + , p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}); + % Area + b_1 = find(xData >= p.Results.energyStart,1); + b_2 = find(xData >= p.Results.energyEnd,1); + + b_area = trapz(xData(b_1:b_2),yData(b_1:b_2)); + elseif p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(1)... + > p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(end) + % prepare Data + [xData, yData] = prepareCurveData( p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}... + , p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}); + % MEAN + b_1 = find(xData <= p.Results.energyStart,1); + b_2 = find(xData <= p.Results.energyEnd,1); + + b_area = trapz(xData(b_2:b_1),yData(b_2:b_1)); + else + disp('Warning: Issue with NEXAFS_col_scaling') + end + + tmp_3(:,1) = p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii}... + ./b_area; + + DataTable(p.Results.tableSelection,:).spectrum_countsNorm{ii} = tmp_3; + clearvars tmp_3 + + end + end +end +% write log to table +DataTable.logScaleMode(p.Results.tableSelection,:) = cellstr(repmat(p.Results.mode,size(p.Results.DataTable(p.Results.tableSelection,:),1),1)); +if endsWith(p.Results.DataTable(p.Results.tableSelection,:).name{ii},'h5') + DataTable.logScaleRange(p.Results.tableSelection,:) = repmat([p.Results.energyStart p.Results.energyEnd],size(p.Results.DataTable(p.Results.tableSelection,:),1),1); +end +DataTable.logScaleDate(p.Results.tableSelection,:) = cellstr(repmat(datestr(now),size(p.Results.DataTable(p.Results.tableSelection,:),1),1)); + +end \ No newline at end of file diff --git a/utils/nappSaveDataTable.m b/utils/nappSaveDataTable.m new file mode 100644 index 0000000..4fb4478 --- /dev/null +++ b/utils/nappSaveDataTable.m @@ -0,0 +1,70 @@ +function saveFilename = nappSaveDataTable(DataTable, options) +% NAPPSAVEDATATABLE Automatically saves the data table with a dynamic filename. +% +% NAPPSAVEDATATABLE(DataTable) saves the provided table into a .mat file. +% The filename is automatically generated based on the script or Live +% Script that called this function (e.g., "myScript_FileList.mat"). +% +% The variable inside the .mat file will keep the exact same name it had +% in your workspace (e.g., if you pass 'FileList', it saves as 'FileList'). +% +% Name-Value arguments: +% suffix - Text to append to the script name (default: nothing) + + arguments + DataTable table + + % Name-Value arguments + options.suffix char = "" + end + + currentScriptName = ''; + + % 1. Determine the name of the script that CALLED this function + stackInfo = dbstack; + if length(stackInfo) > 1 + % stackInfo(1) is nappSaveDataTable, stackInfo(2) is the caller + [~, currentScriptName, ~] = fileparts(stackInfo(2).file); + + % INTERCEPT LIVE EDITOR: If it's a hidden temporary evaluation file, + % clear the name so we force it to use the fallback below. + if contains(currentScriptName, 'LiveEditorEvaluationHelper') + currentScriptName = ''; + end + end + + % 2. Fallback if run from the command window or intercepted from the Live Editor + if isempty(currentScriptName) + try + % Ask the MATLAB editor for the actual name of the currently open tab + [~, currentScriptName, ~] = fileparts(matlab.desktop.editor.getActiveFilename); + catch + currentScriptName = 'ManualRun'; + end + end + + % Ensure we have a valid string (just in case the editor is closed) + if isempty(currentScriptName) + currentScriptName = 'WorkspaceData'; + end + + % 3. Generate the final filename + saveFilename = sprintf('%s%s.mat', currentScriptName, options.suffix); + + % 4. Determine the original variable name passed by the user (e.g., "FileList") + originalVarName = inputname(1); + + % If it has no workspace name, default to 'DataTable' + if isempty(originalVarName) + originalVarName = 'DataTable'; + end + + % 5. Create a local variable with that exact name so the .mat file stores it correctly + eval(sprintf('%s = DataTable;', originalVarName)); + + % 6. Save the specific variable to the file + save(saveFilename, originalVarName); + + % 7. Print confirmation + fprintf('✅ Successfully saved %s to: %s\n', originalVarName, saveFilename); +end \ No newline at end of file