Both Scienta (.ibw) and SpecsLab (.xy) raw files capture multidimensional XPS data but use different terminology to structure it. Scienta uses "Region Iterations" and "Sequence Iterations". SpecsLab organizes data by "Groups" or "Regions" (different elemental energy sweeps), "Scans" (time-resolved repetitions), and "Curves". Scienta Region Iterations are automatically averaged to improve the signal-to-noise ratio, while Scienta Sequence Iterations and SpecsLab Scans are preserved to keep the time-resolved data. For SpecsLab files specifically, distinct Regions are split into separate table rows. Ultimately, the preserved time-resolved data is saved in the table's image variable as a 2D matrix ([Energy bins × Sequence iterations/Scans]). File type handling: Added .xy as a recognized XPS format. Currently, the script differentiates data types strictly by file extension (NEXAFS: .h5, XPS: .ibw and .xy). This extension-based differentiation needs to be improved in future updates. Metadata parsing: Implemented global header scanning for .xy files to correctly extract instrument parameters (Acquisition Mode, Lens Mode, Slit, Workfunction) located before the first region block. Timezone alignment: Added timestamp conversion for .xy files from UTC to local time (Europe/Zurich) to ensure synchronization with LabView logs. Region separation: Modified .xy parsing to append a _RegX suffix to filenames. This correctly splits single .xy files that contain multiple elements or energy sweeps into distinct table rows so they can be processed individually. Import efficiency: Added an originalFileName variable. The script now cross-references the input DataTable and skips files that have already been imported to reduce execution time on re-runs. Testing status: Processing of .xy data has currently only been tested and debugged with nappGenerateSpectra and nappPlot. Compatibility with other functions in the suite remains untested.
1850 lines
81 KiB
Matlab
Executable File
1850 lines
81 KiB
Matlab
Executable File
function ListFiles = nappFileList(dataPath,varargin)
|
|
%NAPPFILELIST Import spectroscopy data (XPS, NEXAFS) from a beam time folder into data table.
|
|
% Photos, analyser and sample settings such as pass energy, lens mode, temperature,
|
|
% position, and pressure are automatically extracted and added to the table.
|
|
%
|
|
% SYNTAX
|
|
% ------
|
|
% ListFiles = NAPPFILELIST(dataPath)
|
|
% ListFiles = NAPPFILELIST(dataPath, DataTable)
|
|
% ListFiles = NAPPFILELIST(dataPath, DataTable, 'Name', Value)
|
|
%
|
|
% DESCRIPTION
|
|
% -----------
|
|
% NAPPFILELIST scans a beam time folder and imports all experimental data
|
|
% files (XPS, NEXAFS, and photos) with their associated
|
|
% metadata into a MATLAB table. The function automatically:
|
|
% - Reads XPS data and metadata from SES (.ibw) or Specs (.xy) files
|
|
% - Reads NEXAFS data and metadata from HDF5 (.h5) files
|
|
% - Splits multi-scan HDF5 files into individual single-scan files
|
|
% - Extracts instrument settings (pass energy, dwell time, lens mode, etc.)
|
|
% - Normalizes count rates (per dwell time) to counts per second (cps)
|
|
% - Averages region iterations during import
|
|
% - Creates a semicolon-separated CSV file for Excel import
|
|
%
|
|
% REQUIRED INPUT ARGUMENTS
|
|
% ------------------------
|
|
% dataPath
|
|
% Beam time folder path (string or char vector).
|
|
% Format: 'YYYY/BeamtimeName'(Mac,Linux,Unix) or 'YYYY\BeamtimeName' (Windows)
|
|
% Examples: '2025/November_NappIss', '2024/March_PolLux'
|
|
%
|
|
% Note: The folder structure must follow the strict naming convention
|
|
% described in the "Folder Structure" section below.
|
|
%
|
|
% OPTIONAL POSITIONAL INPUT
|
|
% -------------------------
|
|
% DataTable
|
|
% Existing data table to append imported data to (table object).
|
|
% If provided, new files are merged with existing table.
|
|
% If not provided, a new table is created from scratch.
|
|
%
|
|
% NAME-VALUE PAIR ARGUMENTS
|
|
% -------------------------
|
|
% 'mode'
|
|
% Import mode (char or string, default: 'local').
|
|
% Supported values:
|
|
% 'local' - Import from local computer (default)
|
|
% 'nappDrive' - Import from mounted NAPP network drive
|
|
% 'dataDrive' - Import from mounted group drive (5505)
|
|
% 'localNew' - Import only new files not in existing DataTable
|
|
% 'nappDrivePick' - Browse and select files from NAPP drive
|
|
% 'localPick' - Browse and select files from local folder
|
|
% 'dataPick' - Browse and select files from mounted group drive
|
|
%
|
|
% 'importLabView'
|
|
% Import LabView pressure/temperature logs (char or string).
|
|
% Default: 'on'
|
|
% Supported values:
|
|
% 'on' - Import LabView logs from Pressure subfolder
|
|
% 'off' - Skip LabView log import (this is much,much faster)
|
|
%
|
|
% OUTPUT ARGUMENTS
|
|
% ----------------
|
|
% ListFiles
|
|
% MATLAB table with the following columns (variables):
|
|
% - name: file name (string)
|
|
% - folder: full path to file (string)
|
|
% - date: file modification date (datetime)
|
|
% - bytes: file size in bytes (numeric)
|
|
% - isdir: directory flag (logical)
|
|
% - datenum: date as serial number (numeric)
|
|
% - creationDate: acquisition timestamp (datevec format)
|
|
% - image: 2D spectral map or image (cell array)
|
|
% - imageRoh: raw 2D spectral map (cell array)
|
|
% - spectrum_counts: integrated spectrum (numeric or cell array)
|
|
% - spectrum_countsPerSecond: mean spectrum normalized to cps
|
|
% - scientaEkin_eV: kinetic energy axis (cell array, eV)
|
|
% - scientaEkinRange_eV: [min max] kinetic energy range
|
|
% - scientaPassEnergy_eV: analyzer pass energy (numeric, eV)
|
|
% - scientaDwellTime_ms: dwell time per channel (numeric, ms)
|
|
% - regionName: region label (e.g., 'C1s', 'O K-edge NEXAFS')
|
|
% - scientaAcquisitionMode: 'Swept' or 'Fixed' mode
|
|
% - scientaLensMode: lens transmission mode (string)
|
|
% - scientaRegionIterations: number of region scans (numeric)
|
|
% - scientaSequenceIterations: number of sequence repeats (numeric)
|
|
% - xRayEkin_eV: X-ray photon energy for XPS (numeric or cell array, eV)
|
|
% - xRayEkinRange_eV: [min max] photon energy range
|
|
% - beamlineInt: beamline intensity (I_0 for NEXAFS)
|
|
% - importDate: timestamp of table generation (char)
|
|
% - analysisDir: working directory at import time (string)
|
|
%
|
|
% Also creates: YYYYMmm_DataList.csv
|
|
% Semicolon-separated ASCII file in beam time folder for Excel import.
|
|
%
|
|
% FOLDER STRUCTURE & NAMING CONVENTIONS
|
|
% =====================================
|
|
%
|
|
% REQUIRED DIRECTORY STRUCTURE
|
|
% ----------------------------
|
|
% The beam time folder must contain the following sub-directories:
|
|
%
|
|
% SES/ - XPS data in Igor (.ibw) or Specs (.xy) format
|
|
% NEXAFS/ - NEXAFS maps in HDF5 (.h5) format
|
|
% Photos/ - Sample photographs in BMP (.bmp) format
|
|
% Pressure/ - LabView pressure and temperature logs
|
|
% PShell/ - XPS data in h5 format (from PShell)
|
|
%
|
|
% Full path convention:
|
|
% LOCAL: ~/Forschung_Einsam/PSI_SLS/NAPP/SC Data/YYYY/BeamtimeName
|
|
% NAPP DRIVE: /Volumes/NAPP/SC Data/YYYY/BeamtimeName (Mac)
|
|
% \\fs03\NAPP\SC Data\YYYY\BeamtimeName (Windows)
|
|
% DATA DRIVE: /Volumes/5505/Data/YYYY/BeamtimeName (Mac)
|
|
% \\fs101\5505\Data\YYYY\BeamtimeName (Windows)
|
|
%
|
|
% EXAMPLE FOLDER LAYOUT
|
|
% ---------------------
|
|
% 2025/November_NappIss/
|
|
% SES/ [XPS data]
|
|
% 0001_C1s_525eV.ibw
|
|
% 0002_O1s_850eV.ibw
|
|
% 0003_Survey_850eV.ibw
|
|
% ...
|
|
% NEXAFS/ [NEXAFS maps]
|
|
% 0010_O_Kedge.h5
|
|
% 0011_N_Kedge.h5
|
|
% ...
|
|
% Photos/ [Sample photos]
|
|
% A0001-20251123_102407.bmp
|
|
% A0002-20251123_102408.bmp
|
|
% ...
|
|
% Pressure/ [Environmental logs]
|
|
% LabView_T_RH_20251119.txt
|
|
% logfile-23-11-2025-12h46min.txt
|
|
%
|
|
% FILE NAMING CONVENTIONS
|
|
% -----------------------
|
|
%
|
|
% XPS FILES (.ibw, .xy)
|
|
% Format: NNNN_RegionName_PhotonEnergy.ext
|
|
% Example: 0191_C1s_525eV.ibw
|
|
% 0192_O1s_850eV.ibw
|
|
% 0045_Survey_1000eV.ibw
|
|
%
|
|
% NNNN = Four-digit SES file number (from instrument)
|
|
% RegionName = XPS region: C1s, O1s, N1s, Si2p, Cl2p, Survey, etc.
|
|
% PhotonEnergy = X-ray photon energy in eV
|
|
% .ibw = Igor file format (Scienta SES analyzer)
|
|
% .xy = Specs SpecsLab file format
|
|
%
|
|
% NEXAFS FILES (.h5)
|
|
% Format: NNNN_ElementName_EdgeType.h5
|
|
% Example: 0200_O_Kedge.h5
|
|
% 0201_N_Kedge.h5
|
|
% 0202_C_Kedge.h5
|
|
%
|
|
% NNNN = Four-digit file number
|
|
% ElementName = Element symbol: O, N, C, etc.
|
|
% EdgeType = K-edge, L-edge, etc.
|
|
%
|
|
% Note: Files starting with "N" are recognized. Files starting with
|
|
% other characters (e.g., from old naming schemes) will be ignored.
|
|
%
|
|
% PHOTO FILES (.bmp)
|
|
% Format: descriptive_name.bmp
|
|
% Example: sample_overview.bmp
|
|
% before_dosing.bmp
|
|
% after_annealing.bmp
|
|
%
|
|
% DATA FILE REQUIREMENTS & LIMITATIONS
|
|
% ====================================
|
|
%
|
|
% - At least ONE .ibw or .xy file must exist in SES/ subfolder
|
|
% - Do NOT run while SES or NEXAFS data are being actively recorded
|
|
% - Multi-scan HDF5 files are automatically split (one scan per file)
|
|
% - Region iterations are averaged during import (not preserved individually)
|
|
% - Counts are normalized to 1 second dwell time (results in cps)
|
|
% - System files starting with '.' or '~' are automatically excluded
|
|
%
|
|
% HDF5 MULTI-SCAN HANDLING
|
|
% -------------------------
|
|
% If an HDF5 file contains multiple scans (>3 groups detected):
|
|
% 1. Function calls splitH5Scans(filename)
|
|
% 2. Each scan is extracted into a separate file
|
|
% 3. New files: original_name_scan_1.h5, original_name_scan_2.h5, etc.
|
|
% 4. Original multi-scan file is deleted after splitting
|
|
%
|
|
% EXAMPLES
|
|
% ========
|
|
%
|
|
% % Example 1: Import all data from local folder
|
|
% FileList = nappFileList('2025/November_NappIss', 'mode', 'local');
|
|
%
|
|
% % Example 2: Add new data to existing table
|
|
% FileList = nappFileList('2025/November_NappIss', FileList, 'mode', 'local');
|
|
%
|
|
% % Example 3: Import only new files not yet in table
|
|
% FileList = nappFileList('2025/November_NappIss', FileList, ...
|
|
% 'mode', 'localNew');
|
|
%
|
|
% % Example 4: Import from network drive with file browser
|
|
% FileList = nappFileList('2025/November_NappIss', ...
|
|
% 'mode', 'nappDrivePick');
|
|
%
|
|
% % Example 5: Skip LabView pressure/temperature logs
|
|
% FileList = nappFileList('2025/November_NappIss', ...
|
|
% 'mode', 'local', 'importLabView', 'off');
|
|
%
|
|
% DEPENDENCIES
|
|
% ============
|
|
% - IBWread function (Igor Pro format reader for MATLAB)
|
|
% https://ch.mathworks.com/matlabcentral/fileexchange/42679-igor-pro-file-format-ibw-to-matlab-variable
|
|
% - h5info, h5read (HDF5 file support, built-in MATLAB)
|
|
% - splitH5Scans function (custom, included in toolkit)
|
|
% - LabView_importData function (custom, included in toolkit)
|
|
%
|
|
% TROUBLESHOOTING
|
|
% =======================
|
|
%
|
|
% - If "path wrong" warning appears: check folder spelling and that
|
|
% network drives are mounted
|
|
% - If Igor files fail to read: verify files are complete and not corrupted
|
|
% (partially recorded files should be renamed with "ignore_" prefix)
|
|
% - For older data (pre-2020): HDF5 file structure may differ; function
|
|
% attempts multiple path conventions automatically
|
|
% - Mac: Creation date for NEXAFS files obtained via 'stat' command
|
|
% - Windows: Creation date for NEXAFS files set to NaN
|
|
%
|
|
% MATLAB VERSION REQUIREMENTS
|
|
% ===========================
|
|
% - MATLAB 2018a or higher (for '**' notation in dir patterns)
|
|
%
|
|
% HISTORY & UPDATES
|
|
% =================
|
|
% - January 2020: Added new h5 file structure and naming convention
|
|
% - March 2021: Added local file structure support (not just network)
|
|
% - March 2021: Added Lens Mode column support
|
|
% - April 2021: X-ray kinetic energy and images now made unique
|
|
% - March 2022: Refactored to use varargin instead of positional arguments
|
|
% - March 2022: Strict data structure required for local and remote sources
|
|
% - November 2025: Added support for PShell h5 file structure (NEXAFS)
|
|
% - November 2025: Added creation date extraction for Mac (NEXAFS)
|
|
%
|
|
% See also: TABLE, IBWread, h5read, h5info, DIR, inputParser
|
|
%
|
|
% Copyright: Laboratory of Atmospheric Chemistry, Paul Scherrer Institut (PSI)
|
|
% thorsten.bartels-rausch@psi.ch
|
|
%
|
|
% =========================================================================
|
|
%% Updated
|
|
%
|
|
% * January 2020, added new h5 file structure and naming convention.
|
|
% * March 2021, added possibility to work on local file structure
|
|
% * March 2021, added Lens Mode as column (have not tested this for older
|
|
% NEXAFS files).
|
|
% * April 2021, X-ray kinetic energy and images are now made unique
|
|
% * March 2022, input now using varargin
|
|
% * March 2022, strict data structure required for local and remote sources
|
|
% * November 2025, PShell h5 files for NEXAFS have a new structure;
|
|
% * November 2025, added creation date to mac for NEXAFS.
|
|
% * March 2026, added movie import from photos folder, added jpg as photo
|
|
% file
|
|
|
|
%% Input
|
|
% Define conditions and defaults of input
|
|
defaultMode = 'local';
|
|
expectedMode = {'local','nappDrive','dataDrive','localNew','nappDrivePick','localPick','dataPick'};
|
|
defaultImportLabView = 'on';
|
|
expectedImportLabView = {'on','off'};
|
|
% Parse input
|
|
p = inputParser;
|
|
addRequired(p,'dataPath');
|
|
addOptional(p,'DataTable',@istable);
|
|
addParameter(p,'mode',defaultMode,...
|
|
@(x) any(validatestring(x,expectedMode)));
|
|
addParameter(p,'importLabView',defaultImportLabView,...
|
|
@(x) any(validatestring(x,expectedImportLabView)));
|
|
parse(p,dataPath,varargin{:});
|
|
%% Data Paths
|
|
%
|
|
% Mac and Windows use different notations to address network drives. For
|
|
% windows, we use the UNC naming convention. This is a more recent way to
|
|
% address mounted drives that does not rely on the use of drive letters.
|
|
% The letters might be different for the same drive on different computer.
|
|
|
|
|
|
if contains(p.Results.mode,'local')
|
|
% Data server address
|
|
if ismac
|
|
server = '/Users/bartels-rausch/Documents/Forschung_Einsam/PSI_SLS/';
|
|
elseif ispc
|
|
server = userpath;
|
|
end
|
|
elseif contains(p.Results.mode,'nappDrive')
|
|
% Data server address
|
|
if ismac
|
|
server = '/Volumes/';
|
|
elseif ispc
|
|
server = '\\fs03\';
|
|
end
|
|
elseif contains(p.Results.mode,'data')
|
|
% Data server address
|
|
if ismac
|
|
server = '/Volumes/';
|
|
elseif ispc
|
|
server = '\\fs101\';
|
|
end
|
|
end
|
|
if contains(p.Results.mode,'data')
|
|
dataPath = fullfile(server,'5505','Data',dataPath);
|
|
else
|
|
dataPath = fullfile(server,'NAPP','SC Data',dataPath);
|
|
end
|
|
% Data Paths to subdirectories for specific files:
|
|
dataPath_XPS = fullfile(dataPath, 'SES');
|
|
dataPath_Photo = fullfile(dataPath, 'Photos');
|
|
dataPath_NEXAFS = fullfile(dataPath, 'NEXAFS');
|
|
dataPath_LabView = fullfile(dataPath, 'Pressure');
|
|
|
|
%
|
|
if ~isfolder(dataPath_XPS)
|
|
warning('There is something wrong with the path. Check for typo and that the drives are mounted')
|
|
disp('There is something wrong with the path. Check for typo and that the drives are mounted')
|
|
disp('I quit now')
|
|
return
|
|
end
|
|
|
|
|
|
%% Naming Convention
|
|
%
|
|
% See: MATLAB Programming Style Guidelines, by Richard Johnson
|
|
%
|
|
% * Variable: mixed case words starting with lower case: credibleThreat, qualityOfLife
|
|
% * Variables with a large scope should have meaningful names. Variables
|
|
% with a small scope can have short names.
|
|
% * A convention on pluralization should be followed consistently.
|
|
% * Structure names should begin with a capital letter: SegmentData.length
|
|
% * The prefix n should be used for variables representing the number of
|
|
% objects. A MATLAB-specific addition is the use of m for number of rows
|
|
% (based on matrix notation). nFiles, mRows
|
|
% * Iterator variables should be named or prefixed with i. for iPosition =
|
|
% 1:nPositions, ii, ij, ik....
|
|
% * Functions should have meaningful names written in lower case.
|
|
% computetotalwidth(), Avoid compwid();
|
|
|
|
%% Query beam time folder content
|
|
%
|
|
% We use MATLAB's 'dir' command which writes the content of a folder into a
|
|
% structure with the following fields:
|
|
%
|
|
% * name
|
|
% * folder
|
|
% * date
|
|
% * bytes
|
|
% * isdir
|
|
% * datenum
|
|
%
|
|
% XPS data: Create a structure 'TmpFileList{1}' with all Igor files in the
|
|
% directory and its subfolders (1st level). The '\N*' at the beginning is
|
|
% important. Otherwise hidden files starting with a . will also be listed.
|
|
% Obviously, Igor file names from SES software and h5 file names from Epics
|
|
% (NEXAFS) need to start with an N, otherwise change the search term below:
|
|
%
|
|
% THE ** notation needs a recent version of Matlab, 2014 does not work.
|
|
|
|
if endsWith(p.Results.mode, 'Pick')
|
|
% --- PICK MODE ---
|
|
[filePick, pathPick] = uigetfile({'*.ibw;*.xy;*.h5'}, ...
|
|
'Select One or More Files', dataPath, 'MultiSelect', 'on');
|
|
|
|
if ischar(filePick), filePick = {filePick}; end % Force cell array
|
|
|
|
% Initialize empty dir structs
|
|
TmpFileList{1} = struct('name',{},'folder',{},'date',{},'bytes',{},'isdir',{},'datenum',{});
|
|
TmpFileList{2} = TmpFileList{1};
|
|
TmpFileList{3} = TmpFileList{1}; % Photos always empty in pick mode
|
|
|
|
% Populate XPS and NEXAFS
|
|
if ~isequal(filePick, 0) % If user didn't cancel
|
|
for ii = 1:length(filePick)
|
|
if endsWith(filePick{ii}, {'.ibw', '.xy'}, 'IgnoreCase', true)
|
|
TmpFileList{1} = [TmpFileList{1}; dir(fullfile(pathPick, filePick{ii}))];
|
|
elseif endsWith(filePick{ii}, '.h5', 'IgnoreCase', true)
|
|
TmpFileList{2} = [TmpFileList{2}; dir(fullfile(pathPick, filePick{ii}))];
|
|
end
|
|
end
|
|
end
|
|
|
|
else
|
|
|
|
% --- FOLDER SEARCH MODE ---
|
|
|
|
% 1. XPS (Both .ibw and .xy)
|
|
tmpIbw = dir(fullfile(dataPath_XPS, '**', '*.ibw'));
|
|
tmpIbw = tmpIbw(find(~cellfun(@isempty,regexp({tmpIbw .name},'^[a-zA-Z0-9]\w*')))); %#ok<FNDSB>
|
|
tmpXy = dir(fullfile(dataPath_XPS, '**', '*.xy'));
|
|
tmpXy = tmpXy(find(~cellfun(@isempty,regexp({tmpXy .name},'^[a-zA-Z0-9]\w*')))); %#ok<FNDSB>
|
|
TmpFileList{1} = [tmpIbw; tmpXy]; % Stack them together
|
|
|
|
% 2. NEXAFS
|
|
TmpFileList{2} = dir(fullfile(dataPath_NEXAFS, '**', '*.h5'));
|
|
TmpFileList{2} = TmpFileList{2}(find(~cellfun(@isempty,regexp({TmpFileList{2}.name},'^[a-zA-Z0-9]\w*')))); %#ok<FNDSB>
|
|
|
|
|
|
% 3. Photos
|
|
photoExts = {'*.bmp', '*.jpg', '*.JPG', '*.mov', '*.avi', '*.mp4', '*.wmv'};
|
|
TmpFileList{3} = [];
|
|
for i = 1:length(photoExts)
|
|
TmpFileList{3} = [TmpFileList{3}; dir(fullfile(dataPath_Photo, '**', photoExts{i}))];
|
|
end
|
|
TmpFileList{3} = TmpFileList{3}(find(~cellfun(@isempty,regexp({TmpFileList{3}.name},'^[a-zA-Z0-9]\w*')))); %#ok<FNDSB>
|
|
|
|
% Clean up system/hidden files for all lists
|
|
for i = 1:3
|
|
if ~isempty(TmpFileList{i})
|
|
% Keep only files that do NOT start with '.' or '~'
|
|
validIdx = ~startsWith({TmpFileList{i}.name}, {'.', '~'});
|
|
TmpFileList{i} = TmpFileList{i}(validIdx);
|
|
end
|
|
end
|
|
|
|
% =========================================================================
|
|
% SPLIT HDF5 FILES WITH MULTIPLE DATASETS INTO INDIVIDUAL FILES
|
|
% =========================================================================
|
|
%
|
|
% BACKGROUND:
|
|
% HDF5 files from the beamline can contain multiple experimental scans
|
|
% (e.g., scan_1, scan_2, scan_3) stored as separate groups within a
|
|
% single file. However, the NEXAFS and XPS data processing pipelines
|
|
% are designed to work with one scan per file.
|
|
%
|
|
% PURPOSE:
|
|
% This script identifies HDF5 files that contain multiple scans and
|
|
% splits each scan into a separate file with a descriptive name.
|
|
% Each output file contains only one scan (renamed to scan_1) plus
|
|
% the logs and metadata groups.
|
|
%
|
|
% PROCESS:
|
|
% 1. Loop through all NEXAFS HDF5 files in the file list
|
|
% 2. Check if each file contains more than 3 groups (indicating multiple scans)
|
|
% 3. If it does, split it using the splitH5Scans function
|
|
% 4. The original file is deleted after splitting
|
|
%
|
|
% OUTPUT:
|
|
% For each multi-scan file, creates separate output files:
|
|
% Example: 0002_..._O_Kedge.h5 0002_..._O_Kedge_scan_1.h5
|
|
% 0002_..._O_Kedge_scan_2.h5
|
|
% =========================================================================
|
|
% Loop through each file in the NEXAFS file list
|
|
% TmpFileList{2} contains the full paths to all NEXAFS HDF5 files
|
|
% ii is the loop counter (1, 2, 3, ... up to the total number of files)
|
|
for ii = 1:size(TmpFileList{2}, 1)
|
|
|
|
% Get the filename of the current file being processed
|
|
current_file = [TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name];
|
|
|
|
% 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__'}
|
|
groupnames = {fileinfo.Groups.Name};
|
|
|
|
% Check if this file has more than 3 groups
|
|
% Files with more than 3 groups likely contain multiple scans
|
|
% (the extra groups beyond scans are typically '/logs' and '/__DATA_TYPES__')
|
|
if numel(groupnames) > 3
|
|
|
|
% This file contains multiple scans, so split it
|
|
% The splitH5Scans function will:
|
|
% - Create separate files for each scan
|
|
% - Rename each scan to '/scan_1' within its new file
|
|
% - Delete the original multi-scan file when done
|
|
splitH5Scans(current_file);
|
|
|
|
end
|
|
|
|
end
|
|
|
|
% =========================================================================
|
|
% NOTE: After this loop completes, all NEXAFS files in data folder
|
|
% will contain exactly one scan per file, ready for processing
|
|
% =========================================================================
|
|
|
|
% EPICS files in h5 format list all files, then get rid of system files
|
|
TmpFileList{2} = dir([dataPath_NEXAFS filesep '**' filesep '*.h5']);
|
|
TmpFileList{2} = TmpFileList{2}(find(~cellfun(@isempty,regexp({TmpFileList{2}.name},'^[a-zA-Z0-9]\w*')))); %#ok<FNDSB>
|
|
|
|
disp('*************************************************')
|
|
disp('* Voila: Splitting NEXAFS files done *')
|
|
disp('*************************************************')
|
|
|
|
% --- Date of First File Check ---
|
|
% Since TmpFileList{1} now safely contains all .ibw and .xy files,
|
|
% we don't need to run dir() again! We just check the struct directly.
|
|
if isempty(TmpFileList{1})
|
|
warning('There appears to be no XPS file (.ibw or .xy) in the folder. Add at least one file first.');
|
|
return;
|
|
end
|
|
|
|
try
|
|
filedate_first = TmpFileList{1}(1).datenum;
|
|
catch
|
|
warning('There appears to be something wrong with the XPS file metadata.');
|
|
end
|
|
|
|
end
|
|
%% Import XPS data to get SES settings
|
|
%
|
|
% Uses the IgorPro importer for Matlab:
|
|
% https://ch.mathworks.com/matlabcentral/fileexchange/42679-igor-pro-file-format-ibw-to-matlab-variable
|
|
%
|
|
% Normally, Matlab stops when an error occurs. I use 'try' to force Matlab
|
|
% to continue even if an error occurs. Matlab will instead print a warning,
|
|
% as defined after the 'catch'. Error might occur when single data files
|
|
% can't be read. If a warning comes up, check the corresponding data file
|
|
% and try to resolve it. I suggest to rename these original data files that
|
|
% are damaged, or not complete (because measurement was aborted) to
|
|
% "ignore_filename".
|
|
%% 2. Import XPS data
|
|
ProcessedXPS = []; % This will hold our final list of structs
|
|
|
|
if ~isempty(TmpFileList{1})
|
|
for ii = 1:length(TmpFileList{1})
|
|
current_file_info = TmpFileList{1}(ii);
|
|
current_path = fullfile(current_file_info.folder, current_file_info.name);
|
|
|
|
% =================================================================
|
|
% 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.lastModifiedDatestr = current_file_info.date;
|
|
current_file_info.image = {[]};
|
|
current_file_info.imageRoh = {[]};
|
|
current_file_info.xRayEkin_eV = NaN;
|
|
current_file_info.xRayEkinRange_eV = [NaN NaN];
|
|
current_file_info.scientaPassEnergy_eV = NaN;
|
|
current_file_info.scientaDwellTime_ms = NaN;
|
|
current_file_info.regionName = '';
|
|
current_file_info.scientaAcquisitionMode = '';
|
|
current_file_info.scientaEkinRange_eV = [NaN NaN];
|
|
current_file_info.scientaEkinStep_eV = NaN;
|
|
current_file_info.scientaEkin_eV = {[]};
|
|
current_file_info.scientaEkinRoh_eV = {[]};
|
|
current_file_info.beamlineInt = {[]};
|
|
current_file_info.scientaLensMode = '';
|
|
current_file_info.scientaAnalyzerSlit = '';
|
|
current_file_info.scientaWorkfunction_eV = NaN;
|
|
current_file_info.scientaRegionIterations = NaN;
|
|
current_file_info.scientaSequenceIterations = NaN;
|
|
current_file_info.importDate = datestr(now);
|
|
try
|
|
current_file_info.analysisDir = file.mFile;
|
|
catch
|
|
current_file_info.analysisDir = pwd;
|
|
end
|
|
current_file_info.sample = '';
|
|
% =================================================================
|
|
% --- Case A: Scienta (.ibw) Files ---
|
|
% =================================================================
|
|
if endsWith(current_file_info.name, '.ibw', 'IgnoreCase', true)
|
|
try
|
|
PE = IBWread(current_path);
|
|
fclose('all');
|
|
tmp = regexp(PE.WaveNotes, '\r', 'split');
|
|
|
|
try
|
|
dateStr = [char(regexp(tmp{29},'((?<=Date=).*)','match')) ' ' char(regexp(tmp{30},'((?<=Time=).*)','match'))];
|
|
parsedDate = datevec(dateStr);
|
|
if size(parsedDate, 2) == 6
|
|
current_file_info.creationDate = parsedDate;
|
|
current_file_info.lastModifiedDatenum = datenum(parsedDate);
|
|
current_file_info.lastModifiedDatestr = dateStr;
|
|
end
|
|
catch
|
|
end
|
|
|
|
current_file_info.scientaPassEnergy_eV = str2double(char(regexp(tmp{5},'((?<=Pass Energy=).*)','match')));
|
|
current_file_info.scientaDwellTime_ms = str2double(char(regexp(tmp{15},'((?<=Step Time=).*)','match')));
|
|
current_file_info.scientaEkinStep_eV = str2double(char(regexp(tmp{14},'((?<=Energy Step=).*)','match')));
|
|
current_file_info.regionName = char(regexp(tmp{3},'((?<=Region Name=).*)','match'));
|
|
current_file_info.scientaAcquisitionMode = char(regexp(tmp{9},'((?<=Acquisition Mode=).*)','match'));
|
|
current_file_info.scientaLensMode = char(regexp(tmp{4},'((?<=Lens Mode=).*)','match'));
|
|
|
|
PE.x = PE.x0(1) + (0:size(PE.y,1)-1)*PE.dx(1);
|
|
current_file_info.scientaEkin_eV = {PE.x};
|
|
current_file_info.scientaEkinRange_eV = [PE.x(1) PE.x(end)];
|
|
|
|
if PE.Ndim == 3
|
|
current_file_info.scientaRegionIterations = size(PE.y,2);
|
|
current_file_info.scientaSequenceIterations = size(PE.y,3);
|
|
PE.y = squeeze(mean(PE.y,2));
|
|
else
|
|
current_file_info.scientaRegionIterations = 1;
|
|
current_file_info.scientaSequenceIterations = size(PE.y,2);
|
|
end
|
|
|
|
PE.y = PE.y .* 1000 ./ current_file_info.scientaDwellTime_ms;
|
|
current_file_info.image = {PE.y};
|
|
current_file_info.imageRoh = {PE.y};
|
|
|
|
if isempty(ProcessedXPS), ProcessedXPS = current_file_info; else, ProcessedXPS = [ProcessedXPS; current_file_info]; end %#ok<AGROW>
|
|
catch ME
|
|
fprintf('Warning: Failed to parse %s (%s)\n', current_file_info.name, ME.message);
|
|
if isempty(ProcessedXPS), ProcessedXPS = current_file_info; else, ProcessedXPS = [ProcessedXPS; current_file_info]; end %#ok<AGROW>
|
|
end
|
|
|
|
% =================================================================
|
|
% --- Case B: Specs (.xy) Files ---
|
|
% =================================================================
|
|
elseif endsWith(current_file_info.name, '.xy', 'IgnoreCase', true)
|
|
try
|
|
[cDateOS, mDateOS] = getOSDates(current_path);
|
|
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 = '';
|
|
globalLensMode = '';
|
|
globalAnalyzerSlit = '';
|
|
globalWorkFunction = NaN;
|
|
globalExEnergy = NaN;
|
|
|
|
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:')
|
|
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:')
|
|
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');
|
|
end
|
|
end
|
|
end
|
|
|
|
for r = 1:length(regionStarts)
|
|
regionEntry = current_file_info;
|
|
startIdx = regionStarts(r);
|
|
if r < length(regionStarts), endIdx = regionStarts(r+1)-1; else, endIdx = length(fileLines); end
|
|
regionBlock = fileLines(startIdx:endIdx);
|
|
|
|
% =====================================================
|
|
% 2. Extract Metadata
|
|
% =====================================================
|
|
% Initialize defaults
|
|
rName = 'UnknownRegion';
|
|
exEnergyStr = num2str(globalExEnergy);
|
|
specID = r;
|
|
regionEntry.xRayEkin_eV = globalExEnergy;
|
|
regionEntry.scientaPassEnergy_eV = NaN;
|
|
regionEntry.scientaDwellTime_ms = NaN;
|
|
|
|
% Inherit defaults from Global Header
|
|
regionEntry.scientaAcquisitionMode = globalScanMode;
|
|
regionEntry.scientaLensMode = globalLensMode;
|
|
regionEntry.scientaAnalyzerSlit = globalAnalyzerSlit;
|
|
regionEntry.scientaWorkfunction_eV = globalWorkFunction;
|
|
regionEntry.creationDate = osDateVec;
|
|
|
|
% 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:')
|
|
regionEntry.xRayEkin_eV = sscanf(strtrim(extractAfter(lineStr, '# Excitation Energy:')), '%f');
|
|
exEnergyStr = num2str(regionEntry.xRayEkin_eV);
|
|
elseif startsWith(lineStr, '# Spectrum ID:')
|
|
specID = sscanf(strtrim(extractAfter(lineStr, '# Spectrum ID:')), '%f');
|
|
elseif startsWith(lineStr, '# Acquisition Date:')
|
|
rawDateStr = strtrim(extractAfter(lineStr, '# Acquisition Date:'));
|
|
dateStr = strtrim(regexprep(rawDateStr, 'UTC$', ''));
|
|
try
|
|
dtUTC = datetime(dateStr, 'InputFormat', 'MM/dd/yy HH:mm:ss', 'PivotYear', 2000, 'TimeZone', 'UTC');
|
|
dtLocal = datetime(dtUTC, 'TimeZone', 'Europe/Zurich');
|
|
regionEntry.lastModifiedDatenum = datenum(dtLocal);
|
|
regionEntry.lastModifiedDatestr = datestr(dtLocal);
|
|
catch
|
|
regionEntry.lastModifiedDatestr = rawDateStr;
|
|
end
|
|
elseif startsWith(lineStr, '# Pass Energy:')
|
|
regionEntry.scientaPassEnergy_eV = str2double(extractAfter(lineStr, '# Pass Energy:'));
|
|
elseif startsWith(lineStr, '# Dwell Time:')
|
|
regionEntry.scientaDwellTime_ms = str2double(extractAfter(lineStr, '# Dwell Time:')) * 1000;
|
|
elseif startsWith(lineStr, '# Scan Mode:')
|
|
regionEntry.scientaAcquisitionMode = strtrim(extractAfter(lineStr, '# Scan Mode:'));
|
|
elseif startsWith(lineStr, '# Analyzer Lens Voltage:')
|
|
regionEntry.scientaLensMode = strtrim(extractAfter(lineStr, '# Analyzer Lens Voltage:'));
|
|
elseif startsWith(lineStr, '# Analyzer Lens:')
|
|
regionEntry.scientaLensMode = strtrim(extractAfter(lineStr, '# Analyzer Lens:'));
|
|
elseif startsWith(lineStr, '# Analyzer Slit:')
|
|
regionEntry.scientaAnalyzerSlit = strtrim(extractAfter(lineStr, '# Analyzer Slit:'));
|
|
elseif startsWith(lineStr, '# Work Function:')
|
|
regionEntry.scientaWorkfunction_eV = sscanf(strtrim(extractAfter(lineStr, '# Work Function:')), '%f');
|
|
elseif startsWith(lineStr, '# Eff. Workfunction:')
|
|
regionEntry.scientaWorkfunction_eV = sscanf(strtrim(extractAfter(lineStr, '# Eff. Workfunction:')), '%f');
|
|
end
|
|
end
|
|
|
|
% --- Format Name & Region ---
|
|
regionEntry.regionName = 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:'));
|
|
tempD = []; enAxis = [];
|
|
|
|
if ~isempty(hIdx)
|
|
for d = 1:length(hIdx)
|
|
cScn = 1; cCrv = 1;
|
|
|
|
headerStr = regionBlock(hIdx(d));
|
|
curveMatch = regexp(headerStr, 'Curve:\s*(\d+)', 'tokens', 'once');
|
|
if ~isempty(curveMatch), cCrv = str2double(curveMatch{1}) + 1; end
|
|
|
|
scanMatch = regexp(headerStr, 'Scan:\s*(\d+)', 'tokens', 'once');
|
|
if ~isempty(scanMatch), cScn = str2double(scanMatch{1}) + 1; end
|
|
|
|
row = hIdx(d) + 1;
|
|
while row <= length(regionBlock) && (startsWith(strtrim(regionBlock(row)), '#') || strlength(strtrim(regionBlock(row))) < 2), row = row + 1; end
|
|
dataS = row;
|
|
while row <= length(regionBlock) && ~startsWith(strtrim(regionBlock(row)), '#') && strlength(strtrim(regionBlock(row))) > 1, row = row + 1; end
|
|
|
|
if dataS < row
|
|
numCols = length(sscanf(regionBlock(dataS), '%f'));
|
|
raw = sscanf(join(regionBlock(dataS:row-1), ' '), '%f');
|
|
if ~isempty(raw) && numCols >= 2
|
|
m = reshape(raw, numCols, [])';
|
|
if isempty(enAxis), enAxis = m(:,1); end
|
|
tempD(1:size(m,1), cCrv, cScn) = m(:,2);
|
|
end
|
|
end
|
|
end
|
|
else
|
|
row = 1;
|
|
while row <= length(regionBlock) && (startsWith(strtrim(regionBlock(row)), '#') || strlength(strtrim(regionBlock(row))) < 2), row = row + 1; end
|
|
dataS = row;
|
|
while row <= length(regionBlock) && ~startsWith(strtrim(regionBlock(row)), '#') && strlength(strtrim(regionBlock(row))) > 1, row = row + 1; end
|
|
|
|
if dataS < row
|
|
numCols = length(sscanf(regionBlock(dataS), '%f'));
|
|
raw = sscanf(join(regionBlock(dataS:row-1), ' '), '%f');
|
|
if ~isempty(raw) && numCols >= 2
|
|
m = reshape(raw, numCols, [])';
|
|
enAxis = m(:,1);
|
|
tempD(1:size(m,1), 1, 1) = m(:,2);
|
|
end
|
|
end
|
|
end
|
|
|
|
% 3. Map Dimensions
|
|
if ~isempty(tempD)
|
|
image2D = squeeze(mean(tempD, 2, 'omitnan'));
|
|
if isvector(image2D), image2D = image2D(:); end
|
|
else
|
|
image2D = [];
|
|
end
|
|
|
|
regionEntry.image = {image2D};
|
|
regionEntry.imageRoh = {image2D};
|
|
|
|
if ~isnan(regionEntry.xRayEkin_eV)
|
|
regionEntry.xRayEkinRange_eV = [regionEntry.xRayEkin_eV regionEntry.xRayEkin_eV];
|
|
end
|
|
|
|
if ~isempty(enAxis)
|
|
enAxis = reshape(enAxis, 1, []);
|
|
regionEntry.scientaEkinRange_eV = [enAxis(1) enAxis(end)];
|
|
regionEntry.scientaEkinStep_eV = enAxis(2) - enAxis(1);
|
|
regionEntry.scientaEkin_eV = {enAxis};
|
|
regionEntry.scientaEkinRoh_eV = {enAxis};
|
|
end
|
|
|
|
regionEntry.scientaRegionIterations = size(tempD, 3);
|
|
|
|
if isempty(ProcessedXPS)
|
|
ProcessedXPS = regionEntry;
|
|
else
|
|
ProcessedXPS = [ProcessedXPS; regionEntry]; %#ok<AGROW>
|
|
end
|
|
end
|
|
catch ME
|
|
fprintf('Warning: Failed to parse %s (%s)\n', current_file_info.name, ME.message);
|
|
if isempty(ProcessedXPS), ProcessedXPS = current_file_info; else, ProcessedXPS = [ProcessedXPS; current_file_info]; end %#ok<AGROW>
|
|
end
|
|
end
|
|
end
|
|
|
|
TmpFileList{1} = ProcessedXPS;
|
|
end
|
|
|
|
disp('*************************************************')
|
|
disp('* Voila: Done reading & writing XPS data! *')
|
|
disp('*************************************************')
|
|
%% Read and write NEXAFS meta data
|
|
%
|
|
% The h5 file is much nicer organised, so we do not need string operations
|
|
% to select individual meta data. The data can be directly addressed and
|
|
% written into the list structure:
|
|
|
|
% if datenum(filedate_first) >= datenum('31-Dec-2017 23:59:00') %('31-Dec-2019 23:59:00')
|
|
for ii = 1:size(TmpFileList{2},1) % do this for each list element
|
|
|
|
% Get OS Dates
|
|
tmpCurrentFile = fullfile(TmpFileList{2}(ii).folder, TmpFileList{2}(ii).name);
|
|
[cDateOS, ~] = getOSDates(tmpCurrentFile);
|
|
TmpFileList{2}(ii).creationDate = datevec(cDateOS);
|
|
|
|
|
|
try
|
|
% read data
|
|
%if datenum(filedate_first) <= datenum('21-Nov-2020 00:01:00') % Date not exaclty certain.
|
|
try
|
|
TmpImage= h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/spectrum');
|
|
catch
|
|
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
|
|
TmpImage = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/detector/data'); % pre-2020 (?) h5 structure
|
|
end
|
|
end
|
|
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
|
|
TmpXrayEkin(TmpXrayEkin==0)=[];
|
|
catch
|
|
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
|
|
end
|
|
|
|
|
|
if ~isempty(TmpXrayEkin)
|
|
TmpFileList{2}(ii).xRayEkinRange_eV = [TmpXrayEkin(1) TmpXrayEkin(end)];
|
|
end
|
|
|
|
% Make unique
|
|
[TmpFileList{2}(ii).xRayEkin_eV, ind] = unique(TmpXrayEkin);
|
|
TmpFileList{2}(ii).image = TmpImage(:,ind);
|
|
TmpFileList{2}(ii).image = TmpImage(:,ind);
|
|
% NEXAFS pass energy
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/PassEnergy'); % in eV
|
|
TmpFileList{2}(ii).scientaPassEnergy_eV = str2double(tmp(1,:));
|
|
catch
|
|
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
|
|
|
|
if tmp(1) == 1
|
|
TmpFileList{2}(ii).scientaPassEnergy_eV = 20;
|
|
elseif tmp(1) == 2
|
|
TmpFileList{2}(ii).scientaPassEnergy_eV = 50;
|
|
elseif tmp(1) == 3
|
|
TmpFileList{2}(ii).scientaPassEnergy_eV = 100;
|
|
elseif tmp(1) == 4
|
|
TmpFileList{2}(ii).scientaPassEnergy_eV = 200;
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
% NEXAFS dwell time
|
|
% if datenum(filedate_first) >= datenum('1-Feb-2020 23:59:00')
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/ScientaStepTime')*1000; % in ms
|
|
|
|
catch
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/ScientaStepTime')*1000; % in ms
|
|
catch
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/StepTime')*1000; % in ms
|
|
catch
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/StepTime')*1000; % in ms
|
|
catch
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/ScientaStepTime')*1000;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
TmpFileList{2}(ii).scientaDwellTime_ms = tmp(1);
|
|
% NEXAFS region Name
|
|
if TmpFileList{2}(ii).xRayEkinRange_eV(:,1) <= 530 && TmpFileList{2}(ii).xRayEkinRange_eV(:,2) >= 530
|
|
TmpFileList{2}(ii).regionName = 'O K-edge NEXAFS';
|
|
else
|
|
TmpFileList{2}(ii).regionName = 'NEXAFS';
|
|
end
|
|
|
|
% Acquisition mode
|
|
try
|
|
TmpFileList{2}(ii).scientaAcquisitionMode = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/AcquisitionMode');
|
|
catch
|
|
try
|
|
TmpFileList{2}(ii).scientaAcquisitionMode = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/AcquisitionMode');
|
|
catch
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/AcquireMode');
|
|
if tmp(1) == 0
|
|
TmpFileList{2}(ii).scientaAcquisitionMode = "Swept";
|
|
elseif tmp(1) == 1
|
|
TmpFileList{2}(ii).scientaAcquisitionMode = "Fixed";
|
|
end
|
|
clear tmp
|
|
end
|
|
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/snaps/ScientaCenterEnergy'); % in eV
|
|
catch
|
|
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
|
|
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))';
|
|
else
|
|
% NEXAFS kinetic energy window - scan mode
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/ScientaLowEnergy'); % in eV
|
|
tmp2 = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/ScientaHighEnergy');
|
|
catch
|
|
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
|
|
end
|
|
TmpFileList{2}(ii).scientaEkinRange_eV = [tmp(1) tmp2(1)];
|
|
clearvars tmp
|
|
try
|
|
TmpFileList{2}(ii).scientaEkinStep_eV = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/StepSize');
|
|
catch
|
|
try
|
|
TmpFileList{2}(ii).scientaEkinStep_eV = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/StepSize');
|
|
catch
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/StepSize');
|
|
TmpFileList{2}(ii).scientaEkinStep_eV = abs(tmp(1));
|
|
end
|
|
end
|
|
TmpFileList{2}(ii).scientaEkin_eV = [TmpFileList{2}(ii).scientaEkinRange_eV(:,1):TmpFileList{2}(ii).scientaEkinStep_eV(1):TmpFileList{2}(ii).scientaEkinRange_eV(:,2)]';
|
|
end
|
|
|
|
clearvars tmp
|
|
% read I_0
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/i0');
|
|
catch
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/i0');
|
|
catch
|
|
end
|
|
end
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/RM2I0');
|
|
catch
|
|
end
|
|
|
|
try
|
|
TmpFileList{2}(ii).beamlineInt = abs(tmp(ind));
|
|
catch
|
|
TmpFileList{2}(ii).beamlineInt = NaN(size(ind,1),1);
|
|
end
|
|
|
|
|
|
|
|
% Lens Mode
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/LensMode');
|
|
TmpFileList{2}(ii).scientaLensMode = tmp;
|
|
catch
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/LensMode');
|
|
TmpFileList{2}(ii).scientaLensMode = tmp{1};
|
|
catch
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/LensMode');
|
|
TmpFileList{2}(ii).scientaLensMode = num2str(tmp(1));
|
|
end
|
|
end
|
|
|
|
clearvars tmp
|
|
try
|
|
TmpFileList{2}(ii).scientaRegionIterations = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan_1/snaps/NumIterations');
|
|
catch
|
|
try
|
|
TmpFileList{2}(ii).scientaRegionIterations = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/scan 1/attrs/NumIterations');
|
|
catch
|
|
try
|
|
tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/NumIterations');
|
|
TmpFileList{2}(ii).scientaRegionIterations = double(tmp(1));
|
|
catch
|
|
TmpFileList{2}(ii).scientaRegionIterations = 1;
|
|
end
|
|
end
|
|
end
|
|
TmpFileList{2}(ii).scientaSequenceIterations = 1;
|
|
|
|
catch
|
|
warning('Problem with a NEXAFS data file in NEXAFS list')
|
|
disp('Problem with a NEXAFS data file in NEXAFS list')
|
|
fprintf('Number %.0f: %s \n',ii,[TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name])
|
|
fprintf('Number %.0f: %s \n',ii,datestr(TmpFileList{2}(ii).datenum))
|
|
end
|
|
|
|
TmpFileList{2}(ii).imageRoh = TmpFileList{2}(ii).image;
|
|
% TmpFileList{2}(ii).spectrum_countsPerSecondRoh = TmpFileList{2}(ii).spectrum_countsPerSecond;
|
|
% log
|
|
TmpFileList{2}(ii).importDate = datestr(now);
|
|
try
|
|
TmpFileList{2}(ii).analysisDir = file.mFile;
|
|
catch
|
|
TmpFileList{2}(ii).analysisDir = pwd;
|
|
end
|
|
end
|
|
|
|
% else
|
|
% for ii = 1:size(TmpFileList{2},1) % do this for each list element
|
|
% try
|
|
% % NEXAFS pass energy
|
|
% tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/PassEnergy'); % in eV
|
|
% % TODO: Confirm that this assignment is correct. (The number refer to the list number in epics software)
|
|
% if tmp(1) == 1
|
|
% TmpFileList{2}(ii).passEnergy_eV = {[20]};
|
|
% elseif tmp(1) == 2
|
|
% TmpFileList{2}(ii).passEnergy_eV = {[50]};
|
|
% elseif tmp(1) == 3
|
|
% TmpFileList{2}(ii).passEnergy_eV = {[100]};
|
|
% elseif tmp(1) == 4
|
|
% TmpFileList{2}(ii).passEnergy_eV = {[200]};
|
|
% end
|
|
%
|
|
% % NEXAFS dwell time
|
|
% if datenum(filedate_first) >= datenum('1-Feb-2020 23:59:00')
|
|
% tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/ScientaStepTime')*1000; % in ms
|
|
%
|
|
% else
|
|
% tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/StepTime')*1000; % in ms
|
|
% end
|
|
% TmpFileList{2}(ii).dwellTime_ms = tmp(1);
|
|
% % NEXAFS region Name
|
|
% TmpFileList{2}(ii).regionName = 'NEXAFS';
|
|
% % FIXME: Can we differentiate O K-edge, N K-edge, C K-edge based on the h5-file meta data?
|
|
%
|
|
% % NEXAFS kinetic energy window
|
|
% 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).kinRange_eV = [tmp(1) tmp2(1)];
|
|
%
|
|
% % Lens mode
|
|
% tmp = h5read([TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name],'/entry/instrument/NDAttributes/LensMode');
|
|
% TmpFileList{2}(ii).scientaLensMode = tmp{1};
|
|
% clearvars tmp
|
|
% catch
|
|
% warning('Problem with a NEXAFS data file in NEXAFS list')
|
|
% disp('Problem with a NEXAFS data file in NEXAFS list')
|
|
% fprintf('Number %.0f: %s \n',ii,[TmpFileList{2}(ii).folder filesep TmpFileList{2}(ii).name])
|
|
% fprintf('Number %.0f: %s \n',ii,datestr(TmpFileList{2}(ii).datenum))
|
|
% end
|
|
%
|
|
% end
|
|
% end
|
|
|
|
|
|
|
|
disp('*************************************************')
|
|
disp('* Voila: Done reading and writing NEXAFS data! *')
|
|
disp('*************************************************')
|
|
%% Create Picture List
|
|
%
|
|
% The file structure needs same number of fields as SES and NEXAFS data.
|
|
% Set them to NaN (Not-a-Number).
|
|
|
|
|
|
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);
|
|
TmpFileList{3}(ii).creationDate = datevec(cDateOS);
|
|
end
|
|
|
|
|
|
%% Merge lists into one
|
|
%
|
|
|
|
%% Merge lists into one
|
|
ListFiles = table();
|
|
for i = 1:length(TmpFileList)
|
|
if ~isempty(TmpFileList{i})
|
|
|
|
% Convert struct to table
|
|
tmpTable = struct2table(TmpFileList{i}, 'AsArray', true);
|
|
|
|
% Ensure the 'name' column is a cell-string for consistency
|
|
if iscell(tmpTable.name) == 0
|
|
tmpTable.name = cellstr(tmpTable.name);
|
|
end
|
|
|
|
% Merge using helper
|
|
if isempty(ListFiles)
|
|
ListFiles = tmpTable;
|
|
else
|
|
ListFiles = outerjoinRowToTable(ListFiles, tmpTable);
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
% Remove rows where the 'image' column is empty
|
|
% not sure why this code is here, probably a workaround for earlier errors.
|
|
|
|
% Identify rows where the 'image' column is empty
|
|
tmpEmptyImgLogic = cellfun(@isempty, ListFiles.image);
|
|
% Extract the names of those files for the warning
|
|
tmpDeletedNames = ListFiles.name(tmpEmptyImgLogic);
|
|
% If any were found, display the warning
|
|
if ~isempty(tmpDeletedNames)
|
|
warning('The following files were removed because they contain no image data:');
|
|
fprintf(' - %s\n', tmpDeletedNames{:}); % Vectorized print of all names
|
|
end
|
|
% Actually remove the rows from the table
|
|
ListFiles(tmpEmptyImgLogic, :) = [];
|
|
clearvars -except ListFiles server dataPath* p
|
|
|
|
%% Sort by date
|
|
|
|
% sort the table based on the time (6th column, datenum)
|
|
ListFiles = sortrows(ListFiles,6);
|
|
% Then, remove un-needed columns
|
|
ListFiles = removevars(ListFiles,{'bytes','isdir'});
|
|
|
|
disp('*************************************************')
|
|
disp('* Voila: Done creating data list! *')
|
|
disp('*************************************************')
|
|
%% Import LabView data
|
|
%
|
|
if matches(p.Results.importLabView,'on')
|
|
if isfolder(dataPath_LabView) % verify that this folder exists.
|
|
try
|
|
% Get list of all files in that folder
|
|
TmpFileList = dir([dataPath_LabView filesep '**' filesep '20*.dat']);
|
|
|
|
% import LabView data
|
|
LabView = LabView_importData(fullfile(TmpFileList(1).folder,TmpFileList(1).name)); % Overwrite LabView data in workspace, if this is the first import
|
|
if size(TmpFileList,1) > 1 % for additional data files, add these to the LabView data structure.
|
|
for ii = 2:size(TmpFileList,1)
|
|
LabView = LabView_importData(fullfile(TmpFileList(ii).folder,TmpFileList(ii).name),LabView);
|
|
end
|
|
end
|
|
catch
|
|
warning('Problem with LabView file')
|
|
disp('Problem reading LabView file')
|
|
end
|
|
|
|
try
|
|
% Get list of all files in that folder
|
|
TmpFileList = dir([dataPath_LabView filesep '**' filesep 'logfile*.txt']);
|
|
|
|
% import LabView data
|
|
LabView = LabView_importData(fullfile(TmpFileList(1).folder,TmpFileList(1).name)); % Overwrite LabView data in workspace, if this is the first import
|
|
if size(TmpFileList,1) > 1 % for additional data files, add these to the LabView data structure.
|
|
for ii = 2:size(TmpFileList,1)
|
|
LabView = LabView_importData(fullfile(TmpFileList(ii).folder,TmpFileList(ii).name),LabView);
|
|
end
|
|
end
|
|
catch
|
|
warning('Problem with LabView file')
|
|
disp('Problem reading LabView file')
|
|
end
|
|
end
|
|
disp('*************************************************')
|
|
disp('* Voila: Done reading LabView data! *')
|
|
disp('*************************************************')
|
|
|
|
clearvars TmpFileList
|
|
|
|
try
|
|
% combine data and/or rename
|
|
LabView = LabView_combine(LabView);
|
|
catch
|
|
warning('Problem with LabView file')
|
|
disp('Problem merging LabView files')
|
|
disp('Check number of columns for each file')
|
|
end
|
|
|
|
%% Extent list with LabView data
|
|
|
|
%Query time of each data file and get LabView log for that time.
|
|
|
|
for ii = 1:size(ListFiles,1)
|
|
|
|
try
|
|
|
|
% The time of the data and the logging of LabView are not
|
|
% necessarily occurring at precisely the same time. So, here define
|
|
% a time range (50 seconds) and get all
|
|
% data between these times.
|
|
tse1 = tsdata.event('Start',datenum(ListFiles.datenum(ii)),'Datenum');
|
|
tse2 = tsdata.event('End',datenum(ListFiles.datenum(ii)+seconds(50)),'Datenum');
|
|
tmp = gettsbetweenevents(LabView.merge,tse1,tse2);
|
|
|
|
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)
|
|
% 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
|
|
% vapour pressure over ice:
|
|
if ListFiles.cellPressure_mbar(ii) >= 0
|
|
% Marti, J., and K. Mauersberger, Geophys. Res.Lett., 20, 363?366, 1993.
|
|
% gives parameterisation from 169 - 273K in Pa. (log10(p)= A1/T+B)
|
|
A_1 = -2663.5;
|
|
B_1 = 12.537;
|
|
ListFiles.iceTemp_dC(ii) = A_1/(log10(ListFiles.cellPressure_mbar(ii)*100)-B_1)-273.15;
|
|
else
|
|
ListFiles.iceTemp_dC(ii) = NaN;
|
|
end
|
|
|
|
if datenum(ListFiles.datenum(ii)) >= datenum('04-Mar-2019 13:30:00') % import additional data, logged after 03/2019
|
|
ListFiles.smplX_mm(ii) = tmp.Data(1,10);
|
|
ListFiles.smplY_mm(ii) = tmp.Data(1,11);
|
|
ListFiles.smplZ_mm(ii) = tmp.Data(1,12);
|
|
ListFiles.sealingTemp(ii) = tmp.Data(1,13);
|
|
else % if these sub-set of data is not logged, set to NaN
|
|
ListFiles.smplX_mm(ii) = NaN;
|
|
ListFiles.smplY_mm(ii) = NaN;
|
|
ListFiles.smplZ_mm(ii) = NaN;
|
|
ListFiles.sealingTemp(ii) = NaN;
|
|
end
|
|
else % if there are no LabView data stored, set all entries to NaN
|
|
ListFiles.sampleTemp_dC(ii) = NaN;
|
|
ListFiles.iceTemp_dC(ii) = NaN;
|
|
ListFiles.cellPressure_mbar(ii) = NaN;
|
|
ListFiles.smplX_mm(ii) = NaN;
|
|
ListFiles.smplY_mm(ii) = NaN;
|
|
ListFiles.smplZ_mm(ii) = NaN;
|
|
ListFiles.sealingTemp(ii) = NaN;
|
|
end
|
|
clearvars tmp
|
|
catch
|
|
warning('Problem merging LabView data into data table')
|
|
disp('Problem merging LabView data into data table')
|
|
%fprintf('Number %.0f: %s \n',ii,[ListFiles.folder(ii) filesep ListFiles.name(ii)])
|
|
%fprintf('Number %.0f: %s \n',ii,datestr(ListFiles.datenum(ii)))
|
|
end
|
|
end
|
|
|
|
disp('*************************************************')
|
|
disp('* Voila: Done writing LabView data! *')
|
|
disp('*************************************************')
|
|
end
|
|
%% Make table nicer
|
|
%
|
|
|
|
|
|
|
|
|
|
if ~iscell(ListFiles.xRayEkin_eV)
|
|
% Use num2cell to wrap the existing numbers in cells.
|
|
ListFiles.xRayEkin_eV = num2cell(ListFiles.xRayEkin_eV);
|
|
end
|
|
if ~iscell(ListFiles.scientaEkin_eV)
|
|
for ii = 1:size(ListFiles,1)
|
|
tmp{ii,:} = ListFiles.scientaEkin_eV(ii,:);
|
|
end
|
|
ListFiles.scientaEkin_eV = tmp;
|
|
end
|
|
|
|
|
|
|
|
% -------------------------------------------------------------------------
|
|
% MAP DATES & CLEANUP
|
|
% -------------------------------------------------------------------------
|
|
|
|
% Delete the raw OS fields and the temporary uppercase fields to avoid duplicates
|
|
ListFiles = removevars(ListFiles, {'date','datenum'});
|
|
|
|
% Move Dates upfront
|
|
ListFiles = movevars(ListFiles,'lastModifiedDatestr','After','creationDate');
|
|
ListFiles = movevars(ListFiles,'lastModifiedDatenum','After','creationDate');
|
|
% -------------------------------------------------------------------------
|
|
|
|
% Copy Ekin Data
|
|
existColumn = strcmp('scientaEkinRoh_eV',ListFiles.Properties.VariableNames);
|
|
if any(existColumn)
|
|
ListFiles = removevars(ListFiles,'scientaEkinRoh_eV');
|
|
end
|
|
existColumn = strcmp('scientaEkinRoh_eV',ListFiles.Properties.VariableNames);
|
|
if ~any(existColumn)
|
|
% save current ekin data before they are modified
|
|
ListFiles.scientaEkinRoh_eV= ListFiles.scientaEkin_eV;
|
|
end
|
|
if ~iscell(ListFiles.scientaEkinRoh_eV)
|
|
for ii = 1:size(ListFiles,1)
|
|
tmp{ii,:} = ListFiles.scientaEkinRoh_eV(ii,:);
|
|
end
|
|
ListFiles.scientaEkinRoh_eV = tmp;
|
|
end
|
|
|
|
|
|
% add the imported data to previous list (if passed to function upon call)
|
|
if istable(p.Results.DataTable) % do this only if previous list was input to function call
|
|
|
|
% find table variables that are missing in ListFiles
|
|
existColumn = strcmp('sample',p.Results.DataTable.Properties.VariableNames);
|
|
if ~any(existColumn)
|
|
p.Results.DataTable.sample = cellstr(repmat(' ',size(p.Results.DataTable,1),1));
|
|
end
|
|
tmpMissing = ~ismember(p.Results.DataTable.Properties.VariableNames,ListFiles.Properties.VariableNames);
|
|
% find all table variables that are cells
|
|
tmpCell = cellfun(@(x) iscell(p.Results.DataTable.(x)), p.Results.DataTable.Properties.VariableNames);
|
|
% find all table variables that are numbers
|
|
tmpNumber = cellfun(@(x) isnumeric(p.Results.DataTable.(x)), p.Results.DataTable.Properties.VariableNames);
|
|
% table variabes that hold cells and are missing
|
|
tmpCellVariables = p.Results.DataTable.Properties.VariableNames(tmpMissing & tmpCell);
|
|
% table variables that hold numbers and are missing
|
|
tmpNumberVariables = p.Results.DataTable.Properties.VariableNames(tmpMissing & tmpNumber);
|
|
% add empty table variables for missing cell and for number variables
|
|
if ~isempty(tmpCellVariables)
|
|
for i = 1: size(p.Results.DataTable(:,tmpMissing & tmpCell),2)
|
|
ListFiles = addvars(ListFiles,cell(size(ListFiles,1),1),'NewVariableNames',tmpCellVariables{i});
|
|
end
|
|
end
|
|
if ~isempty(tmpNumberVariables)
|
|
for i = 1: size(p.Results.DataTable(:,tmpMissing & tmpNumber),2)
|
|
ListFiles = addvars(ListFiles,nan(size(ListFiles,1),1),'NewVariableNames',tmpNumberVariables{i});
|
|
end
|
|
end
|
|
|
|
% % % sort the tables, if they have not the same order
|
|
% tmpCompareTables = cellfun(@isequal, ListFiles.Properties.VariableNames', p.Results.DataTable.Properties.VariableNames');
|
|
%
|
|
% if ~all(tmpCompareTables)
|
|
% indexDifferenzes = find(ismember(p.Results.DataTable.Properties.VariableNames',ListFiles.Properties.VariableNames(~tmpCompareTables)','rows'));
|
|
% nameListFiles =ListFiles.Properties.VariableNames(~tmpCompareTables);
|
|
%
|
|
% for ii = 1:size(indexDifferenzes,1)
|
|
% tmpIndx = find(ismember(p.Results.DataTable.Properties.VariableNames',nameListFiles{ii},'rows'));
|
|
% tmpPreviousVarName = p.Results.DataTable.Properties.VariableNames{tmpIndx-1};
|
|
% ListFiles = movevars(ListFiles,nameListFiles{ii},'After',tmpPreviousVarName);
|
|
% fprintf("%s comes after %s \n", nameListFiles{ii},tmpPreviousVarName)
|
|
% end
|
|
%
|
|
% end
|
|
|
|
|
|
|
|
% merge tables (append imported data to previous table)
|
|
% ListFiles = [p.Results.DataTable; ListFiles];
|
|
ListFiles = outerjoinRowToTable(p.Results.DataTable, ListFiles);
|
|
% Then, we sort the table based on the time (datenum)
|
|
ListFiles = sortrows(ListFiles,'lastModifiedDatenum');
|
|
end
|
|
|
|
%% Export
|
|
|
|
ExportList = removevars(ListFiles, {'image', 'scientaEkin_eV', 'beamlineInt', 'imageRoh', 'scientaEkinRoh_eV', 'xRayEkin_eV'});
|
|
% TODO check windows formatting
|
|
try
|
|
% write a ;-separated list for export to excel.
|
|
writetable(ExportList,fullfile(dataPath,[datestr(ExportList.lastModifiedDatenum(1),'mmmYYYY') '_DataList.csv']),'Delimiter',',');
|
|
|
|
if ispc
|
|
% and open in Excel; works only on Windows Machines
|
|
winopen(fullfile(dataPath,[datestr(ExportList.lastModifiedDatenum(1),'mmmYYYY') '_DataList.csv']));
|
|
end
|
|
catch
|
|
warning('Problem writing Excel File (*.csv). Make sure it is not open in Excel')
|
|
end
|
|
|
|
|
|
|
|
|
|
disp('*************************************************')
|
|
disp('* Voila: nappFileList.m finished ! *')
|
|
disp('*************************************************')
|
|
|
|
|
|
end
|
|
|
|
function LabView = LabView_importData(fullFileName,varargin)
|
|
%
|
|
% LabView_importData reads data log ASCII files stored by LabView into
|
|
% a Matlab time series.
|
|
%
|
|
% LabView = LabView_importData(A) imports file A
|
|
% and stores its content as time series in a structure called "LabView".
|
|
%
|
|
% LabView = LabView_importData(A,LabView) imports file A
|
|
% and appends its content as time series to the LabView structure.
|
|
%
|
|
%
|
|
% Example
|
|
% LabView = LabView_importData('exmpl.dat',LabView)
|
|
%
|
|
% Input
|
|
% * Napp_chamber_file: Complete and absolute path to the Napp chamber log file.
|
|
% * LabView: name of output structure. Always call this'LabView'
|
|
%
|
|
% Output
|
|
% * LabView: time series, the name starting with "ts"
|
|
% followed by date and time the recording started.
|
|
%
|
|
% See also LABVIEW_COMBINE, LABVIEW_PLOT, LABVIEW_CUTDATA
|
|
%
|
|
%% Data Structure
|
|
%
|
|
% Data structure before 4.3.2019
|
|
% col 1: Date
|
|
% col 2: Time
|
|
% col 3: Reservoir "Pressure 1" - Unit "1 water" - mbar -> ts.Data:2
|
|
% col 4: Reservoir "Pressure 2" - Unit "2 other" - mbar -> ts.Data:3
|
|
% col 5: Baratron 1 - cell - 1mbar max - mbar -> ts.Data:4
|
|
% 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 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 14: Flow He(cryoholder) - l/min -> ts.Data:7
|
|
% col 15: none
|
|
% col 16: none
|
|
%
|
|
% Data structure after 4.3.2019
|
|
% col 1: Date
|
|
% col 2: Time
|
|
% col 3: Reservoir "Pressure 1" - Unit "1 water" - mbar -> ts.Data:2
|
|
% col 4: Reservoir "Pressure 2" - Unit "2 other" - mbar -> ts.Data:3
|
|
% col 5: Baratron 1 - cell - 1mbar max - mbar -> ts.Data:4
|
|
% 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 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 13: Flow He(cryoholder) - l/min -> ts.Data:7
|
|
% col 14: Flow gas 1
|
|
% col 15: Pressure chamber - mbar
|
|
% col 16: x-position of cryoholder - mm -> ts.Data:10
|
|
% 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
|
|
|
|
|
|
%%
|
|
|
|
% Create an InputParser object
|
|
p = inputParser;
|
|
% Add inputs to the scheme.
|
|
defaultAppendToStructure = 0;
|
|
addRequired(p,'fullFileName',@isfile);
|
|
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',...
|
|
'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{12} ...
|
|
data{13} data{14} data{6} data{7}] , ...
|
|
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
|
|
|
|
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
|
|
% understand 1 digit month and hours.
|
|
%
|
|
%
|
|
% Split the date in column 1 into year,month,day
|
|
newDateSplit = cellfun(@(x) split(char(x),'/'),data{1},'UniformOutput', false);
|
|
% Split the time in column 2 into hours, minutes, seconds
|
|
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} ...
|
|
data{13} data{14} data{7} data{8} data{17} data{18} data{19} data{21}], ...
|
|
customDateStr);
|
|
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',...
|
|
'HeaderLines',1);
|
|
% data file has 2 text columns, followed by 18 numeric columns (THE AM
|
|
% 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, ...
|
|
'%s %s %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f',...
|
|
'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
|
|
end
|
|
end
|
|
|
|
function data_structure = LabView_combine(data_structure)
|
|
% LabView_combine can be used to merge time series into one.
|
|
%
|
|
% LabView = LabView_combine(LabView) merges all timeseries in the structure
|
|
% LabView into the new timeseries "merge"
|
|
% and adds this timeseries to the LabView structure
|
|
%
|
|
%
|
|
% Input
|
|
% * data_structure: name of the structure holding the time series
|
|
%
|
|
% Output
|
|
% * data_structure.merge: New time series combining all ts in this structure
|
|
%
|
|
% See also LABVIEW_IMPORTDATA, LABVIEW_PLOT
|
|
|
|
% delete previous merged data
|
|
if isfield(data_structure,'merge')
|
|
data_structure = rmfield(data_structure, 'merge');
|
|
end
|
|
|
|
% sort them chronologically
|
|
data_structure = orderfields(data_structure);
|
|
|
|
% query list of time series
|
|
list_ts = fieldnames(data_structure);
|
|
|
|
% and merge all of them into one time series
|
|
for ii = 1:length(list_ts)
|
|
if ii == 1
|
|
data_structure.merge = data_structure.(list_ts{1});
|
|
else
|
|
data_structure.merge = append(data_structure.merge,data_structure.(list_ts{ii}));
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
function splitH5Scans(infile)
|
|
% splitH5Scans - Split an HDF5 file containing multiple scans into separate files
|
|
%
|
|
% This function takes an HDF5 file with multiple scan groups (scan_1, scan_2, etc.)
|
|
% and creates separate files for each scan. Each output file contains only one
|
|
% scan (renamed to scan_1) plus the logs group. The original file is deleted after
|
|
% successful splitting.
|
|
%
|
|
% SYNTAX:
|
|
% splitH5Scans(infile)
|
|
%
|
|
% INPUT:
|
|
% infile - String containing the full path to the input HDF5 file
|
|
% Example: '0002_20251119_145823_O_Kedge_Nov25.h5'
|
|
%
|
|
% OUTPUT:
|
|
% Creates new HDF5 files in the same directory as the input file.
|
|
% Each file is named: [original_name]_scan_N.h5
|
|
% Example outputs:
|
|
% 0002_20251119_145823_O_Kedge_Nov25_scan_1.h5
|
|
% 0002_20251119_145823_O_Kedge_Nov25_scan_2.h5
|
|
%
|
|
% REQUIREMENTS:
|
|
% - MATLAB with HDF5 support (R2011a or later)
|
|
% - Input file must have more than 3 root-level groups to be processed
|
|
%
|
|
% EXAMPLE:
|
|
% splitH5Scans('0002_20251119_145823_O_Kedge_Nov25.h5');
|
|
%
|
|
% Author: Perplexity and Thorsten
|
|
% Date: November 2025
|
|
|
|
% Read the structure of the HDF5 file to see what groups it contains
|
|
% h5info returns information about groups, datasets, and attributes
|
|
fileinfo = h5info(infile);
|
|
|
|
% Get the original file's modification date before we start
|
|
% This will be copied to all new files to preserve the original timestamp
|
|
orig_file_info = dir(infile);
|
|
orig_mod_datetime = orig_file_info.date; % Get as datetime object
|
|
|
|
|
|
% Extract the names of all groups at the root level of the file
|
|
% Example: {'/scan_1', '/scan_2', '/logs', '/__DATA_TYPES__'}
|
|
groupnames = {fileinfo.Groups.Name};
|
|
|
|
% Only process files that have more than 3 groups at the root level
|
|
% This ensures we only split files with multiple scans
|
|
if numel(groupnames) > 3
|
|
|
|
% Find which groups are scan groups (contain 'scan_' in their name)
|
|
% This returns a logical array: true for scan groups, false otherwise
|
|
is_scan = contains(groupnames, 'scan_');
|
|
|
|
% Extract only the scan group names
|
|
% Example: {'/scan_1', '/scan_2'}
|
|
scan_names = groupnames(is_scan);
|
|
|
|
% Split the input filename into its components
|
|
% filepath: directory where file is located (e.g., 'C:\Data\')
|
|
% base: filename without extension (e.g., '0002_20251119_145823_O_Kedge_Nov25')
|
|
% ext: file extension (e.g., '.h5')
|
|
[filepath, base, ext] = fileparts(infile);
|
|
|
|
% If no directory path, use current directory
|
|
if isempty(filepath)
|
|
filepath = '.';
|
|
end
|
|
|
|
% Extract the file number prefix and rest of the name using pattern matching
|
|
% regexp splits '0002_20251119_145823_O_Kedge_Nov25' into:
|
|
% prefix = '0002_'
|
|
% rest = '20251119_145823_O_Kedge_Nov25'
|
|
pieces = regexp(base, '(\d+_)(.*)', 'tokens', 'once');
|
|
prefix = pieces{1}; % The number at the start (e.g., '0002_')
|
|
rest = pieces{2}; % Everything after the number
|
|
|
|
% Loop through each scan group and create a separate file for it
|
|
for k = 1:numel(scan_names)
|
|
|
|
% Get the full path of the current scan group
|
|
% Example: '/scan_1' or '/scan_2'
|
|
scan_group_full = scan_names{k};
|
|
|
|
% Remove the leading '/' from the scan name for use in filename
|
|
% Example: '/scan_2' becomes 'scan_2'
|
|
scan_name_clean = regexprep(scan_group_full, '^/', '');
|
|
|
|
% Build the output filename in the same directory as input file
|
|
% Example: 'C:\Data\0002_20251119_145823_O_Kedge_Nov25_scan_2.h5'
|
|
outname = fullfile(filepath, sprintf('%s%s_%s%s', prefix, rest, scan_name_clean, ext));
|
|
|
|
% Copy the entire original file to create the new output file
|
|
% We'll modify this copy to keep only one scan
|
|
copyfile(infile, outname);
|
|
|
|
% Open the new file for reading and writing using low-level HDF5 functions
|
|
% fid is a file identifier used to reference this file
|
|
% 'H5F_ACC_RDWR' means read and write access
|
|
fid = H5F.open(outname, 'H5F_ACC_RDWR', 'H5P_DEFAULT');
|
|
|
|
% Delete all scan groups except the one we want to keep
|
|
% Loop through all scan groups in the original file
|
|
for j = 1:numel(scan_names)
|
|
other_scan = scan_names{j}; % Get the name of another scan group
|
|
|
|
% If this is not the scan we want to keep, delete it
|
|
if j ~= k
|
|
try
|
|
% H5L.delete removes the link (reference) to the group
|
|
% This makes the group inaccessible but doesn't free space
|
|
H5L.delete(fid, other_scan, 'H5P_DEFAULT');
|
|
catch
|
|
% If deletion fails (e.g., group doesn't exist), continue
|
|
% The catch block prevents the script from stopping on errors
|
|
end
|
|
end
|
|
end
|
|
|
|
% Rename the kept scan group to '/scan_1' for consistency
|
|
% This way every output file has a group called '/scan_1'
|
|
if ~strcmp(scan_group_full, '/scan_1')
|
|
% H5L.move renames/moves a group within the file
|
|
% Parameters: source_file, old_path, dest_file, new_path, options
|
|
H5L.move(fid, scan_group_full, fid, '/scan_1', 'H5P_DEFAULT', 'H5P_DEFAULT');
|
|
end
|
|
|
|
% Close the HDF5 file to save all changes
|
|
H5F.close(fid);
|
|
|
|
% Copy the modification date from the original file to the new file
|
|
% Convert MATLAB datenum to timestamp format: YYYYMMDDhhmm.ss
|
|
timestamp_str = string(datetime(orig_mod_datetime), 'yyyyMMddHHmm.ss');
|
|
if ismac
|
|
touch_cmd = sprintf('touch -t %s "%s"', timestamp_str, outname);
|
|
system(touch_cmd);
|
|
end
|
|
|
|
% Print confirmation message showing which file was created
|
|
% fprintf('Created: %s\n', outname);
|
|
end
|
|
|
|
% After all split files are successfully created, delete the original file
|
|
delete(infile);
|
|
% fprintf('Deleted original file: %s\n', infile);
|
|
|
|
else
|
|
% If the file has 3 or fewer groups, it doesn't need splitting
|
|
% disp('File has three or fewer groups. No processing required.');
|
|
end
|
|
|
|
end
|
|
|
|
|
|
function [creationDateOS, lastModifiedDate] = getOSDates(fullPath)
|
|
% GETOSDATES Extracts the true OS-level creation and modification dates
|
|
|
|
% Get standard modification date
|
|
d = dir(fullPath);
|
|
if isempty(d)
|
|
creationDateOS = '';
|
|
lastModifiedDate = '';
|
|
return;
|
|
end
|
|
lastModifiedDate = d.date;
|
|
|
|
% Get Creation Date based on Operating System
|
|
if ispc
|
|
% Windows: Use .NET framework for accurate Creation Time
|
|
try
|
|
fileObj = System.IO.FileInfo(fullPath);
|
|
creationDateOS = char(fileObj.CreationTime.ToString('dd-MMM-yyyy HH:mm:ss'));
|
|
catch
|
|
creationDateOS = d.date; % Fallback
|
|
end
|
|
elseif ismac || isunix
|
|
% macOS/Linux: Use the stat command to get the "Birth" time
|
|
[status, cmdOut] = system(['stat -f "%SB" -t "%d-%b-%Y %H:%M:%S" "', fullPath, '"']);
|
|
if status == 0
|
|
creationDateOS = strtrim(cmdOut);
|
|
else
|
|
creationDateOS = d.date; % Fallback
|
|
end
|
|
else
|
|
creationDateOS = d.date; % Fallback for unknown OS
|
|
end
|
|
end |