diff --git a/beamtime/nappAddFitToPlot.m b/beamtime/nappAddFitToPlot.m index 362aca2..431b4c8 100644 --- a/beamtime/nappAddFitToPlot.m +++ b/beamtime/nappAddFitToPlot.m @@ -24,8 +24,8 @@ xlims = xlim(ax); energyMin = xlims(1); energyMax = xlims(2); -% Create energy range with 0.2 eV step -energyStep = 0.2; +% Create energy range with 0.01 eV step +energyStep = 0.01; energyRange = (energyMin:energyStep:energyMax)'; % ===================================================================== diff --git a/beamtime/nappFileList.m b/beamtime/nappFileList.m index b1c20cd..c9d1ee1 100755 --- a/beamtime/nappFileList.m +++ b/beamtime/nappFileList.m @@ -548,7 +548,7 @@ if ~isempty(TmpFileList{1}) current_file_info.lastModifiedDatestr = current_file_info.date; current_file_info.image = {[]}; current_file_info.imageRoh = {[]}; - current_file_info.xRayEkin_eV = NaN; + current_file_info.xRayEkin_eV = {[]}; current_file_info.xRayEkinRange_eV = [NaN NaN]; current_file_info.scientaPassEnergy_eV = NaN; current_file_info.scientaDwellTime_ms = NaN; @@ -942,7 +942,8 @@ for ii = 1:size(TmpFileList{2},1) % do this for each list element end % Make unique - [TmpFileList{2}(ii).xRayEkin_eV, ind] = unique(TmpXrayEkin); + [tmpE, ind] = unique(TmpXrayEkin); + TmpFileList{2}(ii).xRayEkin_eV = {tmpE(:)'}; TmpFileList{2}(ii).image = TmpImage(:,ind); TmpFileList{2}(ii).image = TmpImage(:,ind); % NEXAFS pass energy @@ -1029,8 +1030,10 @@ for ii = 1:size(TmpFileList{2},1) % do this for each list element TmpFileList{2}(ii).scientaEkinRange_eV = [double(tmp(1))-TmpFileList{2}(ii).scientaPassEnergy_eV/20 double(tmp(1))+TmpFileList{2}(ii).scientaPassEnergy_eV/20]; - TmpFileList{2}(ii).scientaEkin_eV = linspace(TmpFileList{2}(ii).scientaEkinRange_eV(:,1)... - ,TmpFileList{2}(ii).scientaEkinRange_eV(:,2),size(TmpFileList{2}(ii).image,1))'; + TmpFileList{2}(ii).scientaEkin_eV = { ... + linspace(TmpFileList{2}(ii).scientaEkinRange_eV(1), ... + TmpFileList{2}(ii).scientaEkinRange_eV(2), ... + size(TmpFileList{2}(ii).image,1))' }; else % NEXAFS kinetic energy window - scan mode try @@ -1057,8 +1060,11 @@ for ii = 1:size(TmpFileList{2},1) % do this for each list element TmpFileList{2}(ii).scientaEkinStep_eV = abs(tmp(1)); end end - TmpFileList{2}(ii).scientaEkin_eV = [TmpFileList{2}(ii).scientaEkinRange_eV(:,1):TmpFileList{2}(ii).scientaEkinStep_eV(1):TmpFileList{2}(ii).scientaEkinRange_eV(:,2)]'; - end + TmpFileList{2}(ii).scientaEkin_eV = { ... + (TmpFileList{2}(ii).scientaEkinRange_eV(1) : ... + TmpFileList{2}(ii).scientaEkinStep_eV : ... + TmpFileList{2}(ii).scientaEkinRange_eV(2))' }; + end clearvars tmp % read I_0 @@ -1202,8 +1208,7 @@ for ii = 1:size(TmpFileList{3},1) % do this for each picture in the list TmpFileList{3}(ii).lastModifiedDatestr = TmpFileList{3}(ii).date; end -%% Merge lists into one -% + %% Merge lists into one ListFiles = table(); @@ -1473,6 +1478,9 @@ if istable(p.Results.DataTable) % do this only if previous list was input to fun ListFiles = outerjoinRowToTable(p.Results.DataTable, ListFiles); % Then, we sort the table based on the time (datenum) ListFiles = sortrows(ListFiles,'lastModifiedDatenum'); + % check fromat + assert(iscell(ListFiles.xRayEkin_eV), 'xRayEkin_eV must be cell') + assert(iscell(ListFiles.scientaEkin_eV), 'scientaEkin_eV must be cell') end %% Export diff --git a/import/importJeti.m b/import/importJeti.m index c9e9f74..04e3f97 100644 --- a/import/importJeti.m +++ b/import/importJeti.m @@ -1,20 +1,25 @@ -function dataTable = importJeti(filename) +function dataTable = importJeti(filename, selectedDatasets) %IMPORTJETI Import JETI SPECBOS irradiance data (CSV or TXT format) -% DATATABLE = IMPORTJETI(FILENAME) reads data from text file FILENAME. +% DATATABLE = IMPORTJETI(FILENAME) reads all data from the text file. +% +% DATATABLE = IMPORTJETI(FILENAME, SELECTEDDATASETS) reads only the +% Wavelength and the specific dataset indices you request. +% Example: table = importJeti("20260421_1500_Jeti.csv", [1, 5, 10]); % % Supports: -% 1. CSV: Semicolon-separated with header metadata +% 1. CSV: Auto-detects comma (,) or semicolon (;) delimiters, handles +% various Date/Time formats, and flexible header lengths. % 2. isd: txt-style format with [sections] and tab-separated data -% -% Example: -% table = importJeti("20251212_1606_Jeti.csv"); -% table = importJeti("spectrum_#2.isd"); + +if nargin < 2 + selectedDatasets = []; % Empty means import all +end %% Determine file type and route accordingly [~, ~, ext] = fileparts(filename); if strcmpi(ext, '.csv') - dataTable = importCSV(filename); + dataTable = importCSV(filename, selectedDatasets); elseif strcmpi(ext, '.isd') dataTable = importTXT(filename); else @@ -24,13 +29,27 @@ end end %% ========== CSV IMPORT FUNCTION ========== -function dataTable = importCSV(filename) +function dataTable = importCSV(filename, selectedDatasets) fid = fopen(filename, 'r'); if fid == -1 error('Cannot open file: %s', filename); end +% 1. Auto-detect Delimiter +fileDelimiter = ','; % default +while ~feof(fid) + thisLine = fgetl(fid); + if contains(thisLine, ';') + fileDelimiter = ';'; + break; + elseif contains(thisLine, ',') + fileDelimiter = ','; + break; + end +end +frewind(fid); % Reset back to beginning of file + metadata = struct(); metadata.numDatasets = NaN; metadata.numPoints = NaN; @@ -39,7 +58,9 @@ metadata.Time = {}; lineIdx = 0; dataHeaderLineIdx = NaN; +nHeaderCols = NaN; +% 2. Parse Metadata and find Wavelength header while true thisLine = fgetl(fid); if ~ischar(thisLine) @@ -47,18 +68,20 @@ while true end lineIdx = lineIdx + 1; - % Detect start of data header line - if contains(thisLine, 'Wavelength [nm]') - dataHeaderLineIdx = lineIdx; - break - end - - parts = strsplit(thisLine, ';'); - + parts = strsplit(thisLine, fileDelimiter); if isempty(parts) continue end + % Detect start of data header line (usually 'Wavelength [nm]') + if contains(parts{1}, 'Wavelength', 'IgnoreCase', true) && ... + ~contains(parts{1}, 'Start Wavelength') && ... + ~contains(parts{1}, 'End Wavelength') + dataHeaderLineIdx = lineIdx; + nHeaderCols = numel(parts); + break + end + key = strtrim(parts{1}); switch key @@ -72,47 +95,35 @@ while true end case 'Date' if numel(parts) >= 2 + % Remove empty trailing cells metadata.Date = parts(2:end); + metadata.Date = metadata.Date(~cellfun('isempty', metadata.Date)); end case 'Time' if numel(parts) >= 2 metadata.Time = parts(2:end); + metadata.Time = metadata.Time(~cellfun('isempty', metadata.Time)); end end end if isnan(dataHeaderLineIdx) fclose(fid); - error('Could not find data header line in CSV.'); + error('Could not find Wavelength data header line in CSV.'); end firstDataLineInFile = dataHeaderLineIdx + 1; fclose(fid); -% Derive number of columns: -% If 'Number of datasets' is present: use it. -% Otherwise infer from the header line (new Spec1/Spec2 format).[file:1] -fid = fopen(filename, 'r'); -if fid == -1 - error('Cannot reopen file: %s', filename); -end -headerLine = ''; -for k = 1:dataHeaderLineIdx - headerLine = fgetl(fid); -end -fclose(fid); - -headerParts = strsplit(headerLine, ';'); -nHeaderCols = numel(headerParts); - +% 3. Determine column structures if isnan(metadata.numDatasets) - % New format: first column is wavelength, remaining are datasets + % Infer from header columns (Subtract 1 for Wavelength column) metadata.numDatasets = nHeaderCols - 1; end numCols = 1 + metadata.numDatasets; -% Build dataLines +% Build dataLines bounds if isnan(metadata.numPoints) dataLines = [firstDataLineInFile, Inf]; else @@ -123,9 +134,9 @@ end % Set up Import Options opts = delimitedTextImportOptions("NumVariables", numCols); opts.DataLines = dataLines; -opts.Delimiter = ";"; +opts.Delimiter = fileDelimiter; -% Build variable names with units +% 4. Build Variable Names varNames = cell(1, numCols); varNames{1} = "Wavelength [nm]"; @@ -134,13 +145,9 @@ if ~isempty(metadata.Date) && ~isempty(metadata.Time) for k = 1:nMeta dstr = strtrim(metadata.Date{k}); tstr = strtrim(metadata.Time{k}); - - try - % JETI CSV: e.g. 12/18/2025 and 10:40:14 for 12h clock.[file:1] - dt = datetime([dstr ' ' tstr], ... - 'InputFormat', 'MM/dd/yyyy HH:mm:ss'); - timestr = datestr(dt, 'yyyymmdd_HHMMss'); - catch + + timestr = parseJetiDateTime(dstr, tstr); + if isempty(timestr) timestr = sprintf('dataset%02d', k); end @@ -162,10 +169,48 @@ opts.ExtraColumnsRule = "ignore"; opts.EmptyLineRule = "read"; opts.VariableNamingRule = "preserve"; +% Handle Specific Selected Columns +if ~isempty(selectedDatasets) + colsToKeep = [1, selectedDatasets + 1]; + % Prevent out-of-bounds error + colsToKeep = colsToKeep(colsToKeep <= numCols); + opts.SelectedVariableNames = opts.VariableNames(colsToKeep); +end + +% 5. Read the Table dataTable = readtable(filename, opts); end +%% ========== DATETIME PARSER HELPER ========== +function timestr = parseJetiDateTime(dstr, tstr) +% Tries several Date/Time formats to safely parse standard 24h, 12h(am/pm), +% and regional variations (dd.MM.yyyy vs MM/dd/yyyy). + combinedStr = [dstr ' ' tstr]; + + formatsToTry = { + 'MM/dd/yyyy hh:mm:ssa', ... % US 12-hour (03:05:17pm) + 'dd.MM.yyyy HH:mm:ss', ... % EU 24-hour (12.12.2025 15:54:00) + 'MM/dd/yyyy HH:mm:ss', ... % US 24-hour + 'dd/MM/yyyy HH:mm:ss', ... % EU slash 24-hour + 'yyyy-MM-dd HH:mm:ss' % ISO 8601 + }; + + timestr = ''; + for i = 1:numel(formatsToTry) + try + dt = datetime(combinedStr, 'InputFormat', formatsToTry{i}); + if ~isnat(dt) + timestr = datestr(dt, 'yyyymmdd_HHMMss'); + return; + end + catch + % Format mismatched, try the next one + continue; + end + end +end + %% ========== TXT IMPORT FUNCTION ========== function dataTable = importTXT(filename) @@ -194,13 +239,11 @@ while true end lineIdx = lineIdx + 1; - % Check for data section marker if strcmp(strtrim(thisLine), 'Data') dataStartLineIdx = lineIdx + 1; break end - % Parse key=value pairs if contains(thisLine, '=') parts = strsplit(thisLine, '='); if numel(parts) == 2 @@ -234,13 +277,11 @@ end fclose(fid); -% Set up import options for tab-separated data opts = delimitedTextImportOptions("NumVariables", 2); opts.DataLines = [dataStartLineIdx, Inf]; opts.Delimiter = '\t'; opts.VariableNamingRule = "preserve"; -% Build column names as string array if ~isempty(metadata.XUnit) && ~isempty(metadata.YUnit) varNames = string({ sprintf("Wavelength [%s]", metadata.XUnit) @@ -257,5 +298,4 @@ opts.VariableNames = varNames; opts.VariableTypes = ["double", "double"]; dataTable = readtable(filename, opts); - -end +end \ No newline at end of file diff --git a/utils/exportFigureData.m b/utils/exportFigureData.m index e49e303..460e250 100644 --- a/utils/exportFigureData.m +++ b/utils/exportFigureData.m @@ -21,48 +21,6 @@ function exportFigureData(figHandle) % For non-tiled figures: % __vs__.txt % Each file contains two columns: X-data and Y-data (tab-delimited) -% -% NOTES: -% - Works with line plots (line objects) and scatter plots (scatter objects) -% - Handles both regular axes and tiled layouts -% - Data outside the x-axis scale limits are excluded -% - Lines without legend entries are named as 'line_N' where N is a counter -% - Scatter plots without legend entries are named as 'scatter_N' -% - Axis labels default to 'x' and 'y' if not set -% - Tile titles are included only for tiled layouts -% - If no figure title is set, 'figure' is used as the base name -% -% EXAMPLE: -% % Create a tiled figure with multiple subplots -% figure -% tiledlayout(2, 1, 'TileSpacing', 'compact') -% -% nexttile -% x = 0:0.1:10; -% plot(x, sin(x), 'DisplayName', 'sin(x)') -% hold on -% plot(x, cos(x), 'DisplayName', 'cos(x)') -% title('Sine and Cosine') -% xlabel('Time (s)') -% ylabel('Amplitude') -% legend -% -% nexttile -% plot(x, sin(x).*cos(x), 'DisplayName', 'sin*cos') -% title('Product') -% xlabel('Time (s)') -% ylabel('Value') -% legend -% -% sgtitle('Trigonometric Functions') -% exportFigureData(gcf) -% -% % Creates files like: -% % Trigonometric_Functions_Sine_and_Cosine_Amplitude_vs_Time_(s)_sin(x).txt -% % Trigonometric_Functions_Sine_and_Cosine_Amplitude_vs_Time_(s)_cos(x).txt -% % Trigonometric_Functions_Product_Value_vs_Time_(s)_sin*cos.txt -% -% See also: tiledlayout, nexttile, legend, line, scatter % Handle input: use current figure if none provided if nargin == 0 || isempty(figHandle) @@ -106,45 +64,14 @@ for ii = 1:length(allAxes) continue end - % Get axis titles - xLabel = ax.XLabel.String; - yLabel = ax.YLabel.String; - tileTitle = ax.Title.String; - - % Convert to cell if char, then handle empty cases - if ischar(xLabel), xLabel = cellstr(xLabel); end - if ischar(yLabel), yLabel = cellstr(yLabel); end - if ischar(tileTitle), tileTitle = cellstr(tileTitle); end - - if isempty(xLabel) || (iscell(xLabel) && isempty(xLabel{1})) - xLabel = 'x'; + % Check if this axes uses yyaxis + isYYAxis = isprop(ax, 'YAxis') && numel(ax.YAxis) == 2; + if isYYAxis + sidesToProcess = {'left', 'right'}; else - xLabel = xLabel{1}; + sidesToProcess = {'single'}; end - if isempty(yLabel) || (iscell(yLabel) && isempty(yLabel{1})) - yLabel = 'y'; - else - yLabel = yLabel{1}; - end - - if isempty(tileTitle) || (iscell(tileTitle) && isempty(tileTitle{1})) - tileTitle = ''; - else - tileTitle = tileTitle{1}; - end - - % Sanitize axis labels and tile title - xLabel = sanitizeFilename(xLabel); - yLabel = sanitizeFilename(yLabel); - tileTitle = sanitizeFilename(tileTitle); - - % Get x-axis limits for filtering - xLims = ax.XLim; - - % Get all children of the axes (lines, scatter, etc.) - children = get(ax, 'Children'); - % Build a legend object if it exists (for extracting labels) legendObj = legend(ax); legendLabels = {}; @@ -158,99 +85,156 @@ for ii = 1:length(allAxes) % Initialize a counter for matching legend entries to graphics objects graphicsIndex = 0; - % Loop over all child graphics objects - for jj = 1:length(children) - obj = children(jj); + % Loop through both sides if yyaxis is used, otherwise process normally once + for s = 1:length(sidesToProcess) - % Check if this is a line object - if isa(obj, 'matlab.graphics.chart.primitive.Line') - graphicsIndex = graphicsIndex + 1; - - % Get x and y data - xData = get(obj, 'XData'); - yData = get(obj, 'YData'); - - % Skip if empty - if isempty(xData) || isempty(yData) - continue - end - - % Filter data to x-axis range - mask = (xData >= xLims(1)) & (xData <= xLims(2)); - xData = xData(mask); - yData = yData(mask); - - % Determine label: use legend entry or auto-generate name - if graphicsIndex <= length(legendLabels) && ~isempty(legendLabels{graphicsIndex}) - label = legendLabels{graphicsIndex}; - else - lineCounter = lineCounter + 1; - label = sprintf('line_%d', lineCounter); - end - - % Sanitize label for filename - label = sanitizeFilename(label); - - % Create filename with tile title if present - if ~isempty(tileTitle) - fileName = sprintf('%s_%s_%s_vs_%s_%s.txt', figTitle, tileTitle, yLabel, xLabel, label); - else - fileName = sprintf('%s_%s_vs_%s_%s.txt', figTitle, yLabel, xLabel, label); - end - - % Write data to ASCII file - data = [xData(:), yData(:)]; - writematrix(data, fileName, 'Delimiter', '\t'); - fprintf('Saved: %s\n', fileName); + if isYYAxis + yyaxis(ax, sidesToProcess{s}); % Switch active side to expose its lines and label + end - % Check if this is a scatter object - elseif isa(obj, 'matlab.graphics.chart.primitive.Scatter') - graphicsIndex = graphicsIndex + 1; + % Get axis titles (now correctly tied to the active side) + xLabel = ax.XLabel.String; + yLabel = ax.YLabel.String; + tileTitle = ax.Title.String; + + % Convert to cell if char, then handle empty cases + if ischar(xLabel), xLabel = cellstr(xLabel); end + if ischar(yLabel), yLabel = cellstr(yLabel); end + if ischar(tileTitle), tileTitle = cellstr(tileTitle); end + + if isempty(xLabel) || (iscell(xLabel) && isempty(xLabel{1})) + xLabel = 'x'; + else + xLabel = xLabel{1}; + end + + if isempty(yLabel) || (iscell(yLabel) && isempty(yLabel{1})) + yLabel = 'y'; + else + yLabel = yLabel{1}; + end + + if isempty(tileTitle) || (iscell(tileTitle) && isempty(tileTitle{1})) + tileTitle = ''; + else + tileTitle = tileTitle{1}; + end + + % Sanitize axis labels and tile title + xLabel = sanitizeFilename(xLabel); + yLabel = sanitizeFilename(yLabel); + tileTitle = sanitizeFilename(tileTitle); + + % Get x-axis limits for filtering + xLims = ax.XLim; + + % Get all children of the active side + children = get(ax, 'Children'); + + % Reverse so we extract in creation order to match the legend + if ~isempty(children) + children = flipud(children(:)); + end + + % Loop over all child graphics objects for this side + for jj = 1:length(children) + obj = children(jj); - % Get x and y data - xData = get(obj, 'XData'); - yData = get(obj, 'YData'); + % Check if this is a line object + if isa(obj, 'matlab.graphics.chart.primitive.Line') + graphicsIndex = graphicsIndex + 1; + + % Get x and y data + xData = get(obj, 'XData'); + yData = get(obj, 'YData'); + + % Skip if empty + if isempty(xData) || isempty(yData) + continue + end + + % Filter data to x-axis range + mask = (xData >= xLims(1)) & (xData <= xLims(2)); + xData = xData(mask); + yData = yData(mask); + + % Determine label: prefer internal DisplayName to fix yyaxis mismatch + label = ''; + if isprop(obj, 'DisplayName') && ~isempty(obj.DisplayName) + label = obj.DisplayName; + elseif graphicsIndex <= length(legendLabels) && ~isempty(legendLabels{graphicsIndex}) + label = legendLabels{graphicsIndex}; + else + lineCounter = lineCounter + 1; + label = sprintf('line_%d', lineCounter); + end + + % Sanitize label for filename + label = sanitizeFilename(label); + + % Create filename with tile title if present + if ~isempty(tileTitle) + fileName = sprintf('%s_%s_%s_vs_%s_%s.txt', figTitle, tileTitle, yLabel, xLabel, label); + else + fileName = sprintf('%s_%s_vs_%s_%s.txt', figTitle, yLabel, xLabel, label); + end + + % Write data to ASCII file + data = [xData(:), yData(:)]; + writematrix(data, fileName, 'Delimiter', '\t'); + fprintf('Saved: %s\n', fileName); - % Skip if empty - if isempty(xData) || isempty(yData) - continue + % Check if this is a scatter object + elseif isa(obj, 'matlab.graphics.chart.primitive.Scatter') + graphicsIndex = graphicsIndex + 1; + + % Get x and y data + xData = get(obj, 'XData'); + yData = get(obj, 'YData'); + + % Skip if empty + if isempty(xData) || isempty(yData) + continue + end + + % Ensure row vectors for indexing + xData = xData(:)'; + yData = yData(:)'; + + % Filter data to x-axis range + mask = (xData >= xLims(1)) & (xData <= xLims(2)); + xData = xData(mask); + yData = yData(mask); + + % Determine label + label = ''; + if isprop(obj, 'DisplayName') && ~isempty(obj.DisplayName) + label = obj.DisplayName; + elseif graphicsIndex <= length(legendLabels) && ~isempty(legendLabels{graphicsIndex}) + label = legendLabels{graphicsIndex}; + else + scatterCounter = scatterCounter + 1; + label = sprintf('scatter_%d', scatterCounter); + end + + % Sanitize label for filename + label = sanitizeFilename(label); + + % Create filename with tile title if present + if ~isempty(tileTitle) + fileName = sprintf('%s_%s_%s_vs_%s_%s.txt', figTitle, tileTitle, yLabel, xLabel, label); + else + fileName = sprintf('%s_%s_vs_%s_%s.txt', figTitle, yLabel, xLabel, label); + end + + % Write data to ASCII file + data = [xData(:), yData(:)]; + writematrix(data, fileName, 'Delimiter', '\t'); + fprintf('Saved: %s\n', fileName); end - - % Ensure row vectors for indexing - xData = xData(:)'; - yData = yData(:)'; - - % Filter data to x-axis range - mask = (xData >= xLims(1)) & (xData <= xLims(2)); - xData = xData(mask); - yData = yData(mask); - - % Determine label: use legend entry or auto-generate name - if graphicsIndex <= length(legendLabels) && ~isempty(legendLabels{graphicsIndex}) - label = legendLabels{graphicsIndex}; - else - scatterCounter = scatterCounter + 1; - label = sprintf('scatter_%d', scatterCounter); - end - - % Sanitize label for filename - label = sanitizeFilename(label); - - % Create filename with tile title if present - if ~isempty(tileTitle) - fileName = sprintf('%s_%s_%s_vs_%s_%s.txt', figTitle, tileTitle, yLabel, xLabel, label); - else - fileName = sprintf('%s_%s_vs_%s_%s.txt', figTitle, yLabel, xLabel, label); - end - - % Write data to ASCII file - data = [xData(:), yData(:)]; - writematrix(data, fileName, 'Delimiter', '\t'); - fprintf('Saved: %s\n', fileName); end end end - fprintf('Data export complete.\n'); end @@ -262,7 +246,6 @@ function cleanStr = sanitizeFilename(str) % % Replaces spaces, slashes, backslashes, colons, asterisks, parentheses, % and other problematic characters with underscores. - cleanStr = str; cleanStr = strrep(cleanStr, ' ', '_'); cleanStr = strrep(cleanStr, '/', '_'); @@ -277,5 +260,4 @@ cleanStr = strrep(cleanStr, '|', '_'); cleanStr = strrep(cleanStr, '(', '_'); cleanStr = strrep(cleanStr, ')', '_'); cleanStr = strrep(cleanStr, '.', '_'); % Optional: replace dots - -end +end \ No newline at end of file