From 5b9c7608d7e7ce14ed22387e9dcb1ab4bd4f5773 Mon Sep 17 00:00:00 2001 From: ThorBarRa Date: Fri, 21 Aug 2026 10:18:39 +0200 Subject: [PATCH] Data import now works with 2016 data, too. Data tracking added to md file. --- beamtime/IBWread.m | 188 ++++++ beamtime/Napp_RH_p.m | 33 +- beamtime/Napp_T_ice.m | 63 ++ beamtime/Napp_p_RH.m | 18 +- beamtime/Napp_p_ice.m | 9 +- beamtime/QuickAndDirtyNexafsImport.m | 136 +++++ beamtime/WriteH5.m | 134 +++++ beamtime/brewermap.m | 473 +++++++++++++++ beamtime/brewermap_nodes.m | 63 ++ beamtime/brewermap_view.m | 408 +++++++++++++ beamtime/distFig.m | 831 +++++++++++++++++++++++++++ beamtime/exportFigureData.m | 263 +++++++++ beamtime/getPsiColors.m | 24 + beamtime/importLabViewIssfile.m | 107 ++++ beamtime/nappFileList.m | 181 +++--- beamtime/nappImage.m | 54 ++ beamtime/nappSaveDataTable.m | 48 ++ beamtime/outerjoinRowToTable.m | 85 +++ beamtime/plotExportedData.m | 151 +++++ beamtime/plotRgaData.m | 79 ++- beamtime/preset_colormap.m | 75 +++ beamtime/readIBWbinheader.m | 52 ++ beamtime/readIBWheaders.m | 250 ++++++++ beamtime/read_hdf5_as_table.m | 130 +++++ beamtime/readme.md | 58 ++ beamtime/rgaImportData.m | 97 ++++ beamtime/save_table_as_hdf5.m | 155 +++++ 27 files changed, 4047 insertions(+), 118 deletions(-) create mode 100644 beamtime/IBWread.m create mode 100644 beamtime/Napp_T_ice.m create mode 100644 beamtime/QuickAndDirtyNexafsImport.m create mode 100644 beamtime/WriteH5.m create mode 100644 beamtime/brewermap.m create mode 100644 beamtime/brewermap_nodes.m create mode 100644 beamtime/brewermap_view.m create mode 100644 beamtime/distFig.m create mode 100644 beamtime/exportFigureData.m create mode 100644 beamtime/getPsiColors.m create mode 100644 beamtime/importLabViewIssfile.m create mode 100644 beamtime/nappImage.m create mode 100644 beamtime/nappSaveDataTable.m create mode 100644 beamtime/outerjoinRowToTable.m create mode 100644 beamtime/plotExportedData.m create mode 100644 beamtime/preset_colormap.m create mode 100644 beamtime/readIBWbinheader.m create mode 100644 beamtime/readIBWheaders.m create mode 100644 beamtime/read_hdf5_as_table.m create mode 100644 beamtime/rgaImportData.m create mode 100644 beamtime/save_table_as_hdf5.m diff --git a/beamtime/IBWread.m b/beamtime/IBWread.m new file mode 100644 index 0000000..c45b66f --- /dev/null +++ b/beamtime/IBWread.m @@ -0,0 +1,188 @@ +function D = IBWread(FN); +% IBWread - read Igor wave from IBW file +% D = IBWread('Foo.ibw') reads Igor file Foo.ibw into struct D. + +if exist('fullfilename', 'file'), FFN = fullfilename(FN, cd, '.ibw'); +else, FFN = FN; +end + +% read headers +[D.binHeader, D.waveHeader, FFN] = readIBWheaders(FN); +fid = fopen(FFN,'r'); +fid = fopen(FFN,'r'); +% For numeric waves, the type field is interpreted bitwise. One of the following bits, as represented by symbols defined in IgorBin.h, will be set: +% +% % #define NT_CMPLX 1 % Complex numbers. +% % #define NT_FP32 2 % 32 bit fp numbers. +% % #define NT_FP64 4 % 64 bit fp numbers. +% % #define NT_I8 8 % 8 bit signed integer. Requires Igor Pro 2.0 or later. +% % #define NT_I16 0x10 % 16 bit integer numbers. Requires Igor Pro 2.0 or later. +% % #define NT_I32 0x20 % 32 bit integer numbers. Requires Igor Pro 2.0 or later. +% % #define NT_UNSIGNED 0x40 % Makes above signed integers unsigned. Requires Igor Pro 3.0 or later. +% NT_FP64 64-bit floating point (double) +% NT_FP32 32-bit floating point (float) +% NT_I32 32 bit integer (long) +% NT_16 16 bit integer (short) +% NT_I8 8 bit integer (char) +datatype = D.waveHeader.type; +CMPLX = rem(datatype,2); +if CMPLX, datatype = datatype-1; end +switch datatype, + case 0, prec = '*char'; + case 2, prec = 'single'; + case 4, prec = 'double'; + case 8, prec = 'int8'; + case 16, prec = 'int16'; + case 32, prec = 'int32'; + case 64+8, prec = 'uint8'; + case 64+16, prec = 'uint16'; + case 64+32, prec = 'uint32'; + otherwise, error('Invalid numerical datatype.'); +end + +D.bname = D.waveHeader.bname; +% The date/time is store as seconds since midnight, January 1, 1904. +D.creationDate = local_igorTime2vec(D.waveHeader.creationDate); +D.modDate = local_igorTime2vec(D.waveHeader.creationDate); +switch D.binHeader.version, + case 2, + % The version 2 file has the following general layout. + % BinHeader2 structure: 16 bytes + % WaveHeader2 structure excluding wData field; 110 bytes + % Wave data: Variable size + D.Nsam = D.waveHeader.npnts; + D.Ndim = 1; + % X value for point p = hsA*p + hsB + D.dx = D.waveHeader.hsA; + D.x0 = D.waveHeader.hsB; + D.x1 = D.x0+D.Nsam*D.dx; + fseek(fid, 16+110, 'bof'); + NN = D.Nsam*(1+double(CMPLX)); % if complex, read twice as many numbers + D.y = fread(fid, NN, prec); + if CMPLX, D.y = D.y(1:2:end) + i*D.y(2:2:end); end; % Re & Im values are interleaved + case 5, + % The version 5 file has the following general layout. + % BinHeader5 structure 64 bytes + % WaveHeader5 structure excluding wData field 320 bytes + % Wave data Variable size + % Optional wave dependency formula Variable size + % Optional wave note data Variable size + D.Nsam = D.waveHeader.npnts; + D.Ndim = sum(D.waveHeader.nDim>0); + % X offset & spacing (documentation is obscure) + D.dx = D.waveHeader.sfA(1:D.Ndim); + D.x0 = D.waveHeader.sfB(1:D.Ndim); + D.x1 = nan; % D.x0+D.Nsam*D.dx; + fseek(fid, 64+320, 'bof'); + NN = D.Nsam*(1+double(CMPLX)); % if complex, read twice as many numbers + if isequal(0,datatype), % text data + % Number of bytes of wave data = wfmSize - (sizeof(WaveHeader5) - 4) + NN = D.binHeader.wfmSize-320; + D.y = fread(fid, NN, prec)'; + else, % numerical data + D.y = fread(fid, NN, prec); + end + if CMPLX, D.y = D.y(1:2:end) + i*D.y(2:2:end); end; % Re & Im values are interleaved + if ~isempty(D.y) && D.Ndim>1, + D.y = reshape(D.y, D.waveHeader.nDim(1:D.Ndim)); + end +end +D.WaveNotes = local_readNotes(fid, D.binHeader); +if isequal(0,datatype), % partition text data into lines using string indices stored @ eof + D = local_partition_text(fid, D); +end +fseek(fid,0,'eof'); +D.fileSize = ftell(fid); +fclose(fid); + + +%=============================== +function DV=local_igorTime2vec(it); +% igor time -> date vector a la Matlab +% The date/time is store as seconds since midnight, January 1, 1904. +Nsec_4year = 126230400; +idate_1988 = 21*uint32(Nsec_4year); % # seconds between 1-jan-1904 & 1-jan-1988 +it = double(it-idate_1988); % smaller numbers-> use ordinary numbers ("doubles") +M4 = floor(it/Nsec_4year); +YR = double(1988+4*M4); +it = double(it-M4*Nsec_4year); +last_it = it; +while 1, % subtract years as long as they fit + s = etime([YR+1 1 1 0 0 0], [YR 1 1 0 0 0]); + if it T = A1 / (log10(p[Pa]) - B1) + % p_current is in mbar, so p[Pa] = p_current ./ (1E-5.*1E3) + T(i) = A_1 ./ (log10(p_current(i)./(1E-5.*1E3)) - B_1); % K + plot(p_current(i),T(i),'x','MarkerSize',25,'LineWidth',3); + text(p_current(i),T(i),[' <- ' num2str(p_current(i)) ' mbar'],'FontSize',12); + fprintf('The ice temperature at %.2f mbar is %.1f K (%.3f dC) \n',p_current(i),T(i),T(i)-273.15) +end +end diff --git a/beamtime/Napp_p_RH.m b/beamtime/Napp_p_RH.m index 7b2d196..30579b4 100644 --- a/beamtime/Napp_p_RH.m +++ b/beamtime/Napp_p_RH.m @@ -10,22 +10,30 @@ function Napp_p_RH(T_current,RH) % % % *input* -% T_current in K: temperature -% RH in %: relative humidity (RH_{water}) +% T_current in K: temperature; (format: scalar, or row/column vector) +% RH in %: relative humidity (RH_{water}); same shape/length as T_current % % *output* % partial pressure of water in mbar +%% input validation +if ~isvector(T_current) || ~isvector(RH) + error('Napp_p_RH:InvalidInput','T_current and RH must be scalars or vectors (row or column).'); +end +if numel(T_current) ~= numel(RH) + error('Napp_p_RH:InvalidInput','T_current and RH must have the same number of elements.'); +end + %% % % *water*: % NIST webbook: Bridgeman and Aldrich, 1964 give parametrisation from 273 % to 303 K in bar. log10(p) = A - B/ (T+C). -A = 5.40221; +A = 5.40221; B = 1838.675; -C = -31.737; +C = -31.737; -p_w = 10.^(A - B./(T_current+C)) .* 1E3; % mbar +p_w = 10.^(A - B./(T_current+C)) .* 1E3; % mbar for i = 1:length(T_current) diff --git a/beamtime/Napp_p_ice.m b/beamtime/Napp_p_ice.m index 0b420c8..11d1536 100644 --- a/beamtime/Napp_p_ice.m +++ b/beamtime/Napp_p_ice.m @@ -7,10 +7,15 @@ function p = Napp_p_ice(T_current) % * p = Napp_p_ice([253.15, 263.15, 273.15]) % % *input* -% T_current in K: temperature of ice; (format: either single numbers or array) +% T_current in K: temperature of ice; (format: scalar, or row/column vector) % % *output* -% vapour pressure of ice in mbar +% vapour pressure of ice in mbar (same shape as T_current) + +%% input validation +if ~isvector(T_current) + error('Napp_p_ice:InvalidInput','T_current must be a scalar or a vector (row or column).'); +end %% Water vapour pressure above the ice and in pure liquid water. diff --git a/beamtime/QuickAndDirtyNexafsImport.m b/beamtime/QuickAndDirtyNexafsImport.m new file mode 100644 index 0000000..7d1988e --- /dev/null +++ b/beamtime/QuickAndDirtyNexafsImport.m @@ -0,0 +1,136 @@ +function ListFiles = QuickAndDirtyNexafsImport(DataTable) + + + +%% QuickAndDirtyNexafsImport +% +[filePick,pathPick] = uigetfile({'*.h5'},... + 'Select One or More Files',... + 'MultiSelect', 'on'); +% make lists for Nexafs +if ischar(filePick) + filePick = {filePick}; +end + +for ii = 1:size(filePick,2) + TmpFileList{2}(ii,1) = dir([pathPick filePick{ii}]); +end + + +for ii = 1:size(filePick,2) + try + TmpImage= h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/spectrum'); + %else + catch + try + TmpImage = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/scienta spectrum'); + catch + TmpData = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/detector/data'); + %convert 3 dim matrix to 2 dim matrix + for i = 1:size(TmpData,3)%iterations + TmpImage(:,i) = TmpData(:,1,(i)); %#ok<*SAGROW> + end + + + + end + end + % import Photon Energy (cols) + try %general try to ensure that script does not stop upon errors. + try + TmpXrayEkin = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/energy'); % X-ray excitation energy; Photon energy in eV + TmpXrayEkin(TmpXrayEkin==0)=[]; + catch + TmpXrayEkin = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/PhotonEnergy'); % X-ray excitation energy; Photon energy in eV + TmpXrayEkin(TmpXrayEkin==0)=[]; + end + % Make unique + [TmpFileList{2}(ii).xRayEkin_eV, ind] = unique(TmpXrayEkin); + TmpFileList{2}(ii).image = TmpImage(:,ind); + % NEXAFS pass energy + try + tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/PassEnergy'); % in eV + TmpFileList{2}(ii).scientaPassEnergy_eV = str2double(tmp(1)); + catch + tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/PassEnergy'); % in eV + TmpFileList{2}(ii).scientaPassEnergy_eV = str2double(tmp(1)); + end + % Acquisition mode + try + TmpFileList{2}(ii).scientaAcquisitionMode = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/AcquisitionMode'); + catch + TmpFileList{2}(ii).scientaAcquisitionMode = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/AcquireMode'); + end + if strcmp(TmpFileList{2}(ii).scientaAcquisitionMode,'Fixed') + % NEXAFS kinetic energy window - fixed mode + try + tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/ScientaCenterEnergy'); % in eV + catch + tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/CenterEnergy'); % in eV + end + TmpFileList{2}(ii).scientaEkinRange_eV = [tmp(1)-TmpFileList{2}(ii).scientaPassEnergy_eV/20 tmp(1)+TmpFileList{2}(ii).scientaPassEnergy_eV/20]; + TmpFileList{2}(ii).scientaEkinStep_eV = NaN; + 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 + tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/ScientaLowEnergy'); % in eV + tmp2 = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/ScientaHighEnergy'); + catch + tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/LowEnergy'); % in eV + tmp2 = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/HighEnergy'); + end + TmpFileList{2}(ii).scientaEkinRange_eV = [tmp(1) tmp2(1)]; + try + TmpFileList{2}(ii).scientaEkinStep_eV = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/StepSize'); + catch + TmpFileList{2}(ii).scientaEkinStep_eV = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/StepSize'); + 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 + catch + end +end +%% Merge lists into one +% + +% get index of non-empty structures +tf =~cellfun(@isempty,TmpFileList); + +% merge the non-empty list structures into one structure +ListFiles = cat(1,TmpFileList{tf}); + +% remove lines with empty images +for ii = 1:size(ListFiles,1) + tmpLogic(ii) = isempty(ListFiles(ii).image); + noncells = tmpLogic; +end +ListFiles(noncells) = []; + +clearvars -except ListFiles DataTable + + +%% + + + +%% Sort by date +% rename date fields and delete those fields not needed +[ListFiles.lastModifiedDatestr] = ListFiles.date; +[ListFiles.lastModifiedDatenum] = ListFiles.datenum; + +% First, we need to convert into a table, because structures can not be sorted: +ListFiles = struct2table(ListFiles,'AsArray',true); +% Then, we sort the table based on the time (6th column, datenum) +ListFiles = sortrows(ListFiles,6); +% Then, remove un-needed columns +ListFiles = removevars(ListFiles,{'bytes','isdir','date','datenum'}); + +% merge tables (append imported data to previous table) +if exist("DataTable",'var') + ListFiles = outerjoinRowToTable(ListFiles,DataTable); +end +% Then, we sort the table based on the time (datenum) +ListFiles = sortrows(ListFiles,'lastModifiedDatenum'); +end \ No newline at end of file diff --git a/beamtime/WriteH5.m b/beamtime/WriteH5.m new file mode 100644 index 0000000..5132e64 --- /dev/null +++ b/beamtime/WriteH5.m @@ -0,0 +1,134 @@ +%% Develpment of hdf export and import +% +% from table to our instrument hdf structure +%% Todo +% +% * ensure logging of processing in NEXAFS and XPS to select data, make it +% usfull for string operations (no cells) +% * How to include units and definitions +% * Next, make this work for averageded files +%% +clc +% % add some SES data +%tmpFileImport = load('../beamtimes/20220828BeamTimeIceNappIss/FileList.mat'); +% FileList = outerjoinRowToTable(FileList, tmpFileImport.FileList); +clearvars tmp* +% select only data that has been used, +[tmpn1, ~] = cellfun(@size,FileList.spectrum_counts); % spectra have been generated +[tmpn2, ~] = cellfun(@size,FileList.spectrum_countsNorm); +[tmpn3, ~] = cellfun(@size,FileList.spectrum_countsPerSecond); +tmpDataSelection = contains(FileList.sample,lettersPattern) ... % we have added sample notes, so this is data + & ~contains(FileList.sample, '(bad data)') & ~contains(FileList.sample, '(no notes)') ... + & (tmpn1 > 1 | tmpn2 > 1 | tmpn3 > 1); % has actually been proceesed. + + + +tmpUserName = 'ThorstenBR'; +tmpFileList = FileList(tmpDataSelection,:); +% get table variables that are data +tmpMissingVariable = ~ismember(tmpFileList.Properties.VariableNames,... + [{'image'}; {'spectrum_counts'}; {'spectrum_countsPerSecond'}; {'spectrum_countsNorm'}; {'xRayEkin_eV'}; {'scientaEkin_eV'}; {'imageRoh'}; {'spectrum_countsPerSecondRoh'};{'beamlineInt'}]); +tmpVariablesToBeAdded = tmpFileList.Properties.VariableNames(tmpMissingVariable); + +for ij = 1:size(tmpFileList,1) + % get file name and "instruement" name + tmpPathParts = split(tmpFileList(ij,:).folder,"/"); + tmpFileName = [tmpPathParts{end-1} tmpUserName '.h5']; + tmpTopGroupName = tmpPathParts{end}; + % write data + h5create(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/image'],... + size(tmpFileList(ij,:).image{:})); + h5write(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/image'],... + tmpFileList(ij,:).image{:}) + if %XPS + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/image'],'label','electron emission intensity'); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/image'],'units','XPS (y,x): counts per second vs. electron volt; NEXAFS (y,x,z): counts vs. X-ray photon kinetic energy vs. electron kinetic energy'); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/image'],'definition','X-ray excited electron emission'); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/image'],'comment','XPS: intensity vs. electron kinetic energy for each sequence iteration; NEXAFS: intensity vs. x-ray photon kinetic energy (non-linear scaling) vs. electron kinetic energy'); + else %NEXAFS + + end + + try + h5create(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_counts'],... + size(tmpFileList(ij,:).spectrum_counts{:})); + h5write(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_counts'],... + tmpFileList(ij,:).spectrum_counts{:}) + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_counts'],'label','electron emission intensity'); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_counts'],'units','arb. unit'); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_counts'],'definition','X-ray excited electron emission'); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_counts'],'comment',' '); + catch + end + try + h5create(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsPerSecond'],... + size(tmpFileList(ij,:).spectrum_countsPerSecond{:})); + h5write(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsPerSecond'],... + tmpFileList(ij,:).spectrum_countsPerSecond{:}) + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsPerSecond'],'label',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsPerSecond'],'units',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsPerSecond'],'definition',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsPerSecond'],'comment',' '); + catch + end + try + h5create(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsNorm'],... + size(tmpFileList(ij,:).spectrum_countsNorm{:})); + h5write(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsNorm'],... + tmpFileList(ij,:).spectrum_countsNorm{:}) + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsNorm'],'label',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsNorm'],'units',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsNorm'],'definition',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/spectrum_countsNorm'],'comment',' '); + catch + end + try + h5create(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/beamlineInt'],... + size(tmpFileList(ij,:).beamlineInt{:})); + h5write(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/beamlineInt'],... + tmpFileList(ij,:).beamlineInt{:}) + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/beamlineInt'],'label',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/beamlineInt'],'units',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/beamlineInt'],'definition',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/beamlineInt'],'comment',' '); + catch + end + h5create(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/xRayEkin_eV'],... + size(tmpFileList(ij,:).xRayEkin_eV{:})); + h5write(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/xRayEkin_eV'],... + tmpFileList(ij,:).xRayEkin_eV{:}) + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/xRayEkin_eV'],'label',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/xRayEkin_eV'],'units',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/xRayEkin_eV'],'definition',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/xRayEkin_eV'],'comment',' '); + + h5create(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/scientaEkin_eV'],... + size(tmpFileList(ij,:).scientaEkin_eV{:})); + h5write(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/scientaEkin_eV'],... + tmpFileList(ij,:).scientaEkin_eV{:}) + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/scientaEkin_eV'],'label',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/scientaEkin_eV'],'units',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/scientaEkin_eV'],'definition',' '); + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:} '/scientaEkin_eV'],'comment',' '); + % add attributes + tmpCells= []; % create empty array + for ii = 1:size(tmpVariablesToBeAdded,2) + + if matches(class(tmpFileList.(tmpVariablesToBeAdded{ii})),'cell') + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:}],tmpVariablesToBeAdded{ii},tmpFileList(ij,:).(tmpVariablesToBeAdded{ii}){:}); + tmpCells = [tmpCells,', ' tmpVariablesToBeAdded{ii}]; + else + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:}],tmpVariablesToBeAdded{ii},tmpFileList(ij,:).(tmpVariablesToBeAdded{ii})); + end + % write variables that were cells to a list; so upon import the + % variable type can be set again. + h5writeatt(tmpFileName,['/' tmpTopGroupName '/' tmpFileList(ij,:).name{:}],'matlabVariablesThatAreCells',tmpCells); + + end + clearvars tmpCells + + +end +disp('*************************************************') +disp('* Voila: HDF file(s) written *') +disp('*************************************************') diff --git a/beamtime/brewermap.m b/beamtime/brewermap.m new file mode 100644 index 0000000..f5797a1 --- /dev/null +++ b/beamtime/brewermap.m @@ -0,0 +1,473 @@ +function [map,num,typ,scheme] = brewermap(N,scheme) +% The complete selection of ColorBrewer colorschemes/palettes (RGB colormaps). +% +% Returns an RGB colormap from Cynthia Brewer's ColorBrewer 2.0 palettes, +% intended for mapping and plots with attractive, distinguishable colors. +% +%%% Basic Syntax %%% +% +% brewermap() % print summary +% map = brewermap(N,scheme) +% +%%% Preset Syntax %%% +% +% old = brewermap(scheme) +% map = brewermap() +% map = brewermap(N) +% +% [...,num,typ] = brewermap(...) +% +%% Colorschemes %% +% +% This product includes color specifications and designs developed by Cynthia Brewer. +% See the ColorBrewer website for further information about each colorscheme, +% colour-blind suitability, licensing, and citations: http://colorbrewer.org/ +% Each colorscheme is defined by a set of hand-picked RGB values (nodes). +% +% To reverse the colormap sequence prefix the colorscheme name with '-'. +% +% Diverging | Qualitative | Sequential +% ----------|-------------|------------------ +% BrBG | Accent | Blues PuBuGn +% PiYG | Dark2 | BuGn PuRd +% PRGn | Paired | BuPu Purples +% PuOr | Pastel1 | GnBu RdPu +% RdBu | Pastel2 | Greens Reds +% RdGy | Set1 | Greys YlGn +% RdYlBu | Set2 | OrRd YlGnBu +% RdYlGn | Set3 | Oranges YlOrBr +% Spectral | | PuBu YlOrRd +% +% If is greater than the requested colorscheme's defining nodes then: +% - Diverging and Sequential colorschemes are interpolated. +% - Qualitative colorschemes repeat the nodes (i.e. just like LINES does). +% Else: +% - Exact values from the ColorBrewer colorschemes are returned. +% +%% Examples %% +% +%%% New colors for the COLORMAP example %%% +% +% >> S = load('spine'); +% >> image(S.X) +% >> colormap(brewermap([],"YlGnBu")) +% +%%% New colors for the SURF example %%% +% +% >> [X,Y,Z] = peaks(30); +% >> surfc(X,Y,Z) +% >> colormap(brewermap([],'RdYlGn')) +% >> axis([-3,3,-3,3,-10,5]) +% +%%% Plot a colorscheme's RGB values %%% +% +% >> rgbplot(brewermap(NaN, '+Blues')) % standard +% >> rgbplot(brewermap(NaN, '-Blues')) % reversed +% +%%% View information about a colorscheme %%% +% +% >> [~,num,typ] = brewermap(NaN,'Paired') +% num = 12 +% typ = 'Qualitative' +% +%%% Multi-line plot using matrices %%% +% +% >> N = 6; +% >> axes('ColorOrder',brewermap(N,'Pastel2'),'NextPlot','replacechildren') +% >> X = linspace(0,pi*3,1000); +% >> Y = bsxfun(@(x,n)n*sin(x+2*n*pi/N), X(:), 1:N); +% >> plot(X,Y, 'linewidth',4) +% +%%% Multi-line plot in a loop %%% +% +% set(0,'DefaultAxesColorOrder',brewermap(NaN,'Accent')) +% N = 6; +% X = linspace(0,pi*3,1000); +% Y = bsxfun(@(x,n)n*sin(x+2*n*pi/N), X(:), 1:N); +% for n = 1:N +% plot(X(:),Y(:,n), 'linewidth',4); +% hold all +% end +% +%% Input Arguments (**=default) %% +% +% N = NumericScalar, N>=0, an integer to specify the colormap length. +% = []**, map has the same length as MATLAB's inbuilt colormap functions. +% = NaN, map is exactly the defining RGB nodes (useful for line ColorOrder). +% scheme = CharRowVector or StringScalar, a ColorBrewer colorscheme name. +% +%% Output Arguments %% +% +% map = NumericMatrix, size Nx3, a colormap of RGB values between 0 and 1. +% num = NumericVector, the number of nodes defining the ColorBrewer colorscheme. +% typ = CharRowVector, the colorscheme type: 'Diverging'/'Qualitative'/'Sequential'. +% +%% Dependencies %% +% +% * MATLAB R2009a or later. +% +% See also BREWERMAP_NODES BREWERMAP_VIEW PRESET_COLORMAP CUBEHELIX MAXDISTCOLOR +% LBMAP PARULA LINES RGBPLOT COLORMAP COLORBAR PLOT PLOT3 AXES SET CONTOURF +persistent bmc scm txt +% +if isempty(bmc) + bmc = bmStruct(); +end +% +%% Input Wrangling %% +% +if nargin==0 + N = []; +end +% +err = 'First input must be a real positive scalar numeric or [] or NaN.'; +% +if nargout==0 && nargin==0 + hdr = { 'Type'; 'Scheme'; 'Nodes'}; + tsn = [{bmc.typ};{bmc.str};{bmc.num}]; + fprintf('%-12s %-9s %s\n',hdr{:}); + fprintf('%-12s %-9s %u\n',tsn{:}); + return +elseif isnumeric(N) + if isequal(N,[]) + % Default N is the same as MATLAB colormaps: + N = cmDefaultN(); + else + assert(isscalar(N),... + 'SC:brewermap:N:NotScalarNumeric',err) + assert(isnan(N) || isreal(N)&&isfinite(N)&&fix(N)==N&&N>=0,... + 'SC:brewermap:N:NotWholeRealNotNaN',err) + N = double(N); + end + if nargin<2 + assert(~isempty(scm),... + 'SC:colorbrewer:scheme:NotPreset',... + 'Colorscheme must be preset before this call: BREWERMAP(SCHEME)') + scheme = scm; + else + scheme = bm1s2c(scheme); + assert(ischar(scheme)&&ndims(scheme)==2&&size(scheme,1)==1,... + 'SC:brewermap:scheme:NotText',... + 'Input must be a character vector or a string scalar.') %#ok + end +else % preset + assert(nargin<2,... + 'SC:brewermap:WrongArgumentNumber',... + 'If calling with two inputs then the 1st input must be numeric.') + scheme = bm1s2c(N); + assert(ischar(scheme)&&ndims(scheme)==2&&size(scheme,1)==1,... + 'SC:brewermap:N:NotText',... + 'To preset: the scheme must be a character vector or a string scalar.') %#ok + if strcmpi(scheme,'list') + map = {bmc.str}; + num = [bmc.num]; + typ = {bmc.typ}; + return + end +end +% +isr = strncmp(scheme,'-',1) || strncmp(scheme,'*',1); +isd = strncmp(scheme,'+',1) || isr; % direction +ids = strcmpi(scheme(1+isd:end),{bmc.str}); +assert(any(ids),... + 'SC:brewermap:scheme:UnknownColorscheme',... + 'Unknown colorscheme name: "%s"',scheme) +% +num = bmc(ids).num; +typ = bmc(ids).typ; +% +if ~isnumeric(N) % preset + map = txt; + txt = N; + scm = scheme; + return +elseif N==0 + map = nan(0,3); + return +elseif isnan(N) + N = num; +end +% +% Downsample: +[idx,itp] = bmIndex(N,num,typ); +% +map = bmc(ids).rgb(idx,:)/255; +% +% Interpolate: +if itp + % + % Extrapolate a small amount beyond the end nodes: + %ido = linspace(0,num+1,N+2); + %ido = ido(2:end-1); + % Interpolation completely within the end nodes: + ido = linspace(1,num,N); + % + switch typ + case 'Diverging' + mid = ceil(num/2); + idl = 1:mid; + idr = mid:num; + map = [... + interp1(idl,map(idl,:),ido(ido<=mid),'pchip');... + interp1(idr,map(idr,:),ido(ido>mid),'pchip')]; + case 'Sequential' + map = interp1(1:num,map,ido,'pchip'); + otherwise + error('SC:brewermap:Interp:UnknownType','Unknown colorscheme type.') + end + % +end +% +% Limit output range: +map = max(0,min(1,map)); +% +% Reverse row order: +if isr + map = map(end:-1:1,:); +end +% +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%brewermap +function N = cmDefaultN() +% Get the colormap size from the current figure or default colormap. +try + F = get(groot,'CurrentFigure'); +catch %#ok pre HG2 + N = size(get(gcf,'colormap'),1); + return +end +if isempty(F) + N = size(get(groot,'DefaultFigureColormap'),1); +else + N = size(F.Colormap,1); +end +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%cmDefaultN +function arr = bm1s2c(arr) +% If scalar string then extract the character vector, otherwise data is unchanged. +if isa(arr,'string') && isscalar(arr) + arr = arr{1}; +end +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%bm1s2c +function [idx,itp] = bmIndex(N,num,typ) +% Ensure exactly the same colors as the online ColorBrewer colorschemes. +% +switch typ + case 'Qualitative' + itp = false; + idx = 1+mod(0:N-1,num); + case 'Diverging' + itp = N>num; + switch N + case 1 % extrapolated + idx = 8; + case 2 % extrapolated + idx = [4,12]; + case 3 + idx = [5,8,11]; + case 4 + idx = [3,6,10,13]; + case 5 + idx = [3,6,8,10,13]; + case 6 + idx = [2,5,7,9,11,14]; + case 7 + idx = [2,5,7,8,9,11,14]; + case 8 + idx = [2,4,6,7,9,10,12,14]; + case 9 + idx = [2,4,6,7,8,9,10,12,14]; + case 10 + idx = [1,2,4,6,7,9,10,12,14,15]; + otherwise + idx = [1,2,4,6,7,8,9,10,12,14,15]; + end + case 'Sequential' + itp = N>num; + switch N + case 1 % extrapolated + idx = 6; + case 2 % extrapolated + idx = [4,8]; + case 3 + idx = [3,6,9]; + case 4 + idx = [2,5,7,10]; + case 5 + idx = [2,5,7,9,11]; + case 6 + idx = [2,4,6,7,9,11]; + case 7 + idx = [2,4,6,7,8,10,12]; + case 8 + idx = [1,3,4,6,7,8,10,12]; + otherwise + idx = [1,3,4,6,7,8,10,11,13]; + end + otherwise + error('SC:brewermap:Index:UnknownType','Unknown colorscheme type.') +end +% +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%bmIndex +function bmc = bmStruct() +% Return a structure of all colorschemes: name, scheme type, RGB values, number of nodes. +% Order: sorted first by , then case-insensitive sorted by . +bmc(35).str = 'YlOrRd'; +bmc(35).typ = 'Sequential'; +bmc(35).rgb = [255,255,204;255,255,178;255,237,160;254,217,118;254,204,92;254,178,76;253,141,60;252,78,42;240,59,32;227,26,28;189,0,38;177,0,38;128,0,38]; +bmc(34).str = 'YlOrBr'; +bmc(34).typ = 'Sequential'; +bmc(34).rgb = [255,255,229;255,255,212;255,247,188;254,227,145;254,217,142;254,196,79;254,153,41;236,112,20;217,95,14;204,76,2;153,52,4;140,45,4;102,37,6]; +bmc(33).str = 'YlGnBu'; +bmc(33).typ = 'Sequential'; +bmc(33).rgb = [255,255,217;255,255,204;237,248,177;199,233,180;161,218,180;127,205,187;65,182,196;29,145,192;44,127,184;34,94,168;37,52,148;12,44,132;8,29,88]; +bmc(32).str = 'YlGn'; +bmc(32).typ = 'Sequential'; +bmc(32).rgb = [255,255,229;255,255,204;247,252,185;217,240,163;194,230,153;173,221,142;120,198,121;65,171,93;49,163,84;35,132,67;0,104,55;0,90,50;0,69,41]; +bmc(31).str = 'Reds'; +bmc(31).typ = 'Sequential'; +bmc(31).rgb = [255,245,240;254,229,217;254,224,210;252,187,161;252,174,145;252,146,114;251,106,74;239,59,44;222,45,38;203,24,29;165,15,21;153,0,13;103,0,13]; +bmc(30).str = 'RdPu'; +bmc(30).typ = 'Sequential'; +bmc(30).rgb = [255,247,243;254,235,226;253,224,221;252,197,192;251,180,185;250,159,181;247,104,161;221,52,151;197,27,138;174,1,126;122,1,119;122,1,119;73,0,106]; +bmc(29).str = 'Purples'; +bmc(29).typ = 'Sequential'; +bmc(29).rgb = [252,251,253;242,240,247;239,237,245;218,218,235;203,201,226;188,189,220;158,154,200;128,125,186;117,107,177;106,81,163;84,39,143;74,20,134;63,0,125]; +bmc(28).str = 'PuRd'; +bmc(28).typ = 'Sequential'; +bmc(28).rgb = [247,244,249;241,238,246;231,225,239;212,185,218;215,181,216;201,148,199;223,101,176;231,41,138;221,28,119;206,18,86;152,0,67;145,0,63;103,0,31]; +bmc(27).str = 'PuBuGn'; +bmc(27).typ = 'Sequential'; +bmc(27).rgb = [255,247,251;246,239,247;236,226,240;208,209,230;189,201,225;166,189,219;103,169,207;54,144,192;28,144,153;2,129,138;1,108,89;1,100,80;1,70,54]; +bmc(26).str = 'PuBu'; +bmc(26).typ = 'Sequential'; +bmc(26).rgb = [255,247,251;241,238,246;236,231,242;208,209,230;189,201,225;166,189,219;116,169,207;54,144,192;43,140,190;5,112,176;4,90,141;3,78,123;2,56,88]; +bmc(25).str = 'Oranges'; +bmc(25).typ = 'Sequential'; +bmc(25).rgb = [255,245,235;254,237,222;254,230,206;253,208,162;253,190,133;253,174,107;253,141,60;241,105,19;230,85,13;217,72,1;166,54,3;140,45,4;127,39,4]; +bmc(24).str = 'OrRd'; +bmc(24).typ = 'Sequential'; +bmc(24).rgb = [255,247,236;254,240,217;254,232,200;253,212,158;253,204,138;253,187,132;252,141,89;239,101,72;227,74,51;215,48,31;179,0,0;153,0,0;127,0,0]; +bmc(23).str = 'Greys'; +bmc(23).typ = 'Sequential'; +bmc(23).rgb = [255,255,255;247,247,247;240,240,240;217,217,217;204,204,204;189,189,189;150,150,150;115,115,115;99,99,99;82,82,82;37,37,37;37,37,37;0,0,0]; +bmc(22).str = 'Greens'; +bmc(22).typ = 'Sequential'; +bmc(22).rgb = [247,252,245;237,248,233;229,245,224;199,233,192;186,228,179;161,217,155;116,196,118;65,171,93;49,163,84;35,139,69;0,109,44;0,90,50;0,68,27]; +bmc(21).str = 'GnBu'; +bmc(21).typ = 'Sequential'; +bmc(21).rgb = [247,252,240;240,249,232;224,243,219;204,235,197;186,228,188;168,221,181;123,204,196;78,179,211;67,162,202;43,140,190;8,104,172;8,88,158;8,64,129]; +bmc(20).str = 'BuPu'; +bmc(20).typ = 'Sequential'; +bmc(20).rgb = [247,252,253;237,248,251;224,236,244;191,211,230;179,205,227;158,188,218;140,150,198;140,107,177;136,86,167;136,65,157;129,15,124;110,1,107;77,0,75]; +bmc(19).str = 'BuGn'; +bmc(19).typ = 'Sequential'; +bmc(19).rgb = [247,252,253;237,248,251;229,245,249;204,236,230;178,226,226;153,216,201;102,194,164;65,174,118;44,162,95;35,139,69;0,109,44;0,88,36;0,68,27]; +bmc(18).str = 'Blues'; +bmc(18).typ = 'Sequential'; +bmc(18).rgb = [247,251,255;239,243,255;222,235,247;198,219,239;189,215,231;158,202,225;107,174,214;66,146,198;49,130,189;33,113,181;8,81,156;8,69,148;8,48,107]; +bmc(17).str = 'Set3'; +bmc(17).typ = 'Qualitative'; +bmc(17).rgb = [141,211,199;255,255,179;190,186,218;251,128,114;128,177,211;253,180,98;179,222,105;252,205,229;217,217,217;188,128,189;204,235,197;255,237,111]; +bmc(16).str = 'Set2'; +bmc(16).typ = 'Qualitative'; +bmc(16).rgb = [102,194,165;252,141,98;141,160,203;231,138,195;166,216,84;255,217,47;229,196,148;179,179,179]; +bmc(15).str = 'Set1'; +bmc(15).typ = 'Qualitative'; +bmc(15).rgb = [228,26,28;55,126,184;77,175,74;152,78,163;255,127,0;255,255,51;166,86,40;247,129,191;153,153,153]; +bmc(14).str = 'Pastel2'; +bmc(14).typ = 'Qualitative'; +bmc(14).rgb = [179,226,205;253,205,172;203,213,232;244,202,228;230,245,201;255,242,174;241,226,204;204,204,204]; +bmc(13).str = 'Pastel1'; +bmc(13).typ = 'Qualitative'; +bmc(13).rgb = [251,180,174;179,205,227;204,235,197;222,203,228;254,217,166;255,255,204;229,216,189;253,218,236;242,242,242]; +bmc(12).str = 'Paired'; +bmc(12).typ = 'Qualitative'; +bmc(12).rgb = [166,206,227;31,120,180;178,223,138;51,160,44;251,154,153;227,26,28;253,191,111;255,127,0;202,178,214;106,61,154;255,255,153;177,89,40]; +bmc(11).str = 'Dark2'; +bmc(11).typ = 'Qualitative'; +bmc(11).rgb = [27,158,119;217,95,2;117,112,179;231,41,138;102,166,30;230,171,2;166,118,29;102,102,102]; +bmc(10).str = 'Accent'; +bmc(10).typ = 'Qualitative'; +bmc(10).rgb = [127,201,127;190,174,212;253,192,134;255,255,153;56,108,176;240,2,127;191,91,23;102,102,102]; +bmc(09).str = 'Spectral'; +bmc(09).typ = 'Diverging'; +bmc(09).rgb = [158,1,66;213,62,79;215,25,28;244,109,67;252,141,89;253,174,97;254,224,139;255,255,191;230,245,152;171,221,164;153,213,148;102,194,165;43,131,186;50,136,189;94,79,162]; +bmc(08).str = 'RdYlGn'; +bmc(08).typ = 'Diverging'; +bmc(08).rgb = [165,0,38;215,48,39;215,25,28;244,109,67;252,141,89;253,174,97;254,224,139;255,255,191;217,239,139;166,217,106;145,207,96;102,189,99;26,150,65;26,152,80;0,104,55]; +bmc(07).str = 'RdYlBu'; +bmc(07).typ = 'Diverging'; +bmc(07).rgb = [165,0,38;215,48,39;215,25,28;244,109,67;252,141,89;253,174,97;254,224,144;255,255,191;224,243,248;171,217,233;145,191,219;116,173,209;44,123,182;69,117,180;49,54,149]; +bmc(06).str = 'RdGy'; +bmc(06).typ = 'Diverging'; +bmc(06).rgb = [103,0,31;178,24,43;202,0,32;214,96,77;239,138,98;244,165,130;253,219,199;255,255,255;224,224,224;186,186,186;153,153,153;135,135,135;64,64,64;77,77,77;26,26,26]; +bmc(05).str = 'RdBu'; +bmc(05).typ = 'Diverging'; +bmc(05).rgb = [103,0,31;178,24,43;202,0,32;214,96,77;239,138,98;244,165,130;253,219,199;247,247,247;209,229,240;146,197,222;103,169,207;67,147,195;5,113,176;33,102,172;5,48,97]; +bmc(04).str = 'PuOr'; +bmc(04).typ = 'Diverging'; +bmc(04).rgb = [127,59,8;179,88,6;230,97,1;224,130,20;241,163,64;253,184,99;254,224,182;247,247,247;216,218,235;178,171,210;153,142,195;128,115,172;94,60,153;84,39,136;45,0,75]; +bmc(03).str = 'PRGn'; +bmc(03).typ = 'Diverging'; +bmc(03).rgb = [64,0,75;118,42,131;123,50,148;153,112,171;175,141,195;194,165,207;231,212,232;247,247,247;217,240,211;166,219,160;127,191,123;90,174,97;0,136,55;27,120,55;0,68,27]; +bmc(02).str = 'PiYG'; +bmc(02).typ = 'Diverging'; +bmc(02).rgb = [142,1,82;197,27,125;208,28,139;222,119,174;233,163,201;241,182,218;253,224,239;247,247,247;230,245,208;184,225,134;161,215,106;127,188,65;77,172,38;77,146,33;39,100,25]; +bmc(01).str = 'BrBG'; +bmc(01).typ = 'Diverging'; +bmc(01).rgb = [84,48,5;140,81,10;166,97,26;191,129,45;216,179,101;223,194,125;246,232,195;245,245,245;199,234,229;128,205,193;90,180,172;53,151,143;1,133,113;1,102,94;0,60,48]; +% Number of nodes: +for k = 1:numel(bmc) + switch bmc(k).typ + case 'Diverging' + bmc(k).num = 11; + case 'Qualitative' + bmc(k).num = size(bmc(k).rgb,1); + case 'Sequential' + bmc(k).num = 9; + otherwise + error('SC:brewermap:Struct:UnknownType','Unknown colorscheme type.') + end +end +% +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%bmStruct +% +% Code and Implementation: +% Copyright (c) 2014-2025 Stephen Cobeldick +% Color Values Only: +% Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University. +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and limitations under the License. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% 1. Redistributions as source code must retain the above copyright notice, this +% list of conditions and the following disclaimer. +% +% 2. The end-user documentation included with the redistribution, if any, must +% include the following acknowledgment: "This product includes color +% specifications and designs developed by Cynthia Brewer +% (http://colorbrewer.org/)." Alternately, this acknowledgment may appear in the +% software itself, if and wherever such third-party acknowledgments normally appear. +% +% 4. The name "ColorBrewer" must not be used to endorse or promote products +% derived from this software without prior written permission. For written +% permission, please contact Cynthia Brewer at cbrewer@psu.edu. +% +% 5. Products derived from this software may not be called "ColorBrewer", nor +% may "ColorBrewer" appear in their name, without prior written permission of Cynthia Brewer. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%license \ No newline at end of file diff --git a/beamtime/brewermap_nodes.m b/beamtime/brewermap_nodes.m new file mode 100644 index 0000000..61a6708 --- /dev/null +++ b/beamtime/brewermap_nodes.m @@ -0,0 +1,63 @@ +function brewermap_nodes() +% Create a figure showing all ColorBrewer 2.0 colorschemes' defining nodes. +% +% (c) 2014-2025 Stephen Cobeldick +% +% View Cynthia Brewer's ColorBrewer 2.0 colorschemes/palettes in a figure: +% +% * Each row of the figure shows the defining nodes for one colorscheme. +% * Text with all colorschemes' names. +% * Text with all colorschemes' types (Diverging/Qualitative/Sequential). +% +%%% Syntax %%% +% +% brewermap_nodes() +% +%% Dependencies %% +% +% * MATLAB R2009a or later. +% * brewermap.m +% +% See also BREWERMAP BREWERMAP_VIEW CUBEHELIX MAXDISTCOLOR +% LBMAP PARULA LINES RGBPLOT COLORMAP COLORBAR PLOT PLOT3 AXES SET +persistent cbh axh +% +[mcs,nmn,pyt] = brewermap('list'); +% +xmx = max(nmn); +ymx = numel(pyt); +% +if ishghandle(cbh) + figure(cbh); + delete(axh); +else + cbh = figure('HandleVisibility','callback', 'IntegerHandle','off',... + 'NumberTitle','off', 'Name',mfilename,... + 'MenuBar','figure', 'Toolbar','none', 'Tag',mfilename); + set(cbh,'Units','pixels') + pos = get(cbh,'Position'); + pos(1:2) = pos(1:2) - 123; + pos(3:4) = max(pos(3:4),[842,532]); + set(cbh,'Position',pos) +end +% +axh = axes('Parent',cbh, 'Color','none',... + 'XTick',0:xmx, 'YTick',0.5:ymx, 'YTickLabel',mcs, 'YDir','reverse'); +title(axh,'ColorBrewer Color Schemes (brewermap.m)', 'Interpreter','none') +xlabel(axh,'Scheme Nodes') +ylabel(axh,'Scheme Name') +axf = get(axh,'FontName'); +% +for y = 1:ymx + N = nmn(y); + M = brewermap(N,mcs{y}); + for x = 1:N + patch([x-1,x-1,x,x],[y-1,y,y,y-1],1, 'FaceColor',M(x,:), 'Parent',axh) + end + text(xmx+0.1,y-0.5,pyt{y}, 'Parent',axh, 'FontName',axf) +end +% +drawnow() +% +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%brewermap_nodes \ No newline at end of file diff --git a/beamtime/brewermap_view.m b/beamtime/brewermap_view.m new file mode 100644 index 0000000..e7ec245 --- /dev/null +++ b/beamtime/brewermap_view.m @@ -0,0 +1,408 @@ +function [map,num,typ,scheme] = brewermap_view(N,scheme,isco) +% Create an interactive figure for viewing ColorBrewer colormaps. With demo! +% +% (c) 2014-2025 Stephen Cobeldick +% +% View Cynthia Brewer's ColorBrewer 2.0 colorschemes/palettes in a figure: +% +% * Two colorbars give the colorscheme in color and grayscale. +% * A button toggles between 3D-cube and 2D-lineplot of the RGB values. +% * A button toggles an endless demo cycle through the colorschemes. +% * A button reverses the colormap. +% * Thirty-five buttons select any ColorBrewer colorscheme. +% * Text with the colorscheme's type (Diverging/Qualitative/Sequential). +% * Text with the colorscheme's number of nodes (i.e. defining colors). +% +%%% Syntax %%% +% +% brewermap_view +% brewermap_view(N) +% brewermap_view(N,scheme) +% brewermap_view(N,scheme,isco) +% brewermap_view([],...) +% brewermap_view(axes/figure handle array,...) % see "Adjust Colormaps" +% [map,num,typ] = brewermap_view(...) +% +% Calling the function with an output argument blocks MATLAB execution until +% the figure is deleted: the final colormap and parameters are then returned. +% +%% Adjust Colormaps or Colororders of Figures or Axes %% +% +% For R2014b or later: provide axes or figure handles as the first input +% and their COLORMAP will be updated in real-time by BREWERMAP_VIEW. +% For R2019b or later: set the 3rd input to TRUE to update COLORORDER. +% +% >> S = load('spine'); +% >> image(S.X) +% >> brewermap_view(gca) % colormap +% +% >> plot(rand(5,7)) +% >> brewermap_view(gca,[],true) % colororder +% +%% Input Arguments (**=default) %% +% +% N = NumericScalar, an integer to define the colormap length. +% = []**, colormap length of two hundred and fifty-six (256). +% = NaN, same length as the defining RGB nodes (useful for Line ColorOrder). +% = Array of axes or figure handles. R2014b or later only. +% scheme = CharRowVector or StringScalar, a ColorBrewer colorscheme name. +% = []** randomly selects a colorscheme. +% isco = LogicalScalar, true/false** updates the colororder/colormap of +% the provided axes or figure handles. R2019b or later only. +% +%% Output Arguments (block code execution until figure is closed) %% +% +% map = NumericMatrix, the colormap defined when the figure is closed. +% num = NumericVector, the number of nodes defining the ColorBrewer colorscheme. +% typ = CharRowVector, the colorscheme type: 'Diverging'/'Qualitative'/'Sequential'. +% +%% Dependencies %% +% +% * MATLAB R2009a or later. +% * brewermap.m +% +% See also BREWERMAP BREWERMAP_NODES CUBEHELIX PRESET_COLORMAP MAXDISTCOLOR +% RGBPLOT COLORMAP COLORMAPEDITOR COLORBAR UICONTROL ADDLISTENER +persistent ax2D ln2D ax3D pt3D txtH is2D cbAx cbIm pTxt pSld bEig bGrp bRev scm isr +% +new = isempty(ax2D)||~ishghandle(ax2D); +dfn = 256; +upd = false; +upb = false; +hgv = []; +nmr = dfn; +% +%% Input Wrangling %% +% +if nargin>2 && isco + % >R2019a only! + fnh = @colororder; +else % default + fnh = @colormap; +end +% +err = 'First input must be a real positive scalar numeric or [] or NaN.'; +if nargin==0 || isnumeric(N)&&isequal(N,[]) + N = dfn; +elseif isnumeric(N) + assert(isscalar(N),'SC:brewermap_view:N:NotScalarNumeric',err) + assert(isreal(N)&&isfinite(N)&&fix(N)==N&&N>0||isnan(N),... + 'SC:brewermap_view:N:NotRealPositiveIntegerNorNaN',err) + N = double(N); +elseif all(ishghandle(N(:))) % R2014b or later + assert(all(isgraphics(N(:),'axes')|isgraphics(N(:),'figure')),... + 'SC:brewermap_view:N:NotAxesNorFigureHandles',... + 'First input may be an array of figure or axes handles.') + hgv = N(:); + nmr = arrayfun(@(h)size(fnh(h),1),hgv); + N = nmr(1); +else + error('SC:brewermap_view:N:UnsupportedInput',err) +end +% +[mcs,nmn,pyt] = brewermap('list'); +% +% Check BREWERMAP output: +tmp = find([any(diff(double(char(pyt)),1),2);1]); +assert(isequal(tmp,[9;17;35]),'SC:brewermap_view:BREWERMAP:OutputOrder',... + 'The BREWERMAP function returned an unexpected colorscheme sequence.') +% +if nargin<2 || isnumeric(scheme)&&isequal(scheme,[]) + % Default pseudo-random colorscheme: + isr = rand(1)>0.5; + scm = mcs{1+mod(round(now*1e7),numel(mcs))}; %#ok +else + % Parse input colorscheme: + scheme = bmv1s2c(scheme); + assert(ischar(scheme) && ndims(scheme)==2 && size(scheme,1)==1,... + 'SC:brewermap_view:scheme:NotCharacterVector',... + 'Second input must be a 1xN char vector.') %#ok + % Check if a reversed colormap was requested: + isr = strncmp(scheme,'-',1) || strncmp(scheme,'*',1); + isd = strncmp(scheme,'+',1) || isr; + scm = scheme(1+isd:end); +end +% +if isnan(N) + N = nmn(strcmpi(scm,mcs)); +end +% +%% Ensure Figure Exists %% +% +% LHS and RHS slider bounds/limits, and slider step sizes: +lbd = 1; +rbd = dfn; +stp = [1,10]; % [minor,major] +% +% Define the 3D cube axis order: +xyz = 'RGB'; % specify the order here. +[~,xyz] = ismember(xyz,'RGB'); +% +if new % Create a new figure. + % + % Figure parameters: + M = 9; % buttons per column + gap = 0.01; % gaps + bth = 0.04; % demo button height + btw = 0.10; % demo button width + bgh = 0.40; % button group height + cbw = 0.23; % colorbar width (both together) + cbh = 1-3*gap-bth; % colorbar height + axh = 1-bgh-2*gap; % axes height + axw = 1-cbw-2*gap; % axes width + bgw = axw-btw-gap; % button group width + % + figH = figure('HandleVisibility','callback',... + 'IntegerHandle','off', 'NumberTitle','off', 'Units','normalized',... + 'Name','ColorBrewer Interactive ColorScheme Selector',... + 'MenuBar','figure', 'Toolbar','none', 'Tag',mfilename); + % + % Add 2D lineplot: + ax2D = axes('Parent',figH, 'Position',[gap,bgh+gap,axw,axh], 'Box','on',... + 'ColorOrder',[1,0,0; 0,1,0; 0,0,1; 0.6,0.6,0.6], 'HitTest','off',... + 'Visible','off', 'XLim',[0,1], 'YLim',[0,1], 'XTick',[], 'YTick',[]); + ln2D = line([0,0,0,0;1,1,1,1],[0,0,0,0;1,1,1,1], 'Parent',ax2D,... + 'Visible','off', 'Linestyle','-', 'Marker','.'); + % + % Add 3D scatterplot: + ax3D = axes('Parent',figH, 'OuterPosition',[0,bgh,axw+2*gap,1-bgh],... + 'Visible','on', 'XLim',[0,1], 'YLim',[0,1], 'ZLim',[0,1], 'HitTest','on'); + pt3D = patch('Parent',ax3D, 'XData',[0;1], 'YData',[0;1], 'ZData',[0;1],... + 'Visible','on', 'LineStyle','none', 'FaceColor','none', 'MarkerEdgeColor','none',... + 'Marker','o', 'MarkerFaceColor','flat', 'MarkerSize',10, 'FaceVertexCData',[1,1,0;1,0,1]); + view(ax3D,3); + grid(ax3D,'on') + lbl = {'Red','Green','Blue'}; + xlabel(ax3D,lbl{xyz(1)}) + ylabel(ax3D,lbl{xyz(2)}) + zlabel(ax3D,lbl{xyz(3)}) + % + % Add warning text: + txtH = text('Parent',ax2D, 'Units','normalized', 'Position',[1,1],... + 'HorizontalAlignment','right', 'VerticalAlignment','top'); + % + % Add demo button: + demo = uicontrol(figH, 'Style','togglebutton', 'Units','normalized',... + 'Position',[1-cbw/2,1-bth-gap,cbw/2-gap,bth], 'String','Demo',... + 'Max',1, 'Min',0, 'Callback',@bmvDemo); %#ok + % Add 2D/3D button: + is2D = uicontrol(figH, 'Style','togglebutton', 'Units','normalized',... + 'Position',[1-cbw/1,1-bth-gap,cbw/2-gap,bth], 'String','2D / 3D',... + 'Max',1, 'Min',0, 'Callback',@bmv2D3D); + % Add reverse button: + bRev = uicontrol(figH, 'Style','togglebutton', 'Units','normalized',... + 'Position',[bgw+2*gap,bgh-bth,btw,bth], 'String','Reverse',... + 'Max',1, 'Min',0, 'Callback',@bmvRevM); + % + % Add colorbars: + C(1,1,:) = [1,1,1]; + cbAx(2) = axes('Parent',figH, 'Visible','off', 'Units','normalized',... + 'Position',[1-cbw/2,gap,cbw/2-gap,cbh], 'YLim',[0.5,1.5],... + 'YDir','normal', 'HitTest','off'); + cbAx(1) = axes('Parent',figH, 'Visible','off', 'Units','normalized',... + 'Position',[1-cbw/1,gap,cbw/2-gap,cbh], 'YLim',[0.5,1.5],... + 'YDir','normal', 'HitTest','off'); + cbIm(2) = image('Parent',cbAx(2), 'CData',C); + cbIm(1) = image('Parent',cbAx(1), 'CData',C); + % + % Add parameter sliders, listeners, and corresponding text: + sv = mean([lbd,rbd],2); + pTxt = uicontrol(figH,'Style','text', 'Units','normalized',... + 'Position',[bgw+2*gap,bgh-2*bth-gap,btw,bth], 'String','X'); + pSld = uicontrol(figH,'Style','slider', 'Units','normalized',... + 'Position',[bgw+2*gap,gap,btw,bgh-2*bth-gap], 'Min',lbd(1), 'Max',rbd(1),... + 'SliderStep',stp(1,:)/(rbd(1)-lbd(1)), 'Value',sv(1)); + addlistener(pSld, 'Value', 'PostSet',@bmvSldr); + % + % Add colorscheme button group: + bGrp = uibuttongroup('Parent',figH, 'BorderType','none', ... + 'Units','normalized', 'Position',[gap,gap,bgw,bgh-gap]); + % Determine button locations: + Z = 1:numel(mcs); + Z = Z+(Z>17); + C = (ceil(Z/M)-1)/4; + R = (M-1-mod(Z-1,M))/M; + % Add colorscheme buttons to group: + for jj = numel(mcs):-1:1 + bEig(jj) = uicontrol('Parent',bGrp, 'Style','Toggle', 'String',mcs{jj},... + 'Unit','normalized', 'Position',[C(jj),R(jj),1/4,1/M]); + end + set(bGrp,'SelectionChangeFcn',@bmvChgS); + % +end +% +%% Nested Functions %% +% + function str = bmvName() + % Generate the colorscheme name. + pfx = '+-'; + ids = strcmpi(scm,mcs); + if any(ids) + str = [pfx(1+isr),mcs{ids}]; + else % will throw an error in BREWERMAP: + str = scm; + end + end +% + function bmvUpDt() + % Update all graphics objects in the figure. + % + % Get ColorBrewer colormap and grayscale equivalent: + [map,num,typ] = brewermap(N,bmvName()); + mag = map*[0.298936;0.587043;0.114021]; + % + % Update colorbar values: + set(cbAx, 'YLim',[0,abs(N)+(N==0)]+0.5); + set(cbIm(1), 'CData',reshape(map,[],1,3)) + set(cbIm(2), 'CData', repmat(mag,[1,1,3])) + % + % Update 2D line / 3D patch values: + if get(is2D, 'Value') + set(ln2D, 'XData',linspace(0,1,abs(N))); + set(ln2D,{'YData'},num2cell([map,mag],1).'); + else % 3D + set(pt3D,... + 'XData',map(:,xyz(1)),... + 'YData',map(:,xyz(2)),... + 'ZData',map(:,xyz(3)),... + 'FaceVertexCData',map) + end + % + % Update reverse button: + set(bRev, 'Value',isr) + % + % Update warning text: + str = {[bmvName(),' '];[typ,' '];sprintf('%d Nodes ',num)}; + set(txtH,'String',str); + % + % Update parameter value text: + set(pTxt(1), 'String',sprintf('N = %.0f',N)); + % + % Update external axes/figure colormaps/colororders: + nmr(1) = N; + for k = 1:numel(hgv) + fnh(hgv(k),brewermap(nmr(k),bmvName())); + end + % + drawnow() + end +% + function bmv2D3D(h,~) + % Switch between 2D-line and 3D-cube representation. + % + if get(h,'Value') % 2D + set(ax3D, 'HitTest','off', 'Visible','off') + set(ax2D, 'HitTest','on', 'Visible','on') + set(pt3D, 'Visible','off') + set(ln2D, 'Visible','on') + else % 3D + set(ax2D, 'HitTest','off', 'Visible','off') + set(ax3D, 'HitTest','on', 'Visible','on') + set(ln2D, 'Visible','off') + set(pt3D, 'Visible','on') + end + % + bmvUpDt(); + end +% + function bmvChgS(~,e) + % Change the colorscheme. + % + scm = get(e.NewValue,'String'); + % + bmvUpDt() + end +% + function bmvRevM(h,~) + % Reverse the colormap. + % + isr = logical(get(h,'Value')); + % + bmvUpDt() + end +% + function bmvSldr(~,~) + % Update the slider position. + % + if ~upd + return + end + % + N = round(get(pSld,'Value')); + % + bmvUpDt() + end +% + function bmvDemo(h,~) + % Display all ColorBrewer colorschemes sequentially. + % + cnt = 0; + while ishghandle(h)&&get(h,'Value') + cnt = mod(cnt+1,pow2(53)); + % + if mod(cnt,23)<1 + ids = 1+mod(find(strcmpi(scm,mcs)),numel(mcs)); + scm = mcs{ids}; + try %#ok + set(bGrp, 'SelectedObject',bEig(ids)); + end + end + % + if mod(cnt,69)<1 + isr = ~isr; + end + % + upb = (upb || N<=1) && N + set(pSld, 'Value',N) + bmvUpDt() + end + upd = true; + % + % Faster/slower: + pause(0.1); + end + % + end +% +%% Initialize the Figure %% +% +set(bGrp,'SelectedObject',bEig(strcmpi(scm,mcs))); +set(pSld,'Value',max(lbd,min(rbd,N))); +upd = true; +bmvUpDt() +% +if nargout + waitfor(ax2D); + scheme = bmvName(); +else + clear map +end +% +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%brewermap_view +function arr = bmv1s2c(arr) +% If scalar string then extract the character vector, otherwise data is unchanged. +if isa(arr,'string') && isscalar(arr) + arr = arr{1}; +end +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%bmv1s2c +% +% Copyright (c) 2014-2025 Stephen Cobeldick +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and limitations under the License. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%license \ No newline at end of file diff --git a/beamtime/distFig.m b/beamtime/distFig.m new file mode 100644 index 0000000..0876817 --- /dev/null +++ b/beamtime/distFig.m @@ -0,0 +1,831 @@ +function distFig(varargin) +% ===== Syntax ============================================================ +% distFig(...,'Screen',Value) +% distFig(...,'Position',Value) +% +% distFig(...,'Rows',Value) +% distFig(...,'Columns',Value) +% +% distFig(...,'Not',Value) +% distFig(...,'Only',Value) +% distFig(...,'Offset',Value) +% +% distFig(...,'Adjust',Value) +% distFig(...,'Extra',Value) +% distFig(...,'Menu',Value) +% distFig(...,'Transpose',Value) +% distFig(...,'Simulink',Value) +% distFig(...,'Tight',Value) +% distFig(...,'Scale',Value) +% distFig(...,'ScaleOrigin',Value) +% distFig(...,'FreezeSize',Value) +% distFig(...,'Order',Value) +% +% ===== Description ======================================================= +% distFig(...,'Screen',Value) assigns where the figures will be +% distributed. +% Synonyms for 'Screen' are 'Scr' and 'S'. Value can be: +% 'C' / 'Center' (Default) +% 'W' / 'West' / 'L' / 'Left' +% 'E' / 'East' / 'R' / 'Right' +% 'N' / 'North' / 'T' / 'Top' +% 'S' / 'South' / 'B' / 'Bottom' +% 'Main' / 'Primary' +% 'Ext' / 'External' / 'Secondary' +% +% distFig(...,'Position',Value) assigns in which part of the screen the +% figures will be distributed. +% Synonyms for 'Position' are 'Pos' and 'P'. Value can be: +% 'C' / 'Center' (Default) +% 'W' / 'West' / 'L' / 'Left' +% 'E' / 'East' / 'R' / 'Right' +% 'N' / 'North' / 'T' / 'Top' +% 'S' / 'South' / 'B' / 'Bottom' +% 'NW' +% 'NE' +% 'SW' +% 'SE' +% +% distFig(...,'Rows',Value) assigns how many rows the figures will be +% distributed on. +% Synonyms for 'Rows' are 'R'. Value can be: +% 1...n +% 'Auto' / -1 +% 'Auto' indicates that it automatically calculates the number of required +% rows. +% +% distFig(...,'Columns',Value) assigns how many rows the figures will be +% distributed on. +% Synonyms for 'Columns' are 'Cols' and 'C'. Value can be: +% 1...n +% 'Auto' / -1 +% 'Auto' indicates that it automatically calculates the number of required +% columns. +% +% distFig(...,'Not',Value) excludes specified figures from the distribution +% list. Value must be an array with the excluded figure numbers. The +% default value is []. The input argument 'gcf' (get current figure) can be +% used as well. +% +% distFig(...,'Only',Value) does only distribute specified figures. Value +% must be an array with the figure which will be distributed. The +% default value is []. The input argument 'gcf' (get current figure) can be +% used as well. +% +% distFig(...,'Offset',Value) can be used to shift all figures on the +% distribution list. Synonyms for 'Offset' is 'O'. Value must be an integer +% larger or equal to zero. The default offset value is 0. +% +% distFig(...,'Adjust',Value) can be used to adjust all figure positions. +% For instance, if all figures should be moved 20 pixels to the right, +% which is not possible with the other features, Value should be [20,0]. +% If Value is below 1, the scalars indicate the relative adjustment to the +% screen. If the selected monitor is [1920,1080] pixels, a Values of +% [0.25,-0.5] moves all figures with [480,540] pixels. +% Synonyms for 'Ajust' is 'A'. Value can be: +% [Integer,Integer] (In order to adjust by a specific ammount) +% [Scalar,Scalar]( < 1) (To adjust a relative ammount of the monitor size) +% The default value is [0,0]. +% +% distFig(...,'Extra',Value) is used to handle the extra figures, which +% might not fit into 'Rows' * 'Cols'. For instance, 5 figure are to be +% distributed on a 2x2 = 4 area. The fifth figure is then places on top of +% the first figure if 'Extra' is 'restart'. It can be ignored by setting +% 'Extra' to 'ignore', in which the fifth figure will not be distributed. +% +% distFig(...,'Menu',Value) can be used to modify the menu bar. +% Value can be: +% 'figure' +% 'none' +% +% distFig(...,'Transpose',Value) can be used to tranpose the order of +% distributing. The values must be logical and the default value is false. +% +% distFig(...,'Simulink',Value) can be used to handle simulink figures. +% Value can be: +% 'include' (Default) +% 'exclude' +% 'only' +% If value is true, simulink figures will be distributed along with the +% other figures. If value if 'exclude' simulink figures will be +% excluded from the distribution list. If value is 'only', only the +% simulink figures will be distributed and not the ordinary figures. +% +% distFig(...,'Tight',Value) can be used to stretch the figure to the +% border of the figure, so no unnecessary "whitespace" is present. +% Value must be logical and the default value if false. +% +% distFig(...,'Scale',Value) can be used to scale the figure matrix. Both +% horizontal and vertical scaling can be applied, but also similar scaling. +% Value is a single scalar for similar scaling, but is a 2x1 matrix for +% specific scaling in each direction. For instance, distFig('Scale',0.5) +% scales all figures by 50% relative to the upper left corner of the +% distribution area (Default Scale-origin). distFig('Scale',[0.8,0.4]) +% scales the figures by 80% and 40% in the horizontal and vertical +% directions respectivly relative to the default scale-origin. +% +% distFig(...,'ScaleOrigin',Value) can be used in combination with the +% 'Scale' option. Value is a 2x1 matrix with the origin. If these values +% are between 0 and 1 the relative monitor size is used, whereas larger +% values should be rounded indicating pixel values. For instance, +% distFig('Scale',0.5,'ScaleOrigin',[0.5,1]) scales the figures relative +% to the midpoint on the right hand side of the selected monitor. For a +% monitor with resolution 1920x1080, this would be equal to +% distFig('Scale',0.5,'ScaleOrigin',[1920,960]). +% The default scale origin is in the upper left corner of the distribution +% area. +% Synonyms for 'ScaleOrigin' are 'Origin', 'Orig' and 'Scaleorig'. +% +% distFig(...,'FreezeSize',Value) can be used to freeze the sizes of the +% figures before distribution. Currently, this function anchors the figures +% to the upper left corner of the original position, yet keeping the +% original size - it does not calculate new positions, so the figures may +% overlap. +% Synonym for 'FreezeSize' is 'Freeze'. +% +% distFig(...,'Order',Value) can be used to control the plotting order. For +% instance, distFig('Only',[5,6,7,8],'Order',[2,1,4,3]) will plot figure +% 5,6,7 and 8 in the order of 6,5,8 and 7. The ordering argument is thus +% related to the input of 'Only'. +% +% ===== Examples ========================================================== +% distFig(); +% This will distribute all open figures on the primary screen in two rows. +% +% distFig('Screen','Left','Position','Right','Only',[1,2,4]) +% This will only distribute figure 1, 2 and 4 on the right part of the left +% screen. +% +% distFig('Offset',2,'Not',[1,2]) +% This will distribute all figure but figure 1 and 2 in the same pattern as +% distFig(), but figure 1 and 2 will not be distributed, but instead there will +% be blank spots where they would have been distributed. +% +% ===== Note ============================================================== +% If you find any errors or bugs with this version please contact me on +% AndersSSimonsen@GMail.com or write a comment on file exchange - then I'll +% take a look at it. :) +% +% This function will only work in MATLAB 2014b and onwards due to the +% massive overhaul in the figure handle system - I will not support +% previous versions. + +% ========================================================================= +% ===== Version =========================================================== +% ========================================================================= + +% Anders Schou Simonsen, AndersSSimonsen@GMail.com +% Version 5.5 +% 31/03-2015 + +% ========================================================================= +% ===== Default values ==================================================== +% ========================================================================= + +Rows = -1; +Cols = -1; +Scr = 'C'; +Pos = 'C'; +Tight = false; +Menu = ''; +Transpose = false; +Extra = 'restart'; +Offset = 0; +Not = []; +Only = []; +Simulink = 'include'; +Adjust = [0,0]; +Scale = 1; +Scale_Orig = []; +FreezeSize = false; +Order = []; + +% ========================================================================= +% ===== Get inputs ======================================================== +% ========================================================================= + +if (mod(nargin,2) ~= 0) + error('Uneven number of inputs! Returning...'); +end + +for i = 1:2:nargin + if (ischar(varargin{i+1})) + varargin{i+1} = lower(varargin{i+1}); + end + switch lower(varargin{i}) + case {'scr','screen','s'} + % ============================================================= + Allow = { + 'C' 'c' 'center' 'main' 'primary' + 'W' 'w' 'west' 'l' 'left' + 'E' 'e' 'east' 'r' 'right' + 'N' 'n' 'north' 't' 'top' + 'S' 's' 'south' 'b' 'bottom' + 'Ext' 'ext' 'external' 'secondary' NaN + }; + Trigger = any(strcmp(Allow,varargin{i+1}),2); + if (~any(Trigger)) + warning('Input ''%s'' not recognized for ''%s''!',varargin{i+1},varargin{i}); + else + Scr = Allow{Trigger,1}; + end + case {'pos','position','p'} + % ============================================================= + Allow = { + 'C' 'c' 'center' NaN NaN + 'W' 'w' 'west' 'l' 'left' + 'E' 'e' 'east' 'r' 'right' + 'N' 'n' 'north' 't' 'top' + 'S' 's' 'south' 'b' 'bottom' + 'NW' 'nw' NaN NaN NaN + 'NE' 'ne' NaN NaN NaN + 'SW' 'sw' NaN NaN NaN + 'SE' 'se' NaN NaN NaN + }; + Trigger = any(strcmp(Allow,varargin{i+1}),2); + if (~any(Trigger)) + warning('Input ''%s'' not recognized for ''%s''!',varargin{i+1},varargin{i}); + else + Pos = Allow{Trigger,1}; + end + case {'rows','r'} + % ============================================================= + if (strcmp(varargin{i+1},'auto')) + Rows = -1; + else + if ~((isnumeric(varargin{i+1})) && (mod(varargin{i+1},1) == 0)) + warning('Input to ''%s'' must be a round numeric value!',varargin{i}); + else + Rows = varargin{i+1}; + end + end + case {'cols','columns','c'} + % ============================================================= + if (strcmp(varargin{i+1},'auto')) + Cols = -1; + else + if ~((isnumeric(varargin{i+1})) && (mod(varargin{i+1},1) == 0)) + warning('Input to ''%s'' must be a round numeric value!',varargin{i}); + else + Cols = varargin{i+1}; + end + end + case {'ext','extra'} + % ============================================================= + Allow = { + 'ignore' + 'restart' + }; + Trigger = any(strcmp(Allow,varargin{i+1}),2); + if (~any(Trigger)) + warning('Input ''%s'' not recognized for ''%s''!',varargin{i+1},varargin{i}); + else + Extra = Allow{Trigger,1}; + end + case {'menu','bar'} + % ============================================================= + Allow = { + 'figure' + 'none' + }; + Trigger = any(strcmp(Allow,varargin{i+1}),2); + if (~any(Trigger)) + warning('Input ''%s'' not recognized for ''%s''!',varargin{i+1},varargin{i}); + else + Menu = Allow{Trigger,1}; + end + case {'transpose'} + % ============================================================= + if (~islogical(varargin{i+1})) + warning('Input to ''%s'' must be logical!',varargin{i}); + else + Transpose = varargin{i+1}; + end + case {'offset','o'} + % ============================================================= + if ~((isnumeric(varargin{i+1})) && (mod(varargin{i+1},1) == 0)) + warning('Input to ''%s'' must be a round numeric value!',varargin{i}); + else + Offset = varargin{i+1}; + end + case {'adjust','a'} + % ============================================================= + if ~(isnumeric(varargin{i+1})) + warning('Input to ''%s'' must be numeric!',varargin{i}); + elseif (numel(varargin{i+1}) ~= 2) + warning('Input to ''%s'' must be a (1 x 2) matrix with integers or scalars less than one!',varargin{i}); + elseif (all(mod(varargin{i+1},1) == 0)) + Adjust = varargin{i+1}; + else + if (any(varargin{i+1} > 1)) + warning('Input to ''%s'' must be either integers or scalars less than one!',varargin{i}); + else + Adjust = varargin{i+1}; + end + end + case {'not'} + % ============================================================= + if ~((isnumeric(varargin{i+1}) && all(mod(varargin{i+1},1) == 0)) || isobject(varargin{i+1})) + warning('Input to ''%s'' must be round numeric values!',varargin{i}); + else + if (isobject(varargin{i+1})) + Not = get(varargin{i+1},'Number'); + if (iscell(Not)) + Not = cell2mat(Not); + end + else + Not = varargin{i+1}; + end + end + case {'only'} + % ============================================================= + if ~((isnumeric(varargin{i+1}) && all(mod(varargin{i+1},1) == 0)) || isobject(varargin{i+1})) + warning('Input to ''%s'' must be round numeric values!',varargin{i}); + else + if (isobject(varargin{i+1})) + Only = get(varargin{i+1},'Number'); + if (iscell(Only)) + Only = cell2mat(Only); + end + else + Only = varargin{i+1}; + end + end + case {'simu','simulink'} + % ============================================================= + Allow = { + 'include' NaN + 'exclude' 'ignore' + 'only' NaN + }; + Trigger = any(strcmp(Allow,varargin{i+1}),2); + if (~any(Trigger)) + warning('Input ''%s'' not recognized for ''%s''!',varargin{i+1},varargin{i}); + else + Simulink = Allow{Trigger,1}; + end + case {'tight'} + % ============================================================= + if (~islogical(varargin{i+1})) + warning('Input to ''%s'' must be logical!',varargin{i}); + else + Tight = varargin{i+1}; + end + case {'scale'} + % ============================================================= + if (~isnumeric(varargin{i+1}) || (numel(varargin{i+1}) > 2)) + warning('Input to ''%s'' must be one or two numeric values (x and y scale)!',varargin{i}); + else + Scale = varargin{i+1}; + end + case {'origin','orig','scaleorig','scaleorigin'} + % ============================================================= + if (~isnumeric(varargin{i+1}) || (numel(varargin{i+1}) ~= 2)) + warning('Input to ''%s'' must be two numeric values (x0 and y0)!',varargin{i}); + else + Scale_Orig = varargin{i+1}; + end + case {'freezesize','freeze'} + % ============================================================= + if (~islogical(varargin{i+1})) + warning('Input to ''%s'' must be logical!',varargin{i}); + else + FreezeSize = varargin{i+1}; + end + case {'order'} + % ============================================================= + if ~(isnumeric(varargin{i+1}) && all(mod(varargin{i+1},1) == 0)) + warning('Input to ''%s'' must be round numeric values!',varargin{i}); + else + Order = varargin{i+1}; + end + otherwise + % ============================================================= + fprintf('Input ''%s'' not recognized!',varargin{i}); + end +end + +% ========================================================================= +% ===== Constants ========================================================= +% ========================================================================= + +% ===== Limits ============================================================ +Limits = {... + 'C' [0,1] [0,1] + 'W' [0,0.5] [0,1] + 'E' [0.5,1] [0,1] + 'N' [0,1] [0.5,1] + 'NW' [0,0.5] [0.5,1] + 'NE' [0.5,1] [0.5,1] + 'S' [0,1] [0,0.5] + 'SW' [0,0.5] [0,0.5] + 'SE' [0.5,1] [0,0.5] + }; + +% ===== Screen angles ===================================================== +Screens = {... + (-pi * 1 / 4) (+pi * 1 / 4) 'E' + (-pi * 3 / 4) (-pi * 1 / 4) 'S' + (+pi * 1 / 4) (+pi * 3 / 4) 'N' + (+pi * 3 / 4) Inf 'W' + -Inf (-pi * 3 / 4) 'W' + }; + +% ===== Taskbar height ==================================================== +Taskbar_Height = 40; + +% ========================================================================= +% ===== Figure list ======================================================= +% ========================================================================= + +% ===== All figures ======================================================= +Fig_Object = findall(0,'type','figure'); +if (isempty(Fig_Object)) + fprintf('distFig: No figures to distribute!\n'); + return; +end + +% ===== Remove not visible figures ======================================== +Visible = arrayfun(@(n) (strcmp(get(Fig_Object(n),'Visible'),'on')),1:numel(Fig_Object)); +Fig_Object = Fig_Object(Visible); + +% ===== Include simulink figures ========================================== +Fig_Number = {Fig_Object.Number}; +Empty = arrayfun(@(n) (isempty(Fig_Number{n})),1:numel(Fig_Number)); +Fig_Number(Empty) = {0}; +Fig_Number = cell2mat(Fig_Number); +[~,Index] = sort(get(Fig_Object(Fig_Number == 0),'Name')); +Temp = find(Fig_Number == 0); +Fig_Number(Temp(Index)) = -(numel(Index):(-1):1); + +% ===== Logical distribution array ======================================== +Fig_Dist = true(1,numel(Fig_Object)); + +% ===== Simulink figures ================================================== +if strcmp(Simulink,'exclude') + Fig_Dist = (Fig_Number > 0); +end +if strcmp(Simulink,'only') + Fig_Dist = (Fig_Number < 0); +end + +% ===== Not =============================================================== +Fig_Dist(ismember(Fig_Number,Not)) = false; + +% ===== Only ============================================================== +if (~isempty(Only)) + Fig_Dist(~ismember(Fig_Number,Only)) = false; +end + +% ===== Number of figures ================================================= +nFig_Dist = sum(Fig_Dist); + +% ===== Return ============================================================ +if (nFig_Dist == 0) + return; +end + +% ========================================================================= +% ===== Monitor =========================================================== +% ========================================================================= + +% ===== Get monitor positions ============================================= +Monitors = get(0,'MonitorPositions'); +Monitors_Center = Monitors(:,1:2) + Monitors(:,3:4) / 2; +Monitors_Label = cell(size(Monitors,1),1); + +% ===== Center monitor ==================================================== +Temp = sum(abs(Monitors(:,1:2) - 1),2); +Trigger_C = find(Temp == min(Temp)); +Monitors_Label{Trigger_C} = 'C'; + +% ===== External monitors ================================================= +Dir = Monitors_Center - repmat(Monitors_Center(Trigger_C,:),size(Monitors_Center,1),1); +Theta = atan2(Dir(:,2),Dir(:,1)); +for i = 1:numel(Theta) + if (i ~= Trigger_C) + Monitors_Label{i} = Screens{(Theta(i) > [Screens{:,1}]) & (Theta(i) <= [Screens{:,2}]),3}; + end +end + +% ===== Select monitor ==================================================== +Monitor = Monitors(strcmp(Monitors_Label,Scr),:); +if (isempty(Monitor)) + if (strcmp(Scr,'Ext')) + if (size(Monitors,1) == 1) + warning('No external monitor could be found!'); + Monitor = Monitors(1,:); + else + Monitor = Monitors(find(~strcmp(Monitors_Label,'C'),1,'first'),:); + end + else + warning(['Screen ''%s'' could not be found! (Using ''C'' instead)\nThe following monitors were identified: %s.\n'... + 'If the function cannot find the monitor try and restart MATLAB - this will reset the monitor-matrix, which is being used by the function.'],... + Scr,sprintf('%s\b\b\b\b\b',sprintf('''%s'' and ',Monitors_Label{:}))); + Monitor = Monitors(strcmp(Monitors_Label,'C'),:); + if (isempty(Monitor)) + warning(['distFig cannot find matching monitors - using the first one!',... + '\nPlease report this problem to AndersSSimonsen@GMail.com, where you include this matrix:'],0); + disp(Monitors); + Monitor = Monitors(1,:); + end + end +end + +% ========================================================================= +% ===== Rows and columns ================================================== +% ========================================================================= + +Limit = Limits(strcmp(Limits(:,1),Pos),:); +if ((Rows == (-1)) && (Cols == (-1))) + AR_Ideal = 1.2; + AR_Mean = Inf; + for i = 1:50 + % ===== Exception ================================================= + if ((nFig_Dist + Offset) == 4) + Rows = 2; + Cols = 2; + break; + end + + % ===== Temporary rows and columns ================================ + Rows = i; + Cols = ceil((nFig_Dist + Offset) / Rows); + + % ===== Sizes ===================================================== + x = round(linspace(Limit{2}(1),Limit{2}(2),Cols + 1) * Monitor(3)) + 1; + y = round(linspace(Limit{3}(1),Limit{3}(2),Rows + 1) * (Monitor(4) - Taskbar_Height)) + Taskbar_Height + 1; + clear Size; + [Size(:,:,1),Size(:,:,2)] = meshgrid(diff(x),diff(y)); + + % ===== Output ==================================================== + AR_Mean_Prev = AR_Mean; + AR_Mean = mean(mean(Size(:,:,1) ./ Size(:,:,2))); + if (AR_Mean > AR_Ideal) + AR_Error = abs([AR_Ideal - AR_Mean_Prev,AR_Ideal - AR_Mean]); + if (AR_Error(1) < AR_Error(2)) + Rows = i - 1; + end + Cols = ceil((nFig_Dist + Offset) / Rows); + clear Size; + break; + end + end +elseif (Rows == (-1)) + Rows = ceil((nFig_Dist + Offset) / Cols); +elseif (Cols == (-1)) + Cols = ceil((nFig_Dist + Offset) / Rows); +end + +% ========================================================================= +% ===== Location matrix =================================================== +% ========================================================================= + +Sets = ceil(nFig_Dist / (Rows * Cols) + Offset); +Mat = nan(Rows,Cols,Sets); +if (Transpose) + Mat = permute(Mat,[2,1,3]); +end +if (isempty(Order)) + Order = (1:sum(Fig_Dist)); +end +Temp = sort(Fig_Number(Fig_Dist)); +if (max(Order) > numel(Temp)) + error('The ordering command is related to the input - not the actual figure numbers! Returning...'); +end +Mat((1:nFig_Dist) + Offset) = Temp(Order); +if (Transpose) + Mat = permute(Mat,[2,1,3]); +end +if (strcmp(Extra,'ignore')) + Sets = 1; +end + +% ========================================================================= +% ===== Position and size ================================================= +% ========================================================================= + +% ===== Grid ============================================================== +x = round(linspace(Limit{2}(1),Limit{2}(2),Cols + 1) * Monitor(3)) + 1; +y = round(linspace(Limit{3}(1),Limit{3}(2),Rows + 1) * (Monitor(4) - Taskbar_Height)) + Taskbar_Height + 1; + +% ===== Scale ============================================================= +if (isempty(Scale_Orig)) + Scale_Orig = [x(1),y(end)]; +end +if ((all(mod(Scale_Orig,1) == 0)) && (any(Scale_Orig > 1))) + ScaleOrig_Pixel = Scale_Orig; +else + ScaleOrig_Pixel = Monitor(3:4) .* Scale_Orig; +end +if (numel(Scale) == 1) + Scale(2) = Scale(1); +end +x = (x - ScaleOrig_Pixel(1)) * Scale(1) + ScaleOrig_Pixel(1); +y = (y - ScaleOrig_Pixel(2)) * Scale(2) + ScaleOrig_Pixel(2); + +% ===== Positon =========================================================== +[FPos(:,:,1),FPos(:,:,2)] = meshgrid(x(1:end-1),y(1:end-1)); +FPos(:,:,2) = flipud(FPos(:,:,2)); +FPos(:,:,1) = FPos(:,:,1) + Monitor(1) - 1; +FPos(:,:,2) = FPos(:,:,2) + Monitor(2) - 1; + +% ===== Size ============================================================== +[Size(:,:,1),Size(:,:,2)] = meshgrid(diff(x),diff(y)); + +% ===== Adjust ============================================================ +if (any(Adjust ~= 0)) + if (all(mod(Adjust,1) == 0)) + Adjust_Pixels = Adjust; + else + Adjust_Pixels = round(Adjust .* (Monitor(3:4) + [0,-Taskbar_Height])); + end +else + Adjust_Pixels = [0,0]; +end + +% ========================================================================= +% ===== Distribute figures ================================================ +% ========================================================================= + +for s = 1:Sets + for i = 1:size(Mat,1) + for j = 1:size(Mat,2) + if (~isnan(Mat(i,j,s))) + % ===== Position and figure =============================== + Pos_Temp = [squeeze(FPos(i,j,:))' + Adjust_Pixels,squeeze(Size(i,j,:))']; + gcf_Temp = Fig_Object(Fig_Number == Mat(i,j,s)); + gca_Temp = get(gcf_Temp,'CurrentAxes'); + + % ===== Freeze size ======================================= + if (FreezeSize) + Pos_Orig = get(gcf_Temp,'Position'); + Pos_Temp(2) = Pos_Temp(2) + Pos_Temp(4) - Pos_Orig(4); + Pos_Temp(3:4) = Pos_Orig(3:4); + end + + % ===== Focus figures to bring to front =================== + figure(gcf_Temp); + + % ===== Set units ========================================= + if (~isempty(Menu)) + set(gcf_Temp,'MenuBar',Menu); + end + if (Mat(i,j,s) < 0) + drawnow; + end + gca_Units_Prev = get(gca_Temp,'Units'); + gcf_Units_Prev = get(gcf_Temp,'Units'); + + % ===== Distribute figure ================================= + set(gca_Temp,'Units','normalized'); + set(gcf_Temp,'Units','pixels','OuterPosition',Pos_Temp); + set(gcf_Temp,'Units',gcf_Units_Prev); + + % ===== Apply tight ======================================= + if ((Tight) && (Mat(i,j,s) > 0)) + drawnow; + set(gca_Temp,'Position',[get(gca_Temp,'TightInset') * eye(4,2),1 - get(gca_Temp,'TightInset') * [1,0,1,0;0,1,0,1]']); + end + set(gca_Temp,'Units',gca_Units_Prev); + end + end + end +end + +% ========================================================================= +% ===== Check for new updates ============================================= +% ========================================================================= + +% This feature check for a new version of distFig. It is only an attempt, +% so it might not work without bugs. It can be disables by setting the +% variable below called "Update" to "false" or by deleting this whole +% section - this won't affect the rest of the function. +% +% It generates a .txt file called "distFig_Update.txt" located in the same +% folder as distFig.m, which holds the date for the last time the function +% checked for updates. In order to not check for updates all the time, the +% function only does this ONCE every 7 days (variable "Check_Interval"), +% and at max once for every started MATLAB session. Also, it uses appdata, +% so if you're using this as well, it might display messages multiple times +% within a session, if an update is available, if appdata is deleted. +% +% The update feature works by scanning the file exchange page for distFig +% for a string value called "datePublished", and compares it to the date of +% the current version (variable "Date_Current"). If these are not the same, +% a hyperlink will be shown to the function page on file exchange page. It +% thus depends on a very specific format, which might differ(?) from +% different languages - I'm not aware whether this is true, so please +% contact me on AndersSSimonsen@GMail.com if you find this to be true, so I +% can fix the feature. :) + +% ===== DISABLE THE UPDATE FEATURE BY SETTING THIS VALUE TO false ========= +Update = true; +% ===== DISABLE THE UPDATE FEATURE BY SETTING THIS VALUE TO false ========= + +if (Update) + try + % ===== Initialize ================================================ + Date_Current = [2015,03,31]; + Check_Interval = 7; + Check = false; + + % ===== Path of update file ======================================= + Path = mfilename('fullpath'); + Path = sprintf('%s\\distFig_Update.txt',Path(1:end-numel('\distFig'))); + + if (isempty(getappdata(0,'distFig_Update'))) + % ===== Set appdata =========================================== + setappdata(0,'distFig_Update',1); + + % ===== Read update file ====================================== + File_ID = fopen(Path,'r'); + Clock = clock; + Date_Today = Clock(1:3); + if (File_ID == (-1)) + % ===== Generate update file ============================== + File_ID = fopen(Path,'w'); + fprintf(File_ID,'%d %d %d',Date_Today(1),Date_Today(2),Date_Today(3)); + fclose(File_ID); + Check = true; + else + % ===== Read last date ==================================== + Data = textscan(File_ID,'%s','delimiter',''); + if (strcmp(Data{1},'DISABLE')) + Check = false; + else + Date_Last_Checked = cell2mat(textscan(Data{1}{1},'%f %f %f')); + + % ===== Write new date ================================ + File_ID = fopen(Path,'w'); + fprintf(File_ID,'%d %d %d',Date_Today(1),Date_Today(2),Date_Today(3)); + fclose(File_ID); + + % ===== Check for new updates? ======================== + Days_Elapsed = etime([Date_Today,zeros(1,3)],[Date_Last_Checked,zeros(1,3)]) / (60^2 * 24); + if (Days_Elapsed > Check_Interval) + Check = true; + end + end + end + + % ===== Check for updates ===================================== + if (Check) + % ===== Read File Exchange date =========================== + Data = urlread('http://se.mathworks.com/matlabcentral/fileexchange/37176-distribute-figures'); + Temp = Data((-1:100) + regexp(Data,'datePublished')); + Date_Online = cell2mat(textscan(Temp(26:end),'%f-%f-%f')); + + % ===== New update available? ============================= + if (~all(Date_Current == Date_Online)) + setappdata(0,'distFig_Update',2); + setappdata(0,'distFig_Online_Date',Date_Online); + end + end + end + + % ===== Message =================================================== + if (getappdata(0,'distFig_Update') == 2) + try + % ===== New version ======================================= + Date_Online = getappdata(0,'distFig_Online_Date'); + fprintf('New update available for distFig! '); + disp('Download here') + fprintf('\tCurrent version date:\t%02.0f-%02.0f-%02.0f\n',Date_Current(1),Date_Current(2),Date_Current(3)); + fprintf('\tOnline version date:\t%02.0f-%02.0f-%02.0f\n',Date_Online(1),Date_Online(2),Date_Online(3)); + + % ===== Postpone ========================================== + fprintf('Click here to postpone this message for %d days: ',Check_Interval); + Command = sprintf([... + 'setappdata(0,''distFig_Update'',1);',... + 'fprintf(''distFig update message postponed for %d days!\\n'');' + ],Check_Interval); + disp(sprintf('Postpone update message',Command)); %#ok + + % ===== Disable =========================================== + fprintf('Click here to disable the update feature forever: '); + Command = sprintf([... + 'File_ID = fopen(''%s'',''w'');',... + 'fprintf(File_ID,''DISABLE'');',... + 'fclose(File_ID);',... + 'setappdata(0,''distFig_Update'',1);',... + 'fprintf(''distFig update feature disabled!\\n'');' + ],Path); + disp(sprintf('Disable update feature',Command)); %#ok + catch + setappdata(0,'distFig_Update',1); + end + end + catch Error %#ok + % ===== Error handling ============================================ + try + Path_Error = [Path(1:end-numel([filesep 'distFig_Update.txt'])), [filesep 'distFig_Error.mat']]; + fprintf([ + 'There was an error in distFig when checking for updates.',... + '\nPlease report this to AndersSSimonsen@GMail.com by sending the error-file "distFig_Error.mat" located in %s. :)\n\n'],Path_Error); + AppData = getappdata(0); %#ok + save(Path_Error); + catch + end + end +end \ No newline at end of file diff --git a/beamtime/exportFigureData.m b/beamtime/exportFigureData.m new file mode 100644 index 0000000..460e250 --- /dev/null +++ b/beamtime/exportFigureData.m @@ -0,0 +1,263 @@ +function exportFigureData(figHandle) +%exportFigureData Export line and scatter plot data to ASCII files +% +% This function extracts x and y data from all line and scatter plots in a +% figure (including tiled layouts) and saves each data series to a separate +% ASCII file. File names are constructed from the figure title, tile title +% (if in a tiled layout), axis labels, and legend entries. Data is limited +% to the current x-axis scale range. +% +% SYNTAX: +% exportFigureData() % Uses current active figure +% exportFigureData(figHandle) % Uses specified figure handle +% +% INPUTS: +% figHandle : figure handle (optional) +% If empty or omitted, the current active figure (gcf) is used. +% +% OUTPUT: +% ASCII files are saved in the current working directory, named as: +% ___vs__.txt +% For non-tiled figures: +% __vs__.txt +% Each file contains two columns: X-data and Y-data (tab-delimited) + +% Handle input: use current figure if none provided +if nargin == 0 || isempty(figHandle) + figHandle = gcf; +end + +% Get figure title (use sgtitle if available, else Name) +figTitle = ''; +sgtitleObjs = findall(figHandle, 'type', 'text', 'Tag', 'sgtitle'); +if ~isempty(sgtitleObjs) + figTitle = get(sgtitleObjs(1), 'String'); + if iscell(figTitle) + figTitle = figTitle{1}; + end +end +if isempty(figTitle) + figTitle = get(figHandle, 'Name'); +end +if isempty(figTitle) + figTitle = 'figure'; +end +figTitle = sanitizeFilename(figTitle); % Sanitize figure title + +% Get all axes in the figure (works for both regular and tiled layouts) +allAxes = findall(figHandle, 'type', 'axes'); +if isempty(allAxes) + warning('exportFigureData:NoAxes', 'No axes found in figure.'); + return; +end + +% Counters for unnamed line and scatter objects +lineCounter = 0; +scatterCounter = 0; + +% Loop over all axes +for ii = 1:length(allAxes) + ax = allAxes(ii); + + % Skip legend axes and colorbar axes + if strcmp(ax.Tag, 'legend') || strcmp(ax.Tag, 'colorbar') + continue + end + + % Check if this axes uses yyaxis + isYYAxis = isprop(ax, 'YAxis') && numel(ax.YAxis) == 2; + if isYYAxis + sidesToProcess = {'left', 'right'}; + else + sidesToProcess = {'single'}; + end + + % Build a legend object if it exists (for extracting labels) + legendObj = legend(ax); + legendLabels = {}; + if ~isempty(legendObj) + legendLabels = legendObj.String; + if ischar(legendLabels) + legendLabels = {legendLabels}; + end + end + + % Initialize a counter for matching legend entries to graphics objects + graphicsIndex = 0; + + % Loop through both sides if yyaxis is used, otherwise process normally once + for s = 1:length(sidesToProcess) + + if isYYAxis + yyaxis(ax, sidesToProcess{s}); % Switch active side to expose its lines and label + end + + % 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); + + % 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); + + % 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 + end + end +end +fprintf('Data export complete.\n'); +end + +% ========================================================================= +% Helper function: sanitize strings for use in filenames +% ========================================================================= +function cleanStr = sanitizeFilename(str) +%sanitizeFilename Remove or replace characters invalid in filenames +% +% Replaces spaces, slashes, backslashes, colons, asterisks, parentheses, +% and other problematic characters with underscores. +cleanStr = str; +cleanStr = strrep(cleanStr, ' ', '_'); +cleanStr = strrep(cleanStr, '/', '_'); +cleanStr = strrep(cleanStr, '\', '_'); +cleanStr = strrep(cleanStr, ':', '_'); +cleanStr = strrep(cleanStr, '*', '_'); +cleanStr = strrep(cleanStr, '?', '_'); +cleanStr = strrep(cleanStr, '"', '_'); +cleanStr = strrep(cleanStr, '<', '_'); +cleanStr = strrep(cleanStr, '>', '_'); +cleanStr = strrep(cleanStr, '|', '_'); +cleanStr = strrep(cleanStr, '(', '_'); +cleanStr = strrep(cleanStr, ')', '_'); +cleanStr = strrep(cleanStr, '.', '_'); % Optional: replace dots +end \ No newline at end of file diff --git a/beamtime/getPsiColors.m b/beamtime/getPsiColors.m new file mode 100644 index 0000000..c23bc81 --- /dev/null +++ b/beamtime/getPsiColors.m @@ -0,0 +1,24 @@ +function PsiFarbe = getPsiColors() +% Returns a struct with PSI corporate identity 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; + +end \ No newline at end of file diff --git a/beamtime/importLabViewIssfile.m b/beamtime/importLabViewIssfile.m new file mode 100644 index 0000000..e5a1645 --- /dev/null +++ b/beamtime/importLabViewIssfile.m @@ -0,0 +1,107 @@ +function TableLabViewIss = importLabViewIssfile(filenameOrFolder, dataLines) +%IMPORTFILE Import data from a text file or folder +% TableLabViewIss = IMPORTFILE(FILENAME) reads data from text +% file FILENAME and returns the data as a table. +% +% TableLabViewIss = IMPORTFILE(FOLDER) reads all .txt files from +% FOLDER, merges them into a single table (sorted by datetime). +% +% TableLabViewIss = IMPORTFILE(FILE, DATALINES) reads data +% for the specified row interval(s) of text file FILENAME. +% +% Example (single file): +% TableLabViewIss = importLabViewIssfile("/Users/bartels-rausch/Desktop/17-12-2025_09-14datafile.txt", [2, Inf]); +% +% Example (folder): +% TableLabViewIss = importLabViewIssfile("/Users/bartels-rausch/Desktop/DataFolder"); +% +% See also READTABLE. + +%% Input handling + +% If dataLines is not specified, define defaults +if nargin < 2 + dataLines = [2, Inf]; +end + +%% Check if input is a file or folder +if isfolder(filenameOrFolder) + % Process all .txt files in the folder + folderPath = filenameOrFolder; + fileList = dir(fullfile(folderPath, '*.txt')); + + if isempty(fileList) + error('No .txt files found in folder: %s', folderPath); + end + + fprintf('Found %d .txt files in folder. Processing...\n', length(fileList)); + + % Initialize empty table + TableLabViewIss = table(); + + % Loop through each file + for i = 1:length(fileList) + filepath = fullfile(folderPath, fileList(i).name); + fprintf(' [%d/%d] Processing: %s\n', i, length(fileList), fileList(i).name); + + % Import this file + tbl_temp = importSingleFile(filepath, dataLines); + + % Append to main table + if isempty(TableLabViewIss) + TableLabViewIss = tbl_temp; + else + TableLabViewIss = [TableLabViewIss; tbl_temp]; + end + end + + % Sort by datetime + [~, sortIdx] = sort(TableLabViewIss.datetime); + TableLabViewIss = TableLabViewIss(sortIdx, :); + + fprintf('Merged %d files into single table with %d rows.\n', length(fileList), height(TableLabViewIss)); + +else + % Process single file + TableLabViewIss = importSingleFile(filenameOrFolder, dataLines); +end + +end + +%% Helper function to import a single file +function tbl = importSingleFile(filename, dataLines) +% Import options setup +opts = delimitedTextImportOptions("NumVariables", 10); + +% Specify range and delimiter +opts.DataLines = dataLines; +opts.Delimiter = "\t"; + +% Specify column names and types +opts.VariableNames = ["date_DD_MM_JJJJ_", "time_hh_mm_ss_", "pressure_cell_hPa_", "temperature__C_", "x_mm_", "y_mm_", "z_mm_", "Tsealing__C_", "flowCryo_l_min_", "pOut_hPa_"]; +opts.VariableTypes = ["datetime", "duration", "double", "double", "double", "double", "double", "double", "double", "double"]; + +% Specify file level properties +opts.ExtraColumnsRule = "ignore"; +opts.EmptyLineRule = "read"; + +% Specify variable properties +opts = setvaropts(opts, "date_DD_MM_JJJJ_", "InputFormat", "dd.MM.yyyy"); +opts = setvaropts(opts, "time_hh_mm_ss_", "InputFormat", "hh:mm:ss.S"); + +% Import the data as regular table +tbl = readtable(filename, opts); + +% Merge date + time into a single datetime column +tbl.datetime = tbl.date_DD_MM_JJJJ_ + tbl.time_hh_mm_ss_; + +% Set the datetime display format to show date and time +tbl.datetime.Format = 'dd.MM.yyyy HH:mm:ss.SSS'; + +% Remove the original date and time columns +tbl(:, ["date_DD_MM_JJJJ_", "time_hh_mm_ss_"]) = []; + +% Move datetime to be the first column +tbl = movevars(tbl, "datetime", "Before", 1); + +end diff --git a/beamtime/nappFileList.m b/beamtime/nappFileList.m index 21dcc44..ef5abb0 100755 --- a/beamtime/nappFileList.m +++ b/beamtime/nappFileList.m @@ -460,7 +460,7 @@ else % Read information about the structure of the HDF5 file % This tells us what groups (datasets) are stored in the file fileinfo = h5info(current_file); - + % Extract the names of all groups at the root level % Example: {'/scan_1', '/scan_2', '/logs', '/__DATA_TYPES__'} @@ -542,9 +542,9 @@ if ~isempty(TmpFileList{1}) % 1. INITIALIZE MASTER TEMPLATE (Strict Definition) % ================================================================= % Every variable MUST be declared here so the struct sizes match perfectly - + current_file_info.creationDate = [NaN NaN NaN NaN NaN NaN]; - current_file_info.lastModifiedDatenum = current_file_info.datenum; + current_file_info.lastModifiedDatenum = current_file_info.datenum; current_file_info.lastModifiedDatestr = current_file_info.date; current_file_info.image = {[]}; current_file_info.imageRoh = {[]}; @@ -566,11 +566,11 @@ if ~isempty(TmpFileList{1}) current_file_info.scientaSequenceIterations = NaN; current_file_info.importDate = datestr(now); try - current_file_info.analysisDir = file.mFile; - catch - current_file_info.analysisDir = pwd; + current_file_info.analysisDir = file.mFile; + catch + current_file_info.analysisDir = pwd; end - current_file_info.sample = ''; + current_file_info.sample = ''; % ================================================================= % --- Case A: Scienta (.ibw) Files --- % ================================================================= @@ -619,7 +619,7 @@ if ~isempty(TmpFileList{1}) if isempty(ProcessedXPS), ProcessedXPS = current_file_info; else, ProcessedXPS = [ProcessedXPS; current_file_info]; end %#ok end - % ================================================================= + % ================================================================= % --- Case B: Specs (.xy) Files --- % ================================================================= elseif endsWith(current_file_info.name, '.xy', 'IgnoreCase', true) @@ -628,10 +628,10 @@ if ~isempty(TmpFileList{1}) try, osDateVec = datevec(datetime(cDateOS, 'InputFormat', 'dd-MMM-yyyy HH:mm:ss')); catch, try osDateVec = datevec(mDateOS); catch, osDateVec = [NaN NaN NaN NaN NaN NaN]; end, end if isempty(osDateVec) || size(osDateVec, 1) == 0, osDateVec = [NaN NaN NaN NaN NaN NaN]; end - + fileLines = readlines(current_path); regionStarts = find(startsWith(fileLines, '# Region:')); - + % --- 1. PARSE GLOBAL HEADER (Before First Region) --- % SpecsLab moved many parameters to the top of the file in newer versions globalScanMode = ''; @@ -640,42 +640,42 @@ if ~isempty(TmpFileList{1}) globalWorkFunction = NaN; globalExEnergy = NaN; globalSample = ''; - + if ~isempty(regionStarts) globalHeader = fileLines(1:regionStarts(1)-1); for L = 1:length(globalHeader) lineStr = strtrim(globalHeader(L)); lineStr = regexprep(lineStr, '^#\s+', '# '); % Normalize spaces after # - + if startsWith(lineStr, '# Scan Mode:') globalScanMode = strtrim(extractAfter(lineStr, '# Scan Mode:')); - elseif startsWith(lineStr, '# Analyzer Lens Voltage:') + elseif startsWith(lineStr, '# Analyzer Lens Voltage:') globalLensMode = strtrim(extractAfter(lineStr, '# Analyzer Lens Voltage:')); elseif startsWith(lineStr, '# Analyzer Lens:') globalLensMode = strtrim(extractAfter(lineStr, '# Analyzer Lens:')); elseif startsWith(lineStr, '# Analyzer Slit:') globalAnalyzerSlit = strtrim(extractAfter(lineStr, '# Analyzer Slit:')); - elseif startsWith(lineStr, '# Work Function:') + elseif startsWith(lineStr, '# Work Function:') globalWorkFunction = sscanf(strtrim(extractAfter(lineStr, '# Work Function:')), '%f'); elseif startsWith(lineStr, '# Eff. Workfunction:') globalWorkFunction = sscanf(strtrim(extractAfter(lineStr, '# Eff. Workfunction:')), '%f'); elseif startsWith(lineStr, '# Excitation Energy:') globalExEnergy = sscanf(strtrim(extractAfter(lineStr, '# Excitation Energy:')), '%f'); - elseif startsWith(lineStr, '# Comment:') + elseif startsWith(lineStr, '# Comment:') globalSample = strtrim(extractAfter(lineStr, '# Comment:')); - % ========================================================================= - % OFFLINE XPS BUG FIX (Mg vs Al Source) - % If the file lacks "Monochromator" in the header and the energy is - % incorrectly tagged as ~1453.6 or 1486.6, overwrite it to 1253.6 (Mg Ka). - % ========================================================================= - if ~any(contains(string(fileLines), 'Monochromator', 'IgnoreCase', true)) + % ========================================================================= + % OFFLINE XPS BUG FIX (Mg vs Al Source) + % If the file lacks "Monochromator" in the header and the energy is + % incorrectly tagged as ~1453.6 or 1486.6, overwrite it to 1253.6 (Mg Ka). + % ========================================================================= + if ~any(contains(string(fileLines), 'Monochromator', 'IgnoreCase', true)) % Checking both 1453.6 (as requested) and 1486.61 (as seen in your file) if (abs(globalExEnergy - 1453.61) < 1) || (abs(globalExEnergy - 1486.61) < 1) - globalExEnergy = 1253.6; + globalExEnergy = 1253.6; end end - % ========================================================================= - + % ========================================================================= + end end end @@ -685,7 +685,7 @@ if ~isempty(TmpFileList{1}) startIdx = regionStarts(r); if r < length(regionStarts), endIdx = regionStarts(r+1)-1; else, endIdx = length(fileLines); end regionBlock = fileLines(startIdx:endIdx); - + % ===================================================== % 2. Extract Metadata % ===================================================== @@ -696,7 +696,7 @@ if ~isempty(TmpFileList{1}) regionEntry.xRayEkin_eV = globalExEnergy; regionEntry.scientaPassEnergy_eV = NaN; regionEntry.scientaDwellTime_ms = NaN; - + % Inherit defaults from Global Header regionEntry.scientaAcquisitionMode = globalScanMode; regionEntry.scientaLensMode = globalLensMode; @@ -706,14 +706,14 @@ if ~isempty(TmpFileList{1}) if ~isempty(globalSample) regionEntry.sample = globalSample; end - + % Scan the header lines directly for L = 1:min(150, length(regionBlock)) % Metadata is always at the top lineStr = strtrim(regionBlock(L)); if isempty(lineStr) || ~startsWith(lineStr, '#'), continue; end - + lineStr = regexprep(lineStr, '^#\s+', '# '); % Normalize spaces after # - + if startsWith(lineStr, '# Region:') rName = strtrim(extractAfter(lineStr, '# Region:')); elseif startsWith(lineStr, '# Excitation Energy:') @@ -723,7 +723,7 @@ if ~isempty(TmpFileList{1}) % ========================================================================= if ~any(contains(string(fileLines), 'Monochromator', 'IgnoreCase', true)) if (abs(regionEntry.xRayEkin_eV - 1453.6) < 1) || (abs(regionEntry.xRayEkin_eV - 1486.61) < 1) - regionEntry.xRayEkin_eV = 1253.6; + regionEntry.xRayEkin_eV = 1253.6; end end % ========================================================================= @@ -761,22 +761,22 @@ if ~isempty(TmpFileList{1}) regionEntry.sample = strtrim(extractAfter(lineStr, '# Comment:')); end end - + % --- Format Name & Region --- regionEntry.regionName = rName; - cleanRName = strrep(rName, ' ', '_'); + cleanRName = strrep(rName, ' ', '_'); % Write the original OS file name regionEntry.originalFileName = current_file_info.name; - + % Prevent identical filenames by appending _Reg1, _Reg2 if multiple regions exist if length(regionStarts) > 1 regionEntry.name = sprintf('%04d_%s_Reg%d_%s.xy', specID, cleanRName, r, exEnergyStr); else regionEntry.name = sprintf('%04d_%s_%s.xy', specID, cleanRName, exEnergyStr); end - - + + % 2. Extract Data hIdx = find(startsWith(strtrim(regionBlock), '# Cycle:')); @@ -882,7 +882,7 @@ for ii = 1:size(TmpFileList{2},1) % do this for each list element tmpCurrentFile = fullfile(TmpFileList{2}(ii).folder, TmpFileList{2}(ii).name); % ========================================================== % IN-MEMORY DATE RESTORATION - % Check if this file was created by the splitter. If yes, + % Check if this file was created by the splitter. If yes, % retrieve its historical dates from the memory map! % ========================================================== if exist('splitFileDatesMap', 'var') && isKey(splitFileDatesMap, TmpFileList{2}(ii).name) @@ -893,7 +893,7 @@ for ii = 1:size(TmpFileList{2},1) % do this for each list element % Otherwise, just read the dates directly from the OS [cDateOS, mDateOS] = getOSDates(tmpCurrentFile); end - + % START TIME: OS Creation Date (or mapped historical creation) TmpFileList{2}(ii).creationDate = datevec(cDateOS); % END TIME: OS Modification Date (or mapped historical modification) @@ -921,7 +921,7 @@ for ii = 1:size(TmpFileList{2},1) % do this for each list element end - + % import Photon Energy (cols) try TmpXrayEkin = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/energy'); % X-ray excitation energy; Photon energy in eV @@ -1026,14 +1026,21 @@ for ii = 1:size(TmpFileList{2},1) % do this for each list element tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/CenterEnergy'); % in eV end end + % fix for very early Phoenix data (08.2026) + if tmp(1)==0 + clearvars tmp + tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/LowEnergy'); % in eV + tmp2 = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/HighEnergy'); + TmpFileList{2}(ii).scientaEkinRange_eV = [tmp(1) tmp2(1)]; + else + TmpFileList{2}(ii).scientaEkinRange_eV = [double(tmp(1))-TmpFileList{2}(ii).scientaPassEnergy_eV/20 double(tmp(1))+TmpFileList{2}(ii).scientaPassEnergy_eV/20]; + end - 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))' }; + TmpFileList{2}(ii).scientaEkinStep_eV = NaN; else % NEXAFS kinetic energy window - scan mode try @@ -1060,11 +1067,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 : ... + TmpFileList{2}(ii).scientaEkin_eV = { ... + (TmpFileList{2}(ii).scientaEkinRange_eV(1) : ... + TmpFileList{2}(ii).scientaEkinStep_eV : ... TmpFileList{2}(ii).scientaEkinRange_eV(2))' }; - end + end clearvars tmp % read I_0 @@ -1200,7 +1207,7 @@ for ii = 1:size(TmpFileList{3},1) % do this for each picture in the list % Get OS Dates tmpCurrentFile = fullfile(TmpFileList{3}(ii).folder, TmpFileList{3}(ii).name); [cDateOS, ~] = getOSDates(tmpCurrentFile); - + % START TIME: OS Creation Date TmpFileList{3}(ii).creationDate = datevec(cDateOS); % END TIME: Robust OS Modification Date @@ -1281,12 +1288,12 @@ if matches(p.Results.importLabView,'on') try % Get list of all files in that folder - TmpFileList = [ ... + TmpFileList = [ ... dir([dataPath_LabView filesep '**' filesep 'logfile*.txt']); ... dir([dataPath_LabView filesep '**' filesep '*datafile.txt'])]; if ~isempty(TmpFileList) keep = ~startsWith({TmpFileList.name}, {'.','~'}) & ... - ~startsWith({TmpFileList.name}, 'LabView_T_RH', 'IgnoreCase', true); + ~startsWith({TmpFileList.name}, 'LabView_T_RH', 'IgnoreCase', true); TmpFileList = TmpFileList(keep); [~, iu] = unique(fullfile({TmpFileList.folder}, {TmpFileList.name}), 'stable'); TmpFileList = TmpFileList(iu); @@ -1338,7 +1345,7 @@ if matches(p.Results.importLabView,'on') if ~isempty(tmp.Data) % only do this if LabView data are not missing (because data logging was off). % Cryo-holder set T (Sample holder temperature is in field ts.Data:1) - ListFiles.sampleTemp_dC(ii) = tmp.Data(1,1); % in °C (only get data in first row of tmp) + ListFiles.sampleTemp_dC(ii) = tmp.Data(1,1); % in °C (only get data in first row of tmp) % Cell pressure % (TODO: Pick correct Baratron read out: either field: ts.Data 4,8,and 9) ListFiles.cellPressure_mbar(ii) = tmp.Data(1,4); % in mbar @@ -1554,11 +1561,11 @@ end % % col 6: Baratron 2 - cell - 10mbar max - mbar -> ts.Data:8 % % col 7: Baratron 3 - cell - 0.05mbar max - mbar -> ts.Data:9 % % col 8: Baratron 4 - - mbar -% % col 9: Temp.Ice Sample - °C -> ts.Data:1 +% % col 9: Temp.Ice Sample - °C -> ts.Data:1 % % col 10: Temp.Heated Sample % % col 11: none -% % col 12: Temp. Reservoir "Cooler 1" - °C -> ts.Data:5 -% % col 13: Temp. Reservoir "Cooler 2" - °C -> ts.Data:6 +% % col 12: Temp. Reservoir "Cooler 1" - °C -> ts.Data:5 +% % col 13: Temp. Reservoir "Cooler 2" - °C -> ts.Data:6 % % col 14: Flow He(cryoholder) - l/min -> ts.Data:7 % % col 15: none % % col 16: none @@ -1572,10 +1579,10 @@ end % % col 6: Baratron 2 - cell - 10mbar max - mbar -> ts.Data:8 % % col 7: Baratron 3 - cell - 0.05mbar max - mbar -> ts.Data:9 % % col 8: Baratron 4 - - mbar -% % col 9: Temp.Ice Sample - °C -> ts.Data:1 +% % col 9: Temp.Ice Sample - °C -> ts.Data:1 % % col 10: Temp.Heated Sample -% % col 11: Temp. Reservoir "Cooler 1" - °C -> ts.Data:5 -% % col 12: Temp. Reservoir "Cooler 2" - °C -> ts.Data:6 +% % col 11: Temp. Reservoir "Cooler 1" - °C -> ts.Data:5 +% % col 12: Temp. Reservoir "Cooler 2" - °C -> ts.Data:6 % % col 13: Flow He(cryoholder) - l/min -> ts.Data:7 % % col 14: Flow gas 1 % % col 15: Pressure chamber - mbar @@ -1583,11 +1590,11 @@ end % % col 17: y-position of cryoholder - mm -> ts.Data:11 % % col 18: z-position of cryoholder - mm -> ts.Data:12 % % col 19: nothing -% % col 20: Temp.Sealing - °C -> ts.Data:13 -% -% +% % col 20: Temp.Sealing - °C -> ts.Data:13 +% +% % %% -% +% % % Create an InputParser object % p = inputParser; % % Add inputs to the scheme. @@ -1596,24 +1603,24 @@ end % addOptional(p,'appendToStructure',defaultAppendToStructure,@isstruct); % % Parse the inputs. % parse(p,fullFileName,varargin{:}) -% +% % %% % % get filedate % [~,name,~] = fileparts(p.Results.fullFileName); % tmp = strsplit(name,'_PC'); % filedate = datenum(tmp{1},'yyyy-mm-dd_HH.MM.SS'); -% +% % clearvars name tmp -% +% % %% % % parse structure to append to -% +% % if isstruct(p.Results.appendToStructure) % LabView = p.Results.appendToStructure; % end % %% % if datenum(filedate) <= datenum('04-Mar-2019 13:30:00') -% +% % fid = fopen(p.Results.fullFileName); % data = textscan(fid, ... % '%s %s %f %f %f %f %f %f %f %f %f %f %f %f %f %f',... @@ -1628,14 +1635,14 @@ end % LabView.(['ts_' datestr(datenum([char(data{1}{1}), char(data{2}{1})],'dd.mm.yyyyHH:MM:SS'),'YYYYmmdd_HHMM')]).Name ... % = 'Readout Napp Chamber raw signal'; % clear fid ans formatOut nans t x -% +% % elseif datenum(filedate) >= datenum('15-Sep-2021 01:00:00') && datenum(filedate) <= datenum('01-Apr-2023 01:00:00') % % during this time, date and time were in american format. % fid = fopen(p.Results.fullFileName); % data = textscan(fid, ... % '%s %s %s %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f',... % 'HeaderLines',1); % -% +% % fclose(fid); % % the date formating in the dat file is really messed up. Try to make % % it consitent with MATLAB. The problem is that MATLAB does not @@ -1648,7 +1655,7 @@ end % newTimeSplit = cellfun(@(x) split(char(x),':'),data{2},'UniformOutput', false); % % combine both with the am/pm string in column 3 to a new datestring % customDateStr = cellfun(@(x,y,z) [datestr(datenum([str2double(x{3}),str2double(x{1}),str2double(x{2}),str2double(y{1}),str2double(y{2}),str2double(y{3})])), ' ', char(z)], newDateSplit,newTimeSplit,data{3}, 'uniformOutput',false) ; -% +% % % define time series % LabView.(['ts_' datestr(customDateStr{1},'YYYYmmdd_HHMM')]) ... % = timeseries([data{10} data{4} data{5} data{6} data{12} ... @@ -1657,10 +1664,10 @@ end % LabView.(['ts_' datestr(customDateStr{1},'YYYYmmdd_HHMM')]).Name ... % = 'Readout Napp Chamber raw signal'; % clearvars fid ans formatOut nans t x new* custom* -% -% +% +% % elseif datenum(filedate) >= datenum('01-Apr-2023 02:00:00') -% +% % fid = fopen(p.Results.fullFileName); % data = textscan(fid, ... % '%s %s %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f',... @@ -1669,17 +1676,17 @@ end % % PM IS MISSING and some changes to the LabView file. Confirm with Luca....) % fclose(fid); % % define time series -% +% % LabView.(['ts_' datestr(datenum([char(data{1}{1}), char(data{2}{1})],'dd.mm.yyyyHH:MM:SS'),'YYYYmmdd_HHMM')]) ... % = timeseries([data{9} data{3} data{4} data{5} data{11} ... % data{12} data{20} data{6} data{7} data{15} data{16} data{17} data{19}] , ... % datestr(datenum([char(data{1}), char(data{2})],... % 'dd.mm.yyyyHH:MM:SS'),0)); -% +% % LabView.(['ts_' datestr(datenum([char(data{1}{1}), char(data{2}{1})],'dd.mm.yyyyHH:MM:SS'),'YYYYmmdd_HHMM')]).Name ... % = 'Readout Napp Chamber raw signal'; % clear fid ans formatOut nans t x -% +% % else % fid = fopen(p.Results.fullFileName); % data = textscan(fid, ... @@ -1687,13 +1694,13 @@ end % 'HeaderLines',1); % data file has 2 text columns, followed by 14 numeric columns % fclose(fid); % % define time series -% +% % LabView.(['ts_' datestr(datenum([char(data{1}{1}), char(data{2}{1})],'dd.mm.yyyyHH:MM:SS'),'YYYYmmdd_HHMM')]) ... % = timeseries([data{9} data{3} data{4} data{5} data{11} ... % data{12} data{13} data{6} data{7} data{16} data{17} data{18} data{20}] , ... % datestr(datenum([char(data{1}), char(data{2})],... % 'dd.mm.yyyyHH:MM:SS'),0)); -% +% % LabView.(['ts_' datestr(datenum([char(data{1}{1}), char(data{2}{1})],'dd.mm.yyyyHH:MM:SS'),'YYYYmmdd_HHMM')]).Name ... % = 'Readout Napp Chamber raw signal'; % clear fid ans formatOut nans t x @@ -1885,7 +1892,7 @@ if numel(groupnames) > 3 fileObj.LastWriteTime = System.DateTime.Parse(orig_mDateOS); catch end - + elseif ismac || isunix % Mac/Linux: Use 'touch' to restore Modification time try @@ -1893,10 +1900,10 @@ if numel(groupnames) > 3 system(sprintf('touch -m -t %s "%s"', timestamp_str, outname)); catch end - % NOTE: Mac OS makes it incredibly difficult to change the Birth (Creation) - % time via standard commands. However, because we restored the Modification - % time perfectly, our "Paradox Check" in the main loop will automatically - % step in, realize Creation is newer than Modification, and fix the + % NOTE: Mac OS makes it incredibly difficult to change the Birth (Creation) + % time via standard commands. However, because we restored the Modification + % time perfectly, our "Paradox Check" in the main loop will automatically + % step in, realize Creation is newer than Modification, and fix the % Creation date for us in the data table! end % ========================================================== @@ -1914,7 +1921,7 @@ else % disp('File has three or fewer groups. No processing required.'); end -end +end @@ -1946,10 +1953,10 @@ elseif ismac || isunix [status, cmdOut] = system(['stat -f "%SB" -t "%d-%b-%Y %H:%M:%S" "', fullPath, '"']); if status == 0 creationDateOS = strtrim(cmdOut); - + % ========================================================== % macOS COPIED FILE WORKAROUND - % If copied/downloaded, macOS sets Birth Time to 'today' but + % If copied/downloaded, macOS sets Birth Time to 'today' but % keeps the old Modification Time. If Creation > Modification, % use the older Modification time as the true origin! % ========================================================== diff --git a/beamtime/nappImage.m b/beamtime/nappImage.m new file mode 100644 index 0000000..a661998 --- /dev/null +++ b/beamtime/nappImage.m @@ -0,0 +1,54 @@ + function nappImage(DataTable,varargin) + % + % no longer used. + +% Define conditions and defaults of input +defaultTableVariable = {'image'}; +expectedTableVariable = {'image','imageRoh'}; +defaultTableSelection = true(size(DataTable,1)); + + +% Parse input +p = inputParser; + addRequired(p,'DataTable',@istable); + addOptional(p,'tableSelection',defaultTableSelection,@islogical); + addParameter(p,'tableVariable',defaultTableVariable,... + @(x) any(validatestring(x,expectedTableVariable))); +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 + + for ii = 1:size(p.Results.DataTable(p.Results.tableSelection,:),1) + figure('Name',p.Results.DataTable(p.Results.tableSelection,:).name{ii}); + + % Create axes + hold(gca,'on'); + % Try to plot image with x and y + try + image(p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii},p.Results.DataTable(p.Results.tableSelection,:).scientaEkin_eV{ii},... + p.Results.DataTable.(p.Results.DataFieldname){ii},'Parent',gca,'CDataMapping','scaled'); + axis(gca,'ij'); + set(gca,'Layer','top'); + xlim([p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(1) p.Results.DataTable(p.Results.tableSelection,:).xRayEkin_eV{ii}(end)]); + ylim([p.Results.DataTable(p.Results.tableSelection,:).scientaEkinMin_eV(ii) p.Results.DataTable(p.Results.tableSelection,:).scientaEkinMax_eV(ii)]); + xlabel('Photon energy [eV]') + ylabel('Auger energy [eV]') + % if this does not work, plot without scaling (just with row and column numbers) + catch + image(p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii},'Parent',gca,'CDataMapping','scaled'); + axis(gca,'ij'); + set(gca,'Layer','top'); + ylim([1 size(p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii},1)]); + xlim([1 size(p.Results.DataTable(p.Results.tableSelection,:).(p.Results.tableVariable){ii},2)]); + xlabel('# Photon energy, column number') + ylabel('# Auger energy, column number') + end + + box(gca,'on'); + title(p.Results.DataTable(p.Results.tableSelection,:).name{ii},'Interpreter','none') + end + end \ No newline at end of file diff --git a/beamtime/nappSaveDataTable.m b/beamtime/nappSaveDataTable.m new file mode 100644 index 0000000..b5b95e7 --- /dev/null +++ b/beamtime/nappSaveDataTable.m @@ -0,0 +1,48 @@ +function saveFilename = nappSaveDataTable(DataTable, options) + arguments + DataTable table + options.suffix char = "" + options.varName char = "" + end + + currentScriptName = ''; + + % 1. Determine script name (Your existing logic is solid here) + stackInfo = dbstack; + if length(stackInfo) > 1 + [~, currentScriptName, ~] = fileparts(stackInfo(2).file); + if contains(currentScriptName, 'LiveEditorEvaluationHelper') + currentScriptName = ''; + end + end + + if isempty(currentScriptName) + try + [~, currentScriptName, ~] = fileparts(matlab.desktop.editor.getActiveFilename); + catch + currentScriptName = 'ManualRun'; + end + end + + % 2. Generate the final filename + saveFilename = sprintf('%s%s.mat', currentScriptName, options.suffix); + + % 3. Determine the variable name + % Priority: 1. Manual override, 2. inputname, 3. Default 'DataTable' + if options.varName ~= "" + finalVarName = options.varName; + else + finalVarName = inputname(1); + if isempty(finalVarName) + finalVarName = 'DataTable'; + end + end + + % 4. SAVE + % We create a struct where the field name is our dynamic string. + % Then we save the struct with the '-struct' flag. + saveData.(finalVarName) = DataTable; + save(saveFilename, '-struct', 'saveData'); + + fprintf('✅ Successfully saved %s to: %s\n', finalVarName, saveFilename); +end \ No newline at end of file diff --git a/beamtime/outerjoinRowToTable.m b/beamtime/outerjoinRowToTable.m new file mode 100644 index 0000000..74d9e8a --- /dev/null +++ b/beamtime/outerjoinRowToTable.m @@ -0,0 +1,85 @@ +function Table = outerjoinRowToTable(Table,TableRowToBeAdded) +%outerjoinRowToTable Adds one row to a table. Variables that are missing in +%either the table or the row are added with empty values {' '} for cells +%and NaN for doubles. Works with tables that contain both cells and +%doubles. +% +% * Strings in the table are not yet supported. +% * The row that is added needs to be formatted as table +% * Update 2024: Adds several rows, that is tables +% * Update 2026: Handles peakfitting columns with different size. +% +% T = outerjoinRowToTable(TableToAddTo,RowToBeAdded) +% +% tmpTableToBeAdded = table({tmpCombined(:,1)},{tmpCombined(:,2)},{'660eV, m50, 092022'},{'#288,#289,#290.ibw'}, ... +% 'VariableNames',["bindingEnergy_eV","spectrum_countsNorm","sample","name"]); +% SpectrumList = outerjoinRowToTable(SpectrumList,tmpTableToBeAdded); +% +% Thorsten, December 2023 + +% Original Table has more variables +if ~all(ismember(Table.Properties.VariableNames,TableRowToBeAdded.Properties.VariableNames)) + tmpMissingVariable = ~ismember(Table.Properties.VariableNames,TableRowToBeAdded.Properties.VariableNames); + tmpVariablesToBeAdded = Table.Properties.VariableNames(tmpMissingVariable); + for ii = 1:size(tmpVariablesToBeAdded,2) + if matches(class(Table.(tmpVariablesToBeAdded{ii})),'double') || matches(class(Table.(tmpVariablesToBeAdded{ii})),'int32') + tmpContentToBeAdded = NaN(size(TableRowToBeAdded,1),size(Table.(tmpVariablesToBeAdded{ii}),2)); + TableRowToBeAdded = addvars(TableRowToBeAdded,tmpContentToBeAdded,'NewVariableNames',tmpVariablesToBeAdded{ii}); + elseif matches(class(Table.(tmpVariablesToBeAdded{ii})),'cell') + tmpContentToBeAdded = repmat({' '},size(TableRowToBeAdded,1),size(Table.(tmpVariablesToBeAdded{ii}),2)); + TableRowToBeAdded = addvars(TableRowToBeAdded,tmpContentToBeAdded,'NewVariableNames',tmpVariablesToBeAdded{ii}); + elseif matches(class(Table.(tmpVariablesToBeAdded{ii})),'string') + tmpContentToBeAdded = repmat({" "},size(TableRowToBeAdded,1),size(Table.(tmpVariablesToBeAdded{ii}),2)); + TableRowToBeAdded = addvars(TableRowToBeAdded,tmpContentToBeAdded,'NewVariableNames',tmpVariablesToBeAdded{ii}); + end + end +end +% Table to be added has more variables +if ~all(ismember(TableRowToBeAdded.Properties.VariableNames,Table.Properties.VariableNames)) + tmpMissingVariable = ~ismember(TableRowToBeAdded.Properties.VariableNames,Table.Properties.VariableNames); + tmpVariablesToBeAdded = TableRowToBeAdded.Properties.VariableNames(tmpMissingVariable); + for ii = 1:size(tmpVariablesToBeAdded,2) + if matches(class(TableRowToBeAdded.(tmpVariablesToBeAdded{ii})),'double') || matches(class(TableRowToBeAdded.(tmpVariablesToBeAdded{ii})),'int32') + tmpContentToBeAdded = NaN(size(Table,1),size(TableRowToBeAdded.(tmpVariablesToBeAdded{ii}),2)); + Table = addvars(Table,tmpContentToBeAdded,'NewVariableNames',tmpVariablesToBeAdded{ii}); + elseif matches(class(TableRowToBeAdded.(tmpVariablesToBeAdded{ii})),'cell') + tmpContentToBeAdded = repmat({' '},size(Table,1),size(TableRowToBeAdded.(tmpVariablesToBeAdded{ii}),2)); + Table = addvars(Table,tmpContentToBeAdded,'NewVariableNames',tmpVariablesToBeAdded{ii}); + elseif matches(class(TableRowToBeAdded.(tmpVariablesToBeAdded{ii})),'string') + tmpContentToBeAdded = repmat({" "},size(Table,1),size(TableRowToBeAdded.(tmpVariablesToBeAdded{ii}),2)); + Table = addvars(Table,tmpContentToBeAdded,'NewVariableNames',tmpVariablesToBeAdded{ii}); + end + end +end +% Reconcile column widths for peak-fit columns. +% These numeric columns can have variable width (one entry per peak), +% so a width mismatch is expected when spectra have different numbers +% of peaks. Pad the narrower side with NaN. +variableWidthColumns = {'peakArea', 'peakPosition', 'peakFWHM', ... + 'peakBackground', 'peakEta', 'peakShirleyK'}; + +for ii = 1:numel(variableWidthColumns) + varName = variableWidthColumns{ii}; + + if ~ismember(varName, Table.Properties.VariableNames) || ... + ~ismember(varName, TableRowToBeAdded.Properties.VariableNames) + continue + end + + widthT = size(Table.(varName), 2); + widthR = size(TableRowToBeAdded.(varName), 2); + + if widthT < widthR + Table.(varName) = [Table.(varName), ... + NaN(height(Table), widthR - widthT)]; + elseif widthR < widthT + TableRowToBeAdded.(varName) = [TableRowToBeAdded.(varName), ... + NaN(height(TableRowToBeAdded), widthT - widthR)]; + end +end + +% Ensure matching column order before concatenation +TableRowToBeAdded = TableRowToBeAdded(:, Table.Properties.VariableNames); +% Add row to table +Table = [Table;TableRowToBeAdded]; +end \ No newline at end of file diff --git a/beamtime/plotExportedData.m b/beamtime/plotExportedData.m new file mode 100644 index 0000000..7826b3d --- /dev/null +++ b/beamtime/plotExportedData.m @@ -0,0 +1,151 @@ +function plotExportedData(dataDirectory) +%plotExportedData Plot all exported ASCII data files in one figure +% +% This function reads all .txt files from the exported data (created by +% exportFigureData) and plots them in a single figure for visual comparison. +% File names are parsed to extract axis labels and legend entries. +% +% SYNTAX: +% plotExportedData() % Uses current working directory +% plotExportedData(dataDirectory) % Uses specified directory path +% +% INPUTS: +% dataDirectory : char or string (optional) +% Path to directory containing exported .txt files. +% If omitted, uses current working directory (pwd). +% +% OUTPUT: +% A new figure with all data plotted as lines. Axis labels and legend +% entries are reconstructed from the file names. +% +% NOTES: +% - Reads all .txt files in the specified directory +% - File name format expected: _[_]_vs__.txt +% - Plots are created with underscores converted back to spaces for display +% - X and Y axis labels are set based on parsed filename +% - Legend shows line labels from filenames +% - All data is plotted in a single axes for direct visual comparison +% +% EXAMPLE: +% % After running exportFigureData(): +% plotExportedData() +% % or +% plotExportedData('../exported_data/') + +% Handle input: use current directory if none provided +if nargin == 0 || isempty(dataDirectory) + dataDirectory = pwd; +end + +% Get all .txt files in the directory +fileStruct = dir(fullfile(dataDirectory, '*.txt')); +if isempty(fileStruct) + warning('plotExportedData:NoFiles', 'No .txt files found in %s', dataDirectory); + return; +end + +% Create a new figure +fig = figure('Name', 'Exported Data Comparison', 'NumberTitle', 'off'); +ax = axes(fig); +hold(ax, 'on'); +grid(ax, 'on'); + +% Storage for axis labels (to ensure consistency) +xLabelStr = ''; +yLabelStr = ''; +legendEntries = {}; + +% Loop over all .txt files +for ii = 1:length(fileStruct) + fileName = fileStruct(ii).name; + filePath = fullfile(dataDirectory, fileName); + + % Read the data + try + data = readmatrix(filePath); + catch ME + warning('plotExportedData:ReadFailed', 'Could not read file: %s\n%s', fileName, ME.message); + continue; + end + + % Check if data has at least 2 columns + if size(data, 2) < 2 || size(data, 1) < 1 + warning('plotExportedData:InvalidData', 'File %s does not have valid X,Y columns', fileName); + continue; + end + + xData = data(:, 1); + yData = data(:, 2); + + % Parse filename to extract labels + % Expected format: _[_]_vs__.txt + + % Remove .txt extension + nameNoExt = fileName(1:end-4); + + % Split by '_vs_' to separate y-axis label from the rest + parts = strsplit(nameNoExt, '_vs_'); + + if length(parts) >= 2 + % Extract line label (after _vs__) + rightPart = parts{2}; % _ + + % Find the last underscore to separate XLabel from LineLabel + underscores = find(rightPart == '_'); + if ~isempty(underscores) + lastUnderscore = underscores(end); + xLabelExtracted = rightPart(1:lastUnderscore-1); + lineLabel = rightPart(lastUnderscore+1:end); + else + xLabelExtracted = rightPart; + lineLabel = 'data'; + end + + % Extract y-axis label (before _vs_) + yLabelExtracted = parts{1}; + + % Remove figure title (first part before first underscore) + % This is tricky, so we'll keep what we have and note it's approximate + + % Convert underscores back to spaces for display + xLabelDisplay = strrep(xLabelExtracted, '_', ' '); + yLabelDisplay = strrep(yLabelExtracted, '_', ' '); + lineLabel = strrep(lineLabel, '_', ' '); + + % Store axis labels (use the first valid ones found) + if isempty(xLabelStr) + xLabelStr = xLabelDisplay; + end + if isempty(yLabelStr) + yLabelStr = yLabelDisplay; + end + + else + % Fallback if filename parsing fails + lineLabel = nameNoExt; + end + + % Plot the data + plot(ax, xData, yData, 'DisplayName', lineLabel, 'LineWidth', 1.5, 'Marker', 'o', 'MarkerSize', 4); + + % Store legend entry + legendEntries{end+1} = lineLabel; +end + +% Set axis labels +xlabel(ax, xLabelStr, 'Interpreter', 'none'); +ylabel(ax, yLabelStr, 'Interpreter', 'none'); +title(ax, 'Exported Data Comparison', 'Interpreter', 'none'); + +% Add legend +if ~isempty(legendEntries) + legend(ax, legendEntries, 'Interpreter', 'none', 'Location', 'best'); +end + +% Optimize figure appearance +grid(ax, 'on'); +hold(ax, 'off'); + +fprintf('Plotted %d data files from: %s\n', length(fileStruct), dataDirectory); + +end diff --git a/beamtime/plotRgaData.m b/beamtime/plotRgaData.m index f86f2f8..e9fa0c3 100644 --- a/beamtime/plotRgaData.m +++ b/beamtime/plotRgaData.m @@ -1,30 +1,46 @@ -function ax = plotRgaData(tbl, tStart, tEnd) +function ax = plotRgaData(tbl, opts) %PLOTRGADATA Plot RGA partial pressures vs time (companion to plotCryoData). % -% ax = plotRgaData(tbl, tStart, tEnd) -% Plots every channel column of an rgaImportData table against its shared -% datetime axis. tStart/tEnd (datetime) optionally subset the range, just -% like plotCryoData. Returns a struct with the axes handle. +% ax = plotRgaData(tbl) +% ax = plotRgaData(tbl, TimeRange=[t1 t2], Masses=["m28" "m18"]) % -% By default the y-axis is linear (RGA scans contain small near-noise -% negatives that a log axis cannot show). To switch to the classic -% semilog view afterwards: +% Plots every channel column of an rgaImportData table against its shared +% datetime axis. If Masses is given, a second figure shows a stackedplot +% of those ions over the same time window. Returns a struct with the +% axes/chart handles. +% +% Name-value arguments (type "plotRgaData(tbl, " and tab for suggestions): +% TimeRange (1x2 datetime) subset window [start end]. Default: full range. +% Masses (string) mass labels for the stackedplot, with or without +% the leading "m": "m28", "28", ["m28" "m18"]. Default: none, +% i.e. the stackedplot is skipped unless masses are requested. +% +% By default the y-axis of the first plot is linear (RGA scans contain +% small near-noise negatives that a log axis cannot show). To switch to +% the classic semilog view afterwards: % set(ax.partial, 'YScale', 'log'); % (values <= 0 are simply not drawn on a log axis). % % EXAMPLE % tbl = rgaImportData('rga1_Jul_01_2026_08-23-06_AM.txt'); -% ax = plotRgaData(tbl); -% ax = plotRgaData(tbl, t1, t2); % zoom to a window +% ax = plotRgaData(tbl); % overlay only +% ax = plotRgaData(tbl, TimeRange=[t1 t2]); % zoom to a window +% ax = plotRgaData(tbl, Masses=["m28" "m18" "m44"]); % overlay + ion stack % -% See also RGAIMPORTDATA, PLOTCRYODATA +% See also RGAIMPORTDATA, PLOTCRYODATA, STACKEDPLOT + + arguments + tbl table + opts.TimeRange (1,2) datetime = [NaT NaT] + opts.Masses (1,:) string = string.empty + end %--- Extract datetime column --- dt = tbl.datetime; - %--- If start/stop provided, subset the table --- - if nargin >= 3 && ~isempty(tStart) && ~isempty(tEnd) - mask = dt >= tStart & dt <= tEnd; + %--- If a real time range was given, subset the table (shared by both plots) --- + if ~any(isnat(opts.TimeRange)) + mask = dt >= opts.TimeRange(1) & dt <= opts.TimeRange(2); tbl = tbl(mask, :); dt = dt(mask); end @@ -37,7 +53,7 @@ function ax = plotRgaData(tbl, tStart, tEnd) units = tbl.Properties.UserData.units; end - %--- Plot --- + %--- Plot 1: all partial pressures overlaid --- figure; tl = tiledlayout(1, 1); tl.TileSpacing = 'compact'; @@ -57,6 +73,39 @@ function ax = plotRgaData(tbl, tStart, tEnd) % Match plotCryoData's datetime tick style ax.partial.XAxis.TickLabelFormat = 'dd-MMM HH:mm'; + + %--- Plot 2: selected ions as a stackedplot (only if masses requested) --- + ax.stacked = []; + if ~isempty(opts.Masses) + sel = selectIons(chVars, opts.Masses); + if isempty(sel) + warning('plotRgaData:noIonMatch', ... + 'No channels matched the requested mass label(s): %s', ... + strjoin(cellstr(opts.Masses), ', ')); + else + figure; + ax.stacked = stackedplot(tbl, sel, 'XVariable', 'datetime'); + ax.stacked.Title = sprintf('Selected ions [%s]', units); + ax.stacked.XLabel = 'Date & Time'; + ax.stacked.DisplayLabels = cellfun(@prettyName, sel, 'UniformOutput', false); + ax.stacked.GridVisible = 'on'; + end + end +end + +% ------------------------------------------------------------------------- +function sel = selectIons(chVars, masses) +% Return the channel names whose mass label matches any entry in masses. +% masses may be given with or without the leading "m" ("m28", "28"). + keep = false(1, numel(chVars)); + for j = 1:numel(masses) + lbl = masses(j); + if ~startsWith(lbl, "m") + lbl = "m" + lbl; + end + keep = keep | endsWith(string(chVars), "_" + lbl); + end + sel = chVars(keep); end % ------------------------------------------------------------------------- diff --git a/beamtime/preset_colormap.m b/beamtime/preset_colormap.m new file mode 100644 index 0000000..b2197bd --- /dev/null +++ b/beamtime/preset_colormap.m @@ -0,0 +1,75 @@ +function map = preset_colormap(N,varargin) +% Wrapper for any COLORMAP function, to provide preset parameter values. +% +% (c) 2020-2025 Stephen Cobeldick +% +%%% Syntax %%% +% +% preset_colormap(@fun,p1,p2,...,pN) % store function with any parameters +% map = preset_colormap(N) % generate colormap +% map = preset_colormap % generate colormap +% +%% Examples %% +% +% >> preset_colormap(@cubehelix,0.25,-0.67,1.5,1) +% >> colormap(preset_colormap) +% +% >> preset_colormap(5) +% ans = +% 0 0 0 +% 0.1055 0.2788 0.4895 +% 0.1660 0.6705 0.4961 +% 0.6463 0.8479 0.5076 +% 1.0000 1.0000 1.0000 +% +% >> preset_colormap(@brewermap,'PuOr') +% >> load topo +% >> load coast +% >> figure +% >> worldmap(topo, topolegend) +% >> contourfm(topo, topolegend); +% >> contourcmap('preset_colormap', 'Colorbar','on', 'Location','horizontal','TitleString','Contour Intervals in Meters'); +% >> plotm(lat, long, 'k') +% +%% Dependencies %% +% +% * MATLAB R2009a or later. +% +% See also BREWERMAP CUBEHELIX CMOCEAN LBMAP COLORMAP CONTOURCMAP +persistent fnh arg +% +if nargin==0 % Default N same as MATLAB colormaps. + N = cmDefaultN(); +elseif nargin==1 && isnumeric(N) + assert(isscalar(N) && isreal(N) && fix(N)==N,... + 'SC:preset_colormap:N:NotRealScalarNumeric',... + 'First input must be a real scalar numeric of the colormap size.') + N = double(N); +else % Store function handle and parameter values. + assert(isa(N,'function_handle'),... + 'SC:preset_colormap:N:NotFunctionHandle',... + 'First input must be a function handle to a colormap function.') + fnh = N; + arg = varargin; + return +end +% +map = fnh(N,arg{:}); +% +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%preset_colormap +function N = cmDefaultN() +% Get the colormap size from the current figure or default colormap. +try + F = get(groot,'CurrentFigure'); +catch %#ok pre HG2 + N = size(get(gcf,'colormap'),1); + return +end +if isempty(F) + N = size(get(groot,'DefaultFigureColormap'),1); +else + N = size(F.Colormap,1); +end +end +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%cmDefaultN \ No newline at end of file diff --git a/beamtime/readIBWbinheader.m b/beamtime/readIBWbinheader.m new file mode 100644 index 0000000..2654772 --- /dev/null +++ b/beamtime/readIBWbinheader.m @@ -0,0 +1,52 @@ +function H = readIBWbinheader(FFN); +% readIBWbinheader - read BinHeader segment of Igor IBW file +% H = readIBWbinheader('foo') reads BinHeader of file Foo.ibw +% IBW version 2,5 only +MAXDIMS = 4; + +fid = fopen(FFN,'r'); +H.version = fread(fid,1,'int16'); +switch H.version, + case 2, % typedef struct BinHeader2 { + H.wfmSize = fread(fid,1,'uint32'); % long wfmSize; // The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding. + H.noteSize = fread(fid,1,'uint32'); % long noteSize; // The size of the note text. + H.pictSize = fread(fid,1,'uint32'); % long pictSize; // Reserved. Write zero. Ignore on read. + H.checksum = fread(fid,1,'int16'); % short checksum; // Checksum over this header and the wave header. + case 5, + H.checksum = fread(fid,1,'short'); + H.wfmSize = fread(fid,1,'long'); % The size of the WaveHeader5 data structure plus the wave data. + H.formulaSize = fread(fid,1,'long'); % The size of the dependency formula, if any. + H.noteSize = fread(fid,1,'long'); % The size of the note text. + H.dataEUnitsSize = fread(fid,1,'long'); % The size of optional extended data units. + H.dimEUnitsSize = fread(fid,MAXDIMS,'long'); % The size of optional extended dimension units. + H.dimLabelsSize = fread(fid,MAXDIMS,'long'); % The size of optional dimension labels. + H.sIndicesSize = fread(fid,1,'long'); ; % The size of string indicies if this is a text wave. + H.optionsSize1 = fread(fid,1,'long'); ; % Reserved. Write zero. Ignore on read. + H.optionsSize2 = fread(fid,1,'long'); ; % Reserved. Write zero. Ignore on read. + otherwise, +end % switch H.Version +fclose(fid); + +if ~ismember(H.version,[2 5]), + error(['Cannot read version ' num2str(H.version) ' IBW files - only versions 2,5 are okay.']); +end +%==FROM IgorBin.h ===================== +% #define MAXDIMS 4 +% +% typedef struct BinHeader5 { +% short version; // Version number for backwards compatibility. +% short checksum; // Checksum over this header and the wave header. +% long wfmSize; // The size of the WaveHeader5 data structure plus the wave data. +% long formulaSize; // The size of the dependency formula, if any. +% long noteSize; // The size of the note text. +% long dataEUnitsSize; // The size of optional extended data units. +% long dimEUnitsSize[MAXDIMS]; // The size of optional extended dimension units. +% long dimLabelsSize[MAXDIMS]; // The size of optional dimension labels. +% long sIndicesSize; // The size of string indicies if this is a text wave. +% long optionsSize1; // Reserved. Write zero. Ignore on read. +% long optionsSize2; // Reserved. Write zero. Ignore on read. +% } BinHeader5; + + + + diff --git a/beamtime/readIBWheaders.m b/beamtime/readIBWheaders.m new file mode 100644 index 0000000..75f10df --- /dev/null +++ b/beamtime/readIBWheaders.m @@ -0,0 +1,250 @@ +function [BH, H, FFN] = readIBWheaders(FFN); +% readIBWheaders - read BinHeader & WaveHeader segments of Igor IBW file +% [BH, WH, FFN] = readIBWheaders('foo') reads BinHeader & WaveHeader of +% file Foo.ibw - Versions 2 and 5 only. FFN is full filename. + +% % 1 byte char +% % 2 bytes short +% % 4 bytes int, long, float, Handle, any kind of pointer +% % 8 bytes double +MAXDIMS = 4; +MAX_WAVE_NAME2 = 18; % Maximum length of wave name in version 1 and 2 files. Does not include the trailing null. +MAX_WAVE_NAME5 = 31; % Maximum length of wave name in version 5 files. Does not include the trailing null. +MAX_UNIT_CHARS = 3; + + +BH = readIBWbinheader(FFN); +fid = fopen(FFN,'r'); +switch BH.version + case 2, + % The version 2 file has the following general layout. + % BinHeader2 structure: 16 bytes + % WaveHeader2 structure excluding wData field; 110 bytes + % Wave data: Variable size + fseek(fid,16,'bof'); % start of wave header of IBW vs 2; % struct WaveHeader2 { + H.type = fread(fid,1, 'uint16'); % short type; % See types (e.g. NT_FP64) above. Zero for text waves. + H.WaveHeader2 = fread(fid,1, 'uint32'); % struct WaveHeader2 **next; // Used in memory only. Write zero. Ignore on read. + H.bname = local_char(fread(fid, MAX_WAVE_NAME2+2, 'char')); % char bname[MAX_WAVE_NAME2+2]; // Name of wave plus trailing null. + H.whVersion = fread(fid,1, 'uint16'); % short whVersion; // Write 0. Ignore on read. + H.srcFldr = fread(fid,1, 'uint16'); % short srcFldr; // Used in memory only. Write zero. Ignore on read. + H.fileName = fread(fid,1, 'uint32'); % Handle fileName; // Used in memory only. Write zero. Ignore on read. + % + H.dataUnits = local_char(fread(fid, MAX_UNIT_CHARS+1, 'char')); % char dataUnits[MAX_UNIT_CHARS+1]; // Natural data units go here - null if none. + H.xUnits = local_char(fread(fid, MAX_UNIT_CHARS+1, 'char')); % char xUnits[MAX_UNIT_CHARS+1]; // Natural x-axis units go here - null if none. + % + H.npnts = fread(fid,1, 'uint32'); % long npnts; // Number of data points in wave. + % + H.aModified = fread(fid,1, 'uint16'); % short aModified; // Used in memory only. Write zero. Ignore on read. + H.hsA = fread(fid,1, 'double'); % double hsA,hsB; // X value for point p = hsA*p + hsB + H.hsB = fread(fid,1, 'double'); % double hsA,hsB; // X value for point p = hsA*p + hsB + % + H.wModified = fread(fid,1, 'uint16'); % short wModified; // Used in memory only. Write zero. Ignore on read. + H.swModified = fread(fid,1, 'uint16'); % short swModified; // Used in memory only. Write zero. Ignore on read. + H.fsValid = fread(fid,1, 'uint16'); % short fsValid; // True if full scale values have meaning. + H.topFullScale = fread(fid,1, 'double'); % double topFullScale,botFullScale; // The min full scale value for wave. + H.botFullScale = fread(fid,1, 'double'); % double topFullScale,botFullScale; // The min full scale value for wave. + % + H.useBits = fread(fid,1, 'char'); % char useBits; // Used in memory only. Write zero. Ignore on read. + H.kindBits = fread(fid,1, 'char'); % char kindBits; // Reserved. Write zero. Ignore on read. + H.formula = fread(fid,1, 'uint32'); % void **formula; // Used in memory only. Write zero. Ignore on read. + H.depID = fread(fid,1, 'uint32'); % long depID; // Used in memory only. Write zero. Ignore on read. + H.creationDate = fread(fid,1, '*uint32'); % unsigned long creationDate; // DateTime of creation. Not used in version 1 files. + H.platform = fread(fid,1, 'uint8'); % unsigned char platform; // 0=unspecified, 1=Macintosh, 2=Windows; Added for Igor Pro 5.5. +% dummy = fread(fid,1, 'uint8'); % align + H.wUnused = fread(fid,1, 'uint8'); % char wUnused[1]; // Reserved. Write zero. Ignore on read. + % + H.modDate = fread(fid,1, '*uint32'); % unsigned long modDate; // DateTime of last modification. + H.waveNoteH = fread(fid,1, 'uint32'); % Handle waveNoteH; // Used in memory only. Write zero. Ignore on read. + % + H.wData = fread(fid,4, 'single').'; % float wData[4]; // The start of the array of waveform data. + case 5, + % The version 5 file has the following general layout. + % BinHeader5 structure: 64 bytes + % WaveHeader5 structure excluding wData field: 320 bytes + % Wave data: Variable size + fseek(fid,64,'bof'); % start of wave header of IBW vs 5 + % + H.next = fread(fid,1, 'int32'); % link to next wave in linked list. + % + H.creationDate = fread(fid,1, 'uint32'); % DateTime of creation. + H.modDate = fread(fid,1, 'uint32'); % DateTime of creation. + % + H.npnts = fread(fid,1, 'int32'); % Total number of points (multiply dimensions up to first zero). + H.type = fread(fid,1, 'uint16'); % See types (e.g. NT_FP64) above. Zero for text waves. + H.dLock = fread(fid, 1, 'int16'); % Reserved. Write zero. Ignore on read. + % + H.whpad1 = fread(fid, 6, 'char').'; % Reserved. Write zero. Ignore on read. + H.whVersion = fread(fid, 1, 'int16'); % Write 1. Ignore on read. + H.bname = local_char(fread(fid, MAX_WAVE_NAME5+1, 'char')); % Name of wave plus trailing null. + H.whpad2 = fread(fid, 1, 'int32'); % long Reserved. Write zero. Ignore on read. + H.dFolder = fread(fid, 1, 'int32'); % ptr Used in memory only. Write zero. Ignore on read. + + % % Dimensioning info. [0] == rows, [1] == cols etc + H.nDim = fread(fid, MAXDIMS, 'int32').'; % long nDim[MAXDIMS] Number of of items in a dimension -- 0 means no data. + H.sfA = fread(fid, MAXDIMS, 'double').'; % double sfA[MAXDIMS]; Index value for element e of dimension d = sfA[d]*e + sfB[d]. + H.sfB = fread(fid, MAXDIMS, 'double').'; % double sfB[MAXDIMS];% double sfB[MAXDIMS]; + % % + % % % SI units + H.dataUnits = local_char(fread(fid, MAX_UNIT_CHARS+1, 'char')); % char dataUnits[MAX_UNIT_CHARS+1]; % Natural data units go here - null if none. + H.dimUnits = fread(fid, MAXDIMS*(MAX_UNIT_CHARS+1), '*char').'; % % char dimUnits[MAXDIMS][MAX_UNIT_CHARS+1]; % Natural dimension units go here - null if none. + % % + H.fsValid = fread(fid, 1, 'uint16'); % % short fsValid; % TRUE if full scale values have meaning. + H.whpad3 = fread(fid, 1, 'int16'); % % short whpad3; % Reserved. Write zero. Ignore on read. + H.topFullScale = fread(fid, 1, 'double'); + H.botFullScale = fread(fid, 1, 'double'); % % double topFullScale,botFullScale; % The max and max full scale value for wave. + % % + H.dataEUnits = fread(fid, 1, 'int32'); % % Handle dataEUnits; % Used in memory only. Write zero. Ignore on read. + H.dimEUnits = fread(fid, MAXDIMS, 'int32').'; % % Handle dimEUnits[MAXDIMS]; % Used in memory only. Write zero. Ignore on read. + H.dimLabels = fread(fid, MAXDIMS, 'int32').'; % % Handle dimLabels[MAXDIMS]; % Used in memory only. Write zero. Ignore on read. + % % + H.waveNoteH = fread(fid, 1, 'int32'); % % Handle waveNoteH; % Used in memory only. Write zero. Ignore on read. + % % + H.platform = fread(fid, 1, 'uint8'); % % unsigned char platform; % 0=unspecified, 1=Macintosh, 2=Windows; Added for Igor Pro 5. + H.skip_junk________ = fread(fid,80,'char').'; + % H. = fread(fid, 1, ''); + % % unsigned char spare[3]; + % H. = fread(fid, 1, ''); + % % + % H. = fread(fid, 1, ''); + % % long whUnused[13]; % Reserved. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % + % H. = fread(fid, 1, ''); + % % long vRefNum, dirID; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % + % H. = fread(fid, 1, ''); + % % % The following stuff is considered private to Igor. + % H. = fread(fid, 1, ''); + % % + % H. = fread(fid, 1, ''); + % % short aModified; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % short wModified; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % short swModified; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % + % H. = fread(fid, 1, ''); + % % char useBits; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % char kindBits; % Reserved. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % void **formula; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % long depID; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % + % H. = fread(fid, 1, ''); + % % short whpad4; % Reserved. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % short srcFldr; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % Handle fileName; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % + % H. = fread(fid, 1, ''); + % % long **sIndices; % Used in memory only. Write zero. Ignore on read. + % H. = fread(fid, 1, ''); + % % + H.wData = fread(fid, 1, 'single'); + % % float wData[1]; % The start of the array of data. Must be 64 bit aligned. + % H. = fread(fid, 1, ''); + % }; +end % switch BH.version +H.EOH_pos = ftell(fid); + + + + +fclose(fid); + +%==FROM IgorBin.h ===================== +% #define MAXDIMS 4 + +% % #define MAX_WAVE_NAME5 31 // Maximum length of wave name in version 5 files. Does not include the trailing null. +% % #define MAX_UNIT_CHARS 3 + +% % #define NT_CMPLX 1 % Complex numbers. +% % #define NT_FP32 2 % 32 bit fp numbers. +% % #define NT_FP64 4 % 64 bit fp numbers. +% % #define NT_I8 8 % 8 bit signed integer. Requires Igor Pro 2.0 or later. +% % #define NT_I16 0x10 % 16 bit integer numbers. Requires Igor Pro 2.0 or later. +% % #define NT_I32 0x20 % 32 bit integer numbers. Requires Igor Pro 2.0 or later. +% % #define NT_UNSIGNED 0x40 % Makes above signed integers unsigned. Requires Igor Pro 3.0 or later. +% +% struct WaveHeader5 { +% struct WaveHeader5 **next; % link to next wave in linked list. +% +% unsigned long creationDate; % DateTime of creation. +% unsigned long modDate; % DateTime of last modification. +% +% long npnts; % Total number of points (multiply dimensions up to first zero). +% short type; % See types (e.g. NT_FP64) above. Zero for text waves. +% short dLock; % Reserved. Write zero. Ignore on read. +% +% char whpad1[6]; % Reserved. Write zero. Ignore on read. +% short whVersion; % Write 1. Ignore on read. +% char bname[MAX_WAVE_NAME5+1]; % Name of wave plus trailing null. +% long whpad2; % Reserved. Write zero. Ignore on read. +% struct DataFolder **dFolder; % Used in memory only. Write zero. Ignore on read. +% +% % Dimensioning info. [0] == rows, [1] == cols etc +% long nDim[MAXDIMS]; % Number of of items in a dimension -- 0 means no data. +% double sfA[MAXDIMS]; % Index value for element e of dimension d = sfA[d]*e + sfB[d]. +% double sfB[MAXDIMS]; +% +% % SI units +% char dataUnits[MAX_UNIT_CHARS+1]; % Natural data units go here - null if none. +% char dimUnits[MAXDIMS][MAX_UNIT_CHARS+1]; % Natural dimension units go here - null if none. +% +% short fsValid; % TRUE if full scale values have meaning. +% short whpad3; % Reserved. Write zero. Ignore on read. +% double topFullScale,botFullScale; % The max and max full scale value for wave. +% +% Handle dataEUnits; % Used in memory only. Write zero. Ignore on read. +% Handle dimEUnits[MAXDIMS]; % Used in memory only. Write zero. Ignore on read. +% Handle dimLabels[MAXDIMS]; % Used in memory only. Write zero. Ignore on read. +% +% Handle waveNoteH; % Used in memory only. Write zero. Ignore on read. +% +% unsigned char platform; % 0=unspecified, 1=Macintosh, 2=Windows; Added for Igor Pro 5.5. +% unsigned char spare[3]; +% +% long whUnused[13]; % Reserved. Write zero. Ignore on read. +% +% long vRefNum, dirID; % Used in memory only. Write zero. Ignore on read. +% +% % The following stuff is considered private to Igor. +% +% short aModified; % Used in memory only. Write zero. Ignore on read. +% short wModified; % Used in memory only. Write zero. Ignore on read. +% short swModified; % Used in memory only. Write zero. Ignore on read. +% +% char useBits; % Used in memory only. Write zero. Ignore on read. +% char kindBits; % Reserved. Write zero. Ignore on read. +% void **formula; % Used in memory only. Write zero. Ignore on read. +% long depID; % Used in memory only. Write zero. Ignore on read. +% +% short whpad4; % Reserved. Write zero. Ignore on read. +% short srcFldr; % Used in memory only. Write zero. Ignore on read. +% Handle fileName; % Used in memory only. Write zero. Ignore on read. +% +% long **sIndices; % Used in memory only. Write zero. Ignore on read. +% +% float wData[1]; % The start of the array of data. Must be 64 bit aligned. +% }; + +function s=local_char(S); +% null-terminated string -> char string +S = S(:).'; +S = S(S~=0); +s=char(S); + + + + + + + + diff --git a/beamtime/read_hdf5_as_table.m b/beamtime/read_hdf5_as_table.m new file mode 100644 index 0000000..e2c622f --- /dev/null +++ b/beamtime/read_hdf5_as_table.m @@ -0,0 +1,130 @@ +function output_table = read_hdf5_as_table(filename,exclude_var_set) +% receives a h5 file written with function table2h5. +% reduced_var_set (cell of character_vectors), not usable yet. + + +% Last update: 14.08.2023 +% By: Juan F. Florez Ospina +% Email: juan.florez-ospina@psi.ch + +if ~iscellstr(exclude_var_set) + disp("exclude_var_set needs to be a cell of character vectors, e.g, {'var1','var2'} to be excluded.") + disp("To proceed, exclude_var_set was set to {}. So vars will be excluded.") + exclude_var_set = {}; +end + +INFO = h5info(filename); +num_rows = length(INFO.Groups); + + +num_attr1 = length(INFO.Groups(1).Datasets); +num_attr2 = length(INFO.Groups(1).Attributes); +num_cols = num_attr1 + num_attr2; +sz = [num_rows,num_cols]; + +% Extract group level attributes, they align for all +% groups (here denoting measurements/or rows) of the table in filename .h5 +group_columns = {INFO.Groups(1).Attributes.Name}; + +if num_attr1 == 1 && isempty(INFO.Groups(1).Datasets(num_attr1).Attributes) + dataset_columns = {}; + num_attr1 = 0; + num_cols = num_cols - 1; +else + for j = 1 : num_attr1 + dataset_columns{j} = INFO.Groups(1).Datasets(j).Attributes.Value; + end +end +%TODO: Implement feature to exclude varibales/columns as well as rows at +%reading time. This requires propagating modifications +% % % Select group and dataset columns to be loaded from hdf5 file, which are +% % % those not in the exclude_var_set. +% % if ~isempty(exclude_var_set) +% % for j = 1 : length(exclude_var_set) +% % % keep all vars except exclude_var_set{j} +% % var_filter = ~strcmp(exclude_var_set{j},group_columns); +% % group_columns = group_columns(var_filter); +% % if sum(var_filter) > 0 +% % exclude_var_set(j) = []; +% % end +% % end +% % +% % for j = 1 : length(exclude_var_set) +% % % keep all vars except exclude_var_set{j} +% % var_filter = ~strcmp(exclude_var_set{j},dataset_columns); +% % dataset_columns = dataset_columns(var_filter); +% % if sum(var_filter) > 0 +% % exclude_var_set(j) = []; +% % end +% % end +% % end + + +% Construct cell array with table data, where each row denotes a group, and +% the columns are either datasets or attributes in the given group. +tmp = cell(num_rows,num_cols); % {}; +idx = []; +group_columns = {}; +for i = 1 : num_rows + disp(['Begin construction of row:',INFO.Groups(i).Name]) + + for j = 1 : num_cols + if j <= num_attr1 + ds_name = [INFO.Groups(i).Name '/' INFO.Groups(i).Datasets(j).Name]; + + %disp(ds_name) + tmp{i,j} = h5read(filename,ds_name); + if i < 2 + column_idx = str2double(INFO.Groups(i).Datasets(j).Name(3:end)); + idx = [idx,column_idx]; + end + end + if j > num_attr1 + Name = INFO.Groups(i).Attributes(j-num_attr1).Name; + Datatype = INFO.Groups(i).Attributes(j-num_attr1).Datatype.Class; + + % Detect string attributes and convert them to characters. + % TODO: extend to other necessary data types. + if strcmp(Datatype,'H5T_STRING') + convert_func = @(x) convertStringsToChars(x); + else % no data type conversion is performed + convert_func = @(x) x; + end + + tmp{i,j} = convert_func(h5readatt(filename,INFO.Groups(i).Name, Name)); + if i < 2 + + disp(Name) + % Split attribute name into column_tag (containing original + % column idx) and column_name + + delimiter_idx = find(INFO.Groups(i).Attributes(j-num_attr1).Name=='_'); + % Left most '_' determines the split location + delimiter_idx = delimiter_idx(1); + + column_idx = str2double(Name(5:delimiter_idx-1)); + group_columns = [group_columns,{Name(delimiter_idx+1:end)}]; + %disp([Name(5:delimiter_idx-1),'-',column_name]) + idx = [idx,column_idx]; + end + + end + + end + disp(['Ends construction of row:',INFO.Groups(i).Name]) + +end + +% Find original ordering of columns by sorting idx in ascending order +[~, idx_perm] = sort(idx,'ascend'); + +varNames = [dataset_columns,group_columns]; + +% Create output_table from cell array using cell2table. This as opposed to +% array2table extract the content of its cells, which facilitates filtering +% operations based on table columns/vars. + +%output_table = array2table(tmp(:,idx_perm), 'VariableNames', varNames(idx_perm)); +output_table = cell2table(tmp(:,idx_perm), 'VariableNames', varNames(idx_perm)); + + diff --git a/beamtime/readme.md b/beamtime/readme.md index 436a39c..e2bae63 100644 --- a/beamtime/readme.md +++ b/beamtime/readme.md @@ -19,6 +19,7 @@ Everyone is encouraged to use it, break it (gently), fix it, and extend it. 😄 - [Processing Workflow 🧪](#-processing-workflow) - [Script Overview ⚙️](#-script-overview) - [Typical Processing Chain 🔁](#-typical-processing-chain) + - [Track Data 🔦](#data-provenance-and-dependency-tracking) - [Prerequisites 🧰](#prerequisites) - [Installation 💻](#installation) - [Get the code](#1-get-the-code) @@ -202,6 +203,63 @@ Functions that combine or compare spectra (like `nappNorm` and `nappCopyShift`) * `'after'`: Finds the closest reference spectrum taken *after* the target. * `'spectrumNumber'`: Matches the 4-digit sequence number at the start of the filename (e.g., `0045_O1s` finds `0045_C1s`). * `'internalSelf'` / `'internalMax'`: The spectrum references itself (e.g., normalizing to its own maximum, or calculating its own energy drift). + +**Naming Rule:** Whenever practical, a script should save its primary output to a `.mat` file with the same base name as the script itself. This convention simplifies provenance tracking and dependency visualization. + +### 🔦 Data Provenance and Dependency Tracking + +Every result should be traceable back to the raw data and steps that made it — so future‑you (or your reviewer, or your supervisor at 11pm before a deadline) can actually reconstruct what happened. 🕵️‍♀️ + +nappMATLAB tracks this via dependency annotations added to your scripts. Tiny comments, big payoff: they record what a script eats, what it spits out, and what else it needs to run. + +**What you need to do in your files:** 📝 + +Slap these near the top of each script: + +```matlab +% @DEP_IN: Step1_AnalysisSelectData.mat +% @DEP_OUT: Step2_FitData.mat +% @DEP_RUN: HelperScript.m +``` + +where: + +- `@DEP_IN` 📥 — the data/files this script needs to eat before it can do anything useful. +- `@DEP_OUT` 📤 — the files it produces (its gift to future‑you). +- `@DEP_RUN` 🏃 — other scripts it depends on / calls upon like a sidekick. + +Keep these tags honest and up to date — an untagged script is basically a data ghost 👻, haunting your project with no explanation of where it came from. Once your scripts are tagged, three companion functions turn that breadcrumb trail into an actual graph: + +#### `buildDependencyGraph.m` 🕸️ +The big one. Scans a folder (and one level of subfolders) for `.m` files, hoovers up every `@DEP_IN`/`@DEP_OUT`/`@DEP_RUN` tag it can find, and builds the master dependency graph for your whole project. Run this first and keep its output `G` around — the other two functions feed off it like remoras. 🦈 +- Pops open a MATLAB figure of the full graph, color‑coded by file type (scripts, `.mat` data, everything else). +- Saves a crisp `Dependency_MasterGraph.png` plus a `Dependency_MasterGraph.tgf` file for the free [yEd Graph Editor](https://www.yworks.com/products/yed), where you can drag nodes around like a detective's conspiracy board. 🧵📌 +```matlab +G = buildDependencyGraph(); % scan current folder +G = buildDependencyGraph(scanPath); % scan a specific folder +``` + +#### `plotSingleFileDependencies.m` 🔍 +Zooms in on one file's immediate social circle: who made it, who's using it — no six‑degrees‑of‑separation nonsense, just the direct neighbors. Perfect for "wait, who's touching this file?" energy. 🤨 +```matlab +plotSingleFileDependencies(G, 'FitList.mat'); +``` + +#### `plotNodeHistory.m` 🧬 +The full ancestry.com experience for your data. Traces the **entire upstream chain** — every script and file that ever contributed, all the way back to raw data. Use it when someone asks "but where did THIS number actually come from" and you want a receipt. 🧾 +```matlab +plotNodeHistory(G, 'Results/FinalPlot.fig'); +``` + +All three save a matching high‑res PNG + `.tgf` (for yEd) next to the MATLAB figure, and highlight your target file in gold 🏆 — because it earned it. + +For the fine print, ask MATLAB nicely: + +```matlab +help buildDependencyGraph +help plotSingleFileDependencies +help plotNodeHistory +``` *** ## Prerequisites diff --git a/beamtime/rgaImportData.m b/beamtime/rgaImportData.m new file mode 100644 index 0000000..eeead46 --- /dev/null +++ b/beamtime/rgaImportData.m @@ -0,0 +1,97 @@ +function tbl = rgaImportData(fullFileName) +%RGAIMPORTDATA Import an SRS RGA "Pressure vs Time" scan (.txt) into a table. +% +% tbl = rgaImportData(fullFileName) reads an RGA ASCII log and returns a +% table ready for plotRgaData, with: +% datetime - absolute time (start time + Time(s)) +% time_s - elapsed scan time in seconds +% one column per active channel, named _m (e.g. Water_m18), +% holding the partial pressure in the file's units (usually Torr). +% +% The scan units and channel metadata are stored in +% tbl.Properties.VariableUnits and tbl.Properties.UserData. +% +% Two quirks of this file format are handled: +% * multi-word species names in the channel table (e.g. "Carbn dio") +% * adjacent negative values in the data run together with no space +% (e.g. "-2.8663e-010-2.3529e-010"); values are recovered by matching +% scientific-notation tokens rather than by splitting on whitespace. +% +% EXAMPLE +% tbl = rgaImportData('rga1_Jul_01_2026_08-23-06_AM.txt'); +% ax = plotRgaData(tbl); +% +% See also PLOTRGADATA, PLOTCRYODATA + + lines = readlines(fullFileName); % string array, keeps blanks + + %--- Locate the data block header "Time(s) Channel#1 ..." --- + iData = find(startsWith(strip(lines),'Time(s)'), 1); + if isempty(iData) + error('rgaImportData:noData','Could not find the "Time(s)" data header.'); + end + header = lines(1:iData-1); % metadata + channel table + + %--- Start time (absolute) --- + iStart = find(startsWith(strip(header),'Start time,'), 1); + if isempty(iStart) + startStr = strip(header(1)); % fall back to first line + else + startStr = strip(extractAfter(header(iStart),',')); + end + startStr = regexprep(startStr,'\s+',' '); % collapse the double space + startTime = datetime(startStr, ... + 'InputFormat','MMM d, yyyy h:mm:ss a', 'Locale','en_US'); + + %--- Units --- + units = 'a.u.'; + iu = find(startsWith(strip(header),'Units,'), 1); + if ~isempty(iu) + p = strip(split(header(iu),',')); + units = char(p(end)); + end + + %--- Channel definitions (e.g. "1 28.00 Nitrogen 1.00 3 OFF") --- + chanPat = '^\s*(\d+)\s+([\d.]+)\s+(.+?)\s+[\d.]+\s+\d+\s+\w+\s*$'; + tok = regexp(header, chanPat, 'tokens', 'once'); + tok = tok(~cellfun(@isempty, tok)); + nCh = numel(tok); + if nCh == 0 + error('rgaImportData:noChannels','No channel definitions found.'); + end + chMass = zeros(nCh,1); chName = strings(nCh,1); + for k = 1:nCh + chMass(k) = str2double(tok{k}{2}); + chName(k) = strip(string(tok{k}{3})); + end + + %--- Data: time before the comma + N scientific-notation values --- + dataLines = lines(iData+1:end); + dataLines = dataLines(strlength(strip(dataLines)) > 0); + n = numel(dataLines); + + time_s = str2double(extractBefore(dataLines, ',')); + matches = regexp(dataLines, '[-+]?\d+\.\d+[eE][-+]?\d+', 'match'); + + vals = nan(n, nCh); + for r = 1:n + v = str2double(matches{r}); + nn = min(numel(v), nCh); + vals(r,1:nn) = v(1:nn); + end + + %--- Assemble table --- + dt = startTime + seconds(time_s); + varNames = arrayfun(@(k) matlab.lang.makeValidName( ... + sprintf('%s_m%d', chName(k), round(chMass(k)))), 1:nCh, 'UniformOutput', false); + + tbl = table(dt, time_s, 'VariableNames', {'datetime','time_s'}); + for k = 1:nCh + tbl.(varNames{k}) = vals(:,k); + end + + tbl.Properties.VariableUnits = [{'', 's'}, repmat({char(units)}, 1, nCh)]; + tbl.Properties.UserData = struct( ... + 'units', char(units), 'startTime', startTime, ... + 'channelName', chName, 'channelMass', chMass); +end diff --git a/beamtime/save_table_as_hdf5.m b/beamtime/save_table_as_hdf5.m new file mode 100644 index 0000000..5391e72 --- /dev/null +++ b/beamtime/save_table_as_hdf5.m @@ -0,0 +1,155 @@ +function success = save_table_as_hdf5(input_table, filename) + +% This function takes input_table (of table type), structured with +% Thorsten's FileTable format, and stores it as .h5 file with name +% filename. Each row is treated as a group, where columns can be attributes +% of the group or datasets inside the group. + +% Last update: 11.08.2023 +% By: Juan F. Florez Ospina +% Email: juan.florez-ospina@psi.ch + +if istable(input_table) + + string_columns = input_table(:,vartype("string")).Properties.VariableNames; + input_table = convertvars(input_table,string_columns,'cellstr'); + % split multicolumn variables + input_table = splitvars(input_table); + column_names = input_table.Properties.VariableNames; +else + success = 0; + return +end + +% Create array with indices of column_names +column_names_idx = 1 : length(column_names); + +% Divide input_table into tables with numeric and cellstr variables/columns +numeric_table = input_table(:,vartype('numeric')); % group attributes of a timestamped measurement +cellstr_table = input_table(:,vartype('cellstr')); % group datasets of a timestamped measurement + +% Record columns/vars original index in reference table +numeric_columns = numeric_table.Properties.VariableNames; +numeric_columns_idx = argwhere_var_in_varset(numeric_columns,column_names); + +str_columns = cellstr_table.Properties.VariableNames; +str_columns_idx = argwhere_var_in_varset(str_columns,column_names); + +% Based on homogeneous columns/cell arrays, we identify the remaining columns +% with possibly heterogeneous entries. Ideally, these should store numeric +% n-dimensional arrays/datasets. + +rest_columns = {}; +cntr = 1; +for i = 1 : length(input_table.Properties.VariableNames) + variable = input_table.Properties.VariableNames{i}; + + is_str = sum(strcmp(variable,str_columns))>0; + is_num = sum(strcmp(variable,numeric_columns))>0; + if ~(is_num || is_str) + rest_columns{cntr} = variable; + disp(variable) + cntr = cntr + 1; + end +end + +rest_columns_idx = argwhere_var_in_varset(rest_columns,column_names); + +% Create an hdf5 file, where rows are seen as measurements/groups with +% attributes given by numeric and str variables and datasets given by the +% remaining variables as long as they are numeric n-dimensional arrays. + +cell_table = input_table(:,rest_columns); +[rows, cols] = size(input_table); + +for i = 1 : rows + group_name = strcat("/MEAS_",string(i),"/"); + if ~isempty(rest_columns) + for j = 1 : length(rest_columns) + dataset_name = strcat("DS",string(rest_columns_idx(j))); + + % Some table entries do not provide direct access to the object + % of interest, but may contain either {object} or {{object}}. + % The following loop solves that problem. TODO: is this generic enough? + + data = cell_table{i,column_names{rest_columns_idx(j)}}{1}; % <=> data = cell_table{i,rest_columns{j}}{1}; + + while iscell(data) + data = data{1}; + end + % when there is no data, we still need to provide a valid size + if ~isempty(data) + sz = size(data); + else + sz = [1,1]; + end + %%%% + % Group's dataset: Create dataset of size sz in a certain group + h5create(filename, strcat(group_name,dataset_name),sz); + % Dataset's attribute: Add attribute with dataset_name equal to column_name + attr = "column_name"; + attr_value = column_names{rest_columns_idx(j)}; % <=> rest_columns{j}; + h5writeatt(filename,strcat(group_name,dataset_name),attr,attr_value) + + %attr = "column_name_idx"; + %attr_value = rest_columns_idx(j); % <=> rest_columns{j}; + %h5writeatt(filename,strcat(group_name,dataset_name),attr,attr_value) + + % Assign 'data' to the dataset. 'data' has to be of admissible type. + try + h5write(filename,strcat(group_name,dataset_name),data) + catch exception + disp(exception) + %h5write(filename,strcat(group_name,dataset_name),cell_table{i,rest_columns{j}}) + end + end + else + dataset_name = "DS_EMPTY"; + sz = [1,1]; + h5create(filename, strcat(group_name,dataset_name),sz); + end + + % Group's attribute: Assign group attributes + for attr_idx = 1 : length(numeric_columns) + attr = column_names{numeric_columns_idx(attr_idx)};% <=> attr = numeric_columns{attr_idx}; + attr_value = numeric_table{i,attr}; + + column_idx_tag = ['ATTR',num2str(numeric_columns_idx(attr_idx)),'_']; + h5writeatt(filename,group_name,[column_idx_tag,attr],attr_value) + + %attr = "column_name_idx"; + %attr_value = numeric_columns_idx(attr_idx); % <=> rest_columns{j}; + + %h5writeatt(filename,group_name,attr,attr_value) + end + + for attr_idx = 1 : length(str_columns) + + attr = column_names{str_columns_idx(attr_idx)}; % <=> attr = str_columns{attr_idx} + attr_value = cellstr_table{i,attr}; + + column_idx_tag = ['ATTR',num2str(str_columns_idx(attr_idx)),'_']; + h5writeatt(filename,group_name,[column_idx_tag,attr],attr_value) + + %attr = "column_name_idx"; + %attr_value = str_columns_idx(attr_idx); % <=> rest_columns{j}; + + %h5writeatt(filename,group_name,attr,attr_value) + end + %h5create(filename,"/g2/DS2",[20 Inf],"Chunksize",[5 5]); +end + +success = 1; + +function idx = argwhere_var_in_varset(var,varset) + ref_idx = 1 : length(varset); + if iscellstr(var) + idx = []; + for i = 1 : length(var) + logical_idx = strcmp(var{i},varset); + idx = [idx,ref_idx(logical_idx)]; + end + else + logical_idx = strcmp(var,varset); + idx = ref_idx(logical_idx); + end