diff --git a/.gitignore b/.gitignore index 3d3243a..cfa822b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ empty_log_process_temp.py tests/**/bids/ tests/test_main_functionality/data/projects/test-project/sub-100 +tests/data # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/docs/bids_convert_and_upload.md b/docs/bids_convert_and_upload.md index bb8d903..901ae52 100644 --- a/docs/bids_convert_and_upload.md +++ b/docs/bids_convert_and_upload.md @@ -91,15 +91,15 @@ If otherFilesUsed=True in project config file: 1. Behavioral files are copied via `_copy_behavioral_files()`. - - Validates required files against TOML config (`OtherFilesInfo`). In this config we add the the extensions of the expected other files. For example, in our testproject we use EyeList 1000 Plus eye tracker which generates .edf and .csv files. So we add these extensions as required other files. We also have mandatory labnotebook and participant info files in .tsv format. - - Renames files to include sub-XXX_ses-YYY_ prefix if missing. - - Deletes the other files in the project_other directory that are not listed in `OtherFilesInfo` in the project config file. It doesn"t delete from the source directory, only from out BIDS dataset. + - Validates required files against TOML config (`OtherFilesInfo`). In this config we add the the extensions of the expected other files. For example, in our testproject we use EyeList 1000 Plus eye tracker which generates .edf and .csv files. So we add these extensions as required other files. We also typically have mandatory labnotebook and participant info files in .tsv format. + - The `"*.src"="beh/{prefix}_target"` allows users to easily add BIDS-compatible custom data from the experiments. Note that `json` sidecars are not automatically generated yet. + 2. Experimental files are copied via `_copy_experiment_files().` - - Gathers files from the experiment folder. + - Gathers files from the `/experiment/` folder. - Copies into BIDS `misc/` directory i.e. `/misc/` - - Compresses into experiment.tar.gz. + - Compresses into `experiment.tar.gz`. - Removes the uncompressed folder. There is a flag in the `lslautobids run` command called `--redo_other_pc` which when specified, forces overwriting of existing other and experiment files in the BIDS dataset. This is useful if there are updates or corrections to the other/behavioral data that need to be reflected in the BIDS dataset. @@ -121,7 +121,7 @@ This produces a clean, memory-efficient Raw object ready for BIDS conversion. #### BIDS Validation (`validate_bids()`) This function validates the generated BIDS files using the `bids-validator` package. It performs the following steps: - Walks through the BIDS directory. -- Skips irrelevant files: (`.xdf`, `.tar.gz`, behavioral files, hidden/system files.) +- Skips irrelevant files already ignored in `.bidsignore` (`misc` folder, some hidden files) - Uses `BIDSValidator` to validate relative paths. - If any file fails validation, logs an error and returns 0 ; Otherwise, logs success and returns 1. diff --git a/docs/data_organization.md b/docs/data_organization.md index 29072aa..34254de 100644 --- a/docs/data_organization.md +++ b/docs/data_organization.md @@ -54,7 +54,7 @@ Filename Convention for the raw data files : ## Project Other Folder -This folder contains the experimental and behavioral files which we also store in the dataverse. The folder structure is should as follows: +This folder contains the experimental and behavioral files which we also store in the dataverse. The folder structure has to be as follows: projectname/ └── experiment @@ -65,6 +65,7 @@ This folder contains the experimental and behavioral files which we also store i └── beh └── behavioral_files((lab notebook, CSV, EDF file, etc)) +It is possible to modify the `src=target` syntax to "skip" folders via `..` (maybe we should simply allow `{prefix}` in the src as well => not yet implemented) - **projectname** - any descriptive name for the project - **experiment** - contains the experimental files for the project. Eg: showOther.m, showOther.py - **data** - contains the behavioral files for the corresponding subject. Eg: experimentalParameters.csv, eyetrackingdata.edf, results.tsv. @@ -91,8 +92,9 @@ This folder contains the converted BIDS data files and other files we want to ve ......... └── beh └──behavioral files (other files) - └── misc + └── misc (added to .bidsignore) └── experimental files (This needs to stored in zip format) + └── labnotebook, subjectform etc. └── sourcedata └── raw xdf files └── dataset_description.json diff --git a/docs/developers_documentation.md b/docs/developers_documentation.md index 7ccca0f..fc0e2b7 100644 --- a/docs/developers_documentation.md +++ b/docs/developers_documentation.md @@ -270,9 +270,8 @@ If otherFilesUsed=True in project config file: 1. Behavioral files are copied via `_copy_behavioral_files()`. - - Validates required files against TOML config (`OtherFilesInfo`). In this config we add the the extensions of the expected other files. For example, in our testproject we use EyeList 1000 Plus eye tracker which generates .edf and .csv files. So we add these extensions as required other files. We also have mandatory labnotebook and participant info files in .tsv format. - - Renames files to include sub-XXX_ses-YYY_ prefix if missing. - - Deletes the other files in the project_other directory that are not listed in `OtherFilesInfo` in the project config file. It doesn"t delete from the source directory, only from out BIDS dataset. + - Validates required files against TOML config (`OtherFilesInfo`). In this config we add the the extensions of the expected other files. For example, in our testproject we use EyeList 1000 Plus eye tracker which generates .edf and .csv files. So we add these extensions as required other files. We also typically use a mandatory labnotebook and participant info files in .tsv format. Currently it is not possible to convert files in this step, but should maybe become possible for e.g. `EDF` files and `CSV=>TSV` files + - follows the src=target regexp syntax to copy files over 2. Experimental files are copied via `_copy_experiment_files().` @@ -300,7 +299,7 @@ This produces a clean, memory-efficient Raw object ready for BIDS conversion. #### 5. BIDS Validation (`validate_bids()`) This function validates the generated BIDS files using the `bids-validator` package. It performs the following steps: - Walks through the BIDS directory. -- Skips irrelevant files: (`.xdf`, `.tar.gz`, behavioral files, hidden/system files.) +- Skips irrelevant files: (`misc`-folder, hidden/system files.) - Uses `BIDSValidator` to validate relative paths. - If any file fails validation, logs an error and returns 0 ; Otherwise, logs success and returns 1. diff --git a/docs/tutorial.md b/docs/tutorial.md index ed239f1..f31731d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -103,7 +103,7 @@ In this example, we will see how to use the LSLAutoBIDS package to: otherFilesUsed = true [OtherFilesInfo] - expectedOtherFiles = [".edf", ".csv", "_labnotebook.tsv", "_participantform.tsv"] + expectedOtherFiles = ["*.edf"="misc/{prefix}_et.edf", "*.csv"="misc/{prefix}_beh.csv", "*_labnotebook.tsv"="misc/{prefix}_labnotebook.tsv", "*_participantform.tsv"="{prefix}_participantform.tsv"] ``` 2. Run the conversion and upload command to convert the `xdf` files to BIDS format and upload the data to the dataverse. ``` diff --git a/lslautobids/convert_to_bids_and_upload.py b/lslautobids/convert_to_bids_and_upload.py index 2da8b19..310dd2f 100644 --- a/lslautobids/convert_to_bids_and_upload.py +++ b/lslautobids/convert_to_bids_and_upload.py @@ -2,6 +2,7 @@ import os import shutil import sys +import re from pyxdf import match_streaminfos, resolve_streams from mnelab.io.xdf import read_raw_xdf @@ -92,101 +93,89 @@ def copy_source_files_to_bids(self,xdf_file,subject_id,session_id,other, logger) def _copy_behavioral_files(self, file_base, subject_id, session_id, logger): """ - Copy behavioral files to the BIDS structure. + Copy behavioral files to the BIDS structure based on regex patterns. + Iterates through patterns and matches files, copying them directly to target locations. Args: file_base (str): Base name of the file (without extension). subject_id (str): Subject ID. session_id (str): Session ID. + logger: Logger instance. """ + project_name = cli_args.project_name logger.info("Copying the behavioral files to BIDS...") + + # Get the TOML configuration + toml_path = os.path.join(project_root, cli_args.project_name, cli_args.project_name + '_config.toml') + data = read_toml_file(toml_path) + _expectedotherfiles = data["OtherFilesInfo"]["expectedOtherFiles"] + + if not isinstance(_expectedotherfiles, dict): + raise ValueError("expectedOtherFiles must be a dictionary with regex patterns. List format is no longer supported since v0.2.0 .") + # get the source path - behavioural_path = os.path.join(project_other_root,project_name,'data', subject_id,session_id,'beh') - # get the destination path - dest_dir = os.path.join(bids_root , project_name, subject_id , session_id , 'beh') - #check if the directory exists - os.makedirs(dest_dir, exist_ok=True) - - processed_files = [] + behavioural_path = os.path.join(project_other_root, project_name, 'data', subject_id, session_id, 'beh') + + if not os.path.exists(behavioural_path): + raise FileNotFoundError(f"Behavioral path does not exist: {behavioural_path} - did you forget to mount?") + return + # Extract the sub-xxx_ses-yyy part def extract_prefix(filename): parts = filename.split("_") sub = next((p for p in parts if p.startswith("sub-")), None) ses = next((p for p in parts if p.startswith("ses-")), None) if sub and ses: - return f"{sub}_{ses}_" + return f"{sub}_{ses}" return None prefix = extract_prefix(file_base) - - for file in os.listdir(behavioural_path): - # Skip non-files (like directories) - original_path = os.path.join(behavioural_path, file) - if not os.path.isfile(original_path): - continue - - if not file.startswith(prefix): - logger.info(f"Renaming {file} to include prefix {prefix}") - renamed_file = prefix + file - else: - renamed_file = file + processed_files = [] - processed_files.append(renamed_file) - dest_file = os.path.join(dest_dir, renamed_file) + # Get all files in source directory once + source_files = [f for f in os.listdir(behavioural_path) + if os.path.isfile(os.path.join(behavioural_path, f))] + # Iterate through patterns (not files) + for pattern, target_template in _expectedotherfiles.items(): + compiled_regex = re.compile(pattern) + + # Find matching files for this pattern + matched_files = [f for f in source_files if compiled_regex.match(f)] + + if not matched_files: + raise FileExistsError(f"No files matched pattern '{pattern}' in {behavioural_path}") + + if len(matched_files) > 1: + raise ValueError(f"Multiple files matched pattern '{pattern}': {matched_files}. Only one file per pattern is supported - manually intervention required") + + # Process the first matching file + file = matched_files[0] + original_path = os.path.join(behavioural_path, file) + + # Format the target path with prefix + target_path = target_template.format(prefix=prefix) + dest_file = os.path.join(bids_root, project_name, subject_id, session_id, target_path) + + # Ensure destination directory exists + os.makedirs(os.path.dirname(dest_file), exist_ok=True) + + # Track the relative path for checking + processed_files.append(target_path) + if cli_args.redo_other_pc: - logger.info(f"Copying (overwriting if needed) {file} to {dest_file}") + logger.info(f"Copying (overwriting) {file} to {target_path}") shutil.copy(original_path, dest_file) else: if os.path.exists(dest_file): - logger.info(f"Behavioural file {file} already exists in BIDS. Skipping.") + logger.info(f"Behavioural file {target_path} already exists in BIDS. Skipping.") else: - logger.info(f"Copying new file {file} to {dest_file}") + logger.info(f"Copying {file} to {target_path}") shutil.copy(original_path, dest_file) - - - - unnecessary_files = self._check_required_behavioral_files(processed_files, prefix, logger) - - # remove the unnecessary files - for file in unnecessary_files: - file_path = os.path.join(dest_dir, file) - if os.path.exists(file_path): - logger.info(f"Removing unnecessary file: {file_path}") - os.remove(file_path) - else: - logger.warning(f"File to remove does not exist: {file_path}") - - - - def _check_required_behavioral_files(self, files, prefix, logger): - """ - Check for required behavioral files after copying. - - Args: - files (list): List of copied file names. - prefix (str): Expected prefix (e.g., "sub-001_ses-002_"). - """ - logger.info("Checking for required behavioral files...") - - # Get the expected file names from the toml file - toml_path = os.path.join(project_root, cli_args.project_name, cli_args.project_name + '_config.toml') - data = read_toml_file(toml_path) - - required_files = data["OtherFilesInfo"]["expectedOtherFiles"] - - - for required_file in required_files: - if not any(f.startswith(prefix) and f.endswith(required_file) for f in files): - raise FileNotFoundError(f"Missing required behavioral file: {required_file}") - unnecessary_files = [] - # remove everything except the required files - for file in files: - if not any(file.endswith(required_file) for required_file in required_files): - unnecessary_files.append(file) - return unnecessary_files + logger.info(f"Successfully processed {len(processed_files)} behavioral files") + def _copy_experiment_files(self, subject_id, session_id, logger): @@ -350,8 +339,6 @@ def convert_to_bids(self, xdf_path,subject_id,session_id, run_id, task_id,other, f.write('sourcedata\n') # ignore the code folder - containing log files f.write('code\n') - # ignore the beh folder in each sub-xxx/ses-yyys - f.write('**/beh\n') # ignore the misc folder in each sub-xxx/ses-yyy f.write('**/misc\n') # ignore hidden files @@ -387,19 +374,19 @@ def validate_bids(self,bids_path,subject_id,session_id, logger): file_path = os.path.join(root, file) # Skip non-relevant files - if file_path.endswith(".xdf") or file_path.endswith(".tar.gz") or 'beh' in file_path or file.startswith('.') or '.git' in file_path or os.path.basename(root).startswith('.'): + if 'misc' in file_path or file.startswith('.') or '.git' in file_path or os.path.basename(root).startswith('.'): continue if root == root_directory: # Validate BIDS for files in the root directory - res = BIDSValidator().is_bids(file) + res = BIDSValidator().is_bids('/'+file) else: # Modify file path to be relative to the root directory relative_path = os.path.relpath(file_path, root_directory) res = BIDSValidator().is_bids('/'+relative_path) if not res: - print(f"Validation failed for {file_path}") + logger.info(f"Validation failed for {file_path}") file_paths.append(res) diff --git a/lslautobids/gen_project_config.py b/lslautobids/gen_project_config.py index bf4136a..e451cae 100644 --- a/lslautobids/gen_project_config.py +++ b/lslautobids/gen_project_config.py @@ -21,7 +21,19 @@ [OtherFilesInfo] otherFilesUsed = true # Set to true if you want to include other (non-eeg-files) files (experiment files, other modalities like eye tracking) in the dataset, else false - expectedOtherFiles = [".edf", ".csv", "_labnotebook.tsv", "_participantform.tsv"] # List of expected other file extensions. Only the expected files will be copied to the beh folder in BIDS dataset. Give an empty list [] if you don't want any other files to be in the dataset. In this case only experiment files will be zipeed and copied to the misc folder in BIDS dataset. + + # expectedOtherFiles: Dictionary format with regex patterns + # - The key is a regular expression to match source filenames in the project_other/.../beh/ folder + # - The value is a template path that includes {prefix} (e.g. sub-003_ses-002) and the target folder (beh/ or misc/) + # - Only files matching these patterns will be copied to the BIDS dataset + # the following is a sample configuration, you could also write it in short-hand notation: expectedOtherFiles={ ".*.edf"= "beh/{prefix}_physio.edf", ...} + + [OtherFilesInfo.expectedOtherFiles] + ".*.edf" = "beh/{prefix}_physio.edf" + ".*.csv" = "beh/{prefix}_beh.tsv" + ".*_labnotebook.tsv" = "misc/{prefix}_labnotebook.tsv" + ".*_participantform.tsv" = "misc/{prefix}_participantform.tsv" + [FileSelection] ignoreSubjects = ['sub-777'] # List of subjects to ignore during the conversion - Leave empty to include all subjects. Changing this value will not delete already existing subjects. diff --git a/requirements.txt b/requirements.txt index ce05b61..883a51a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ pyxdf mne mne-bids -bids_validator==1.13.1 -datalad-dataverse==1.0.1 -datalad-installer==1.0.3 -pyDataverse==0.3.1 +bids_validator>=1.13.1 +datalad-dataverse>=1.0.1 +datalad-installer>=1.0.3 +pyDataverse>=0.3.1 requests>=2.12.0 jsonschema>=3.2.0 AnnexRemote@git+https://github.com/Lykos153/AnnexRemote.git@master#egg=AnnexRemote @@ -13,4 +13,4 @@ pyyaml mnelab pybv pytest -eeglabio \ No newline at end of file +eeglabio diff --git a/tests/data/projects/test-project/test-project_config.toml b/tests/data/projects/test-project/test-project_config.toml deleted file mode 100644 index b1d78a6..0000000 --- a/tests/data/projects/test-project/test-project_config.toml +++ /dev/null @@ -1,27 +0,0 @@ - - # This is the project configuration file - This configuration can be customized for each project - - [AuthorsInfo] - authors = "John Doe, Lina Doe" # List of authors separated by commas - affiliation = "University of Stuttgart, University of Stuttgart" # Affiliation of the authors in the same order as authors - email = "john@gmail.com" # Contact email of the authors in the same order as authors - - [DataverseDataset] - title = "Convert XDF to BIDS" # Title of the Dataverse dataset. This gets updated automatically by the project name. - datasetDescription = "This is a test project to set up the pipeline to convert XDF to BIDS." # Description of the dataset. This description will appear in the dataset.json file which then eventually gets displayed in the dataverse metadata - license = "MIT License" # License for the dataset, e.g. "CC0", "CC-BY-4.0", "ODC-By-1.0", "PDDL-1.0", "ODC-PDDL-1.0", "MIT License" - subject = ["Medicine, Health and Life Sciences","Engineering"] # List of subjects related to the dataset required for dataverse metadata - pid = '' # Persistent identifier for the dataset, e.g. DOI or Handle. This will be updated automatically after creating the dataset in dataverse. - - [OtherFilesInfo] - otherFilesUsed = true # Set to true if you want to include other (non-eeg-files) files (experiment files, other modalities like eye tracking) in the dataset, else false - expectedOtherFiles = [".edf", ".csv", "_labnotebook.tsv", "_participantform.tsv"] # List of expected other file extensions. Only the expected files will be copied to the beh folder in BIDS dataset. Give an empty list [] if you don't want any other files to be in the dataset. In this case only experiment files will be zipeed and copied to the misc folder in BIDS dataset. - - [FileSelection] - ignoreSubjects = ['sub-777'] # List of subjects to ignore during the conversion - Leave empty to include all subjects. Changing this value will not delete already existing subjects. - excludeTasks = ['sampletask'] # List of tasks to exclude from the conversion for all subjects - Leave empty to include all tasks. Changing this value will not delete already existing tasks. - - [BidsConfig] - anonymizationNumber = 123 # This is an anomization number that will be added to the recording date of all subjects. - - \ No newline at end of file diff --git a/tests/test_main_functionality/data/projects/test-project/last_run_log.txt b/tests/test_main_functionality/data/projects/test-project/last_run_log.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_main_functionality/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-copy-me_run-002_eeg.xdf b/tests/test_main_functionality/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-copy-me_run-002_eeg.xdf new file mode 100644 index 0000000..6239d63 Binary files /dev/null and b/tests/test_main_functionality/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-copy-me_run-002_eeg.xdf differ diff --git a/tests/test_main_functionality/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-ignore-me_run-002_eeg.xdf b/tests/test_main_functionality/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-ignore-me_run-002_eeg.xdf new file mode 100644 index 0000000..6239d63 Binary files /dev/null and b/tests/test_main_functionality/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-ignore-me_run-002_eeg.xdf differ diff --git a/tests/test_main_functionality/data/projects/test-project/test-project_config.toml b/tests/test_main_functionality/data/projects/test-project/test-project_config.toml index 0c0d89e..7ba687d 100644 --- a/tests/test_main_functionality/data/projects/test-project/test-project_config.toml +++ b/tests/test_main_functionality/data/projects/test-project/test-project_config.toml @@ -12,11 +12,16 @@ pid = "" [OtherFilesInfo] otherFilesUsed = true -expectedOtherFiles = [ ".edf", ".csv", "_labnotebook.tsv", "_participantform.tsv",] [FileSelection] ignoreSubjects = [ "sub-777",] -excludeTasks = [ "sampletask",] +excludeTasks = [ "ignore-me",] [BidsConfig] anonymizationNumber = 123 + +[OtherFilesInfo.expectedOtherFiles] +".*.edf" = "misc/{prefix}_physio.edf" +".*.csv" = "misc/{prefix}_beh.csv" +".*_labnotebook.tsv" = "misc/{prefix}_labnotebook.tsv" +".*_participantform.tsv" = "misc/{prefix}_participantform.tsv" diff --git a/tests/test_main_functionality/test_main_functionality.py b/tests/test_main_functionality/test_main_functionality.py index c374d9f..719d82c 100644 --- a/tests/test_main_functionality/test_main_functionality.py +++ b/tests/test_main_functionality/test_main_functionality.py @@ -1,10 +1,10 @@ import os import sys import pytest -import yaml +#import yaml import shutil #import lslautobids -import importlib +#import importlib #import lslautobids.main # Compute project root (two levels up from current test.py) PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) @@ -24,7 +24,7 @@ @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_process_main_functionality(setup_project, monkeypatch): """ - Expect the main pipeline to raise RuntimeError when duplicate files are found. + This should not raise any errors at all! """ project_name = setup_project # fixture via pytest paths = get_root_paths(__file__) @@ -43,14 +43,26 @@ def test_process_main_functionality(setup_project, monkeypatch): # run once import lslautobids.main lslautobids.main.main() - + + # sub-099 exists, but not sub-100 + fixture_path = os.path.join(paths["bids_root"],project_name,"sub-099","ses-001","eeg","sub-099_ses-001_task-freeviewing_run-2_eeg.set") + assert os.path.exists(fixture_path) + + fixture_path = os.path.join(paths["bids_root"],project_name,"sub-100","ses-001","eeg","sub-100_ses-001_task-freeviewing_run-2_eeg.set") + assert not os.path.exists(fixture_path) # add a subject shutil.copytree(os.path.join(paths["project_root"], "copy_later","sub-100"), os.path.join(paths["project_root"], project_name,"sub-100")) lslautobids.main.main() + # test that bids/sub-100 folder exists + # test that both subjects have experiment.tar.gz with identical content + + fixture_path = os.path.join(paths["bids_root"],project_name,"sub-100","ses-001","eeg","sub-100_ses-001_task-freeviewing_run-2_eeg.set") + assert os.path.exists(fixture_path) + # cleanup shutil.rmtree(os.path.join(paths["project_root"], project_name,"sub-100")) - shutil.rmtree(paths["bids_root"]) + #shutil.rmtree(paths["bids_root"]) diff --git a/tests/test_otherfiles-renaming/__init__.py b/tests/test_otherfiles-renaming/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/ignoreme.tsv b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/ignoreme.tsv new file mode 100644 index 0000000..1a6c624 --- /dev/null +++ b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/ignoreme.tsv @@ -0,0 +1 @@ +id age gender handedness dom_eye no_preex_conditions visual_acuity_test remarks diff --git a/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/participantform.tsv b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/participantform.tsv new file mode 100644 index 0000000..1a6c624 --- /dev/null +++ b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/participantform.tsv @@ -0,0 +1 @@ +id age gender handedness dom_eye no_preex_conditions visual_acuity_test remarks diff --git a/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/sub-666_ses-001_labnotebook.tsv b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/sub-666_ses-001_labnotebook.tsv new file mode 100644 index 0000000..c68197a --- /dev/null +++ b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/sub-666_ses-001_labnotebook.tsv @@ -0,0 +1,4 @@ +time event what +00:00 cap size selection +00:00 camera working y/n + diff --git a/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/sub_99.edf b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/sub_99.edf new file mode 100644 index 0000000..86d8d1e Binary files /dev/null and b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/sub_99.edf differ diff --git a/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/subject-99.csv b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/subject-99.csv new file mode 100644 index 0000000..c18df11 --- /dev/null +++ b/tests/test_otherfiles-renaming/data/project_other/test-project/data/sub-099/ses-001/beh/subject-99.csv @@ -0,0 +1,4 @@ +"acc","accuracy","average_response_time","avg_rt","background","block_size","canvas_backend","clock_backend","coco_id","color_backend","correct","correct_instructions_sketchpad","correct_keyboard_response","correct_manual_pause_end_kbd","correct_manual_pause_kbd","correct_start_practice_trials_info","correct_welcome","count_PressSpacebar","count_block_sequence","count_break_sequence","count_break_sketchpad","count_end_of_exp","count_end_of_practice","count_endofinstructions","count_endofpractice","count_experiment","count_fixation1_sketchpad","count_fixation_jittered","count_fixation_loop","count_fixation_sequence","count_general_setup","count_getting_started","count_instructions","count_instructions_sketchpad","count_keyboard_response","count_log_all_variables","count_main_loop","count_manual_calibrate","count_manual_pause_end","count_manual_pause_end_kbd","count_manual_pause_kbd","count_manual_pause_start","count_new_1_form_text_display","count_new_1_inline_script","count_new_1_sketchpad","count_new_2_inline_script","count_new_2_sequence","count_new_3_inline_script","count_new_4_inline_script","count_new_5_inline_script","count_new_6_inline_script","count_new_7_inline_script","count_new_advanced_delay","count_new_feedback","count_new_form_text_display","count_new_inline_script","count_new_loop","count_new_pygaze_init","count_new_pygaze_log","count_new_pygaze_start_recording","count_new_pygaze_stop_recording","count_new_reset_feedback","count_new_sequence","count_practiceinstructions","count_practiceloop","count_priliminaryinstructions","count_send_trigger_breakend","count_send_trigger_breakstart","count_send_trigger_end_practice","count_send_trigger_end_stimulus","count_send_trigger_manual_pause_end","count_send_trigger_manual_pause_start","count_send_trigger_start_stimulus","count_setup","count_start_practice_trials_info","count_stimuli","count_stimulus_sequence","count_trial_sequence","count_trigger_setup","count_wait_for_centre_gaze","count_welcome","current_block_no","delay_new_advanced_delay","description","disable_garbage_collection","duration","empty_column","exp_imgdetails_file_name","experiment_file","experiment_path","file_name","file_path_coco_dataset","fixated","font_bold","font_family","font_italic","font_size","font_underline","foreground","form_clicks","fullscreen","gaze_timeout","height","keyboard_backend","license","live_row","live_row_practiceloop","logfile","mouse_backend","n_practicetrials","opensesame_codename","opensesame_version","psychopy_screen","repeat_cycle","response","response_break_sketchpad","response_end_of_exp","response_end_of_practice","response_instructions_sketchpad","response_keyboard_response","response_manual_pause_end_kbd","response_manual_pause_kbd","response_new_1_sketchpad","response_new_feedback","response_start_practice_trials_info","response_time","response_time_break_sketchpad","response_time_end_of_exp","response_time_end_of_practice","response_time_instructions_sketchpad","response_time_keyboard_response","response_time_manual_pause_end_kbd","response_time_manual_pause_kbd","response_time_new_1_sketchpad","response_time_new_feedback","response_time_start_practice_trials_info","response_time_welcome","response_welcome","round_decimals","sampler_backend","sound_buf_size","sound_channels","sound_freq","sound_sample_size","start","subject_nr","subject_parity","time_PressSpacebar","time_block_sequence","time_break_sequence","time_break_sketchpad","time_end_of_exp","time_end_of_practice","time_endofinstructions","time_endofpractice","time_experiment","time_fixation1_sketchpad","time_fixation_jittered","time_fixation_loop","time_fixation_sequence","time_general_setup","time_getting_started","time_instructions","time_instructions_sketchpad","time_keyboard_response","time_log_all_variables","time_main_loop","time_manual_calibrate","time_manual_pause_end","time_manual_pause_end_kbd","time_manual_pause_kbd","time_manual_pause_start","time_new_1_form_text_display","time_new_1_inline_script","time_new_1_sketchpad","time_new_2_inline_script","time_new_2_sequence","time_new_3_inline_script","time_new_4_inline_script","time_new_5_inline_script","time_new_6_inline_script","time_new_7_inline_script","time_new_advanced_delay","time_new_feedback","time_new_form_text_display","time_new_inline_script","time_new_loop","time_new_pygaze_init","time_new_pygaze_log","time_new_pygaze_start_recording","time_new_pygaze_stop_recording","time_new_reset_feedback","time_new_sequence","time_practiceinstructions","time_practiceloop","time_priliminaryinstructions","time_send_trigger_breakend","time_send_trigger_breakstart","time_send_trigger_end_practice","time_send_trigger_end_stimulus","time_send_trigger_manual_pause_end","time_send_trigger_manual_pause_start","time_send_trigger_start_stimulus","time_setup","time_start_practice_trials_info","time_stimuli","time_stimulus_sequence","time_trial_sequence","time_trigger_setup","time_wait_for_centre_gaze","time_welcome","title","tolerance","total_blocks","total_correct","total_response_time","total_responses","total_trials","width" +"undefined","undefined","810","810","#808080","50","psycho","psycho","304187","psycho","undefined","undefined","undefined","NA","NA","undefined","undefined","0","-3","0","0","0","0","NA","NA","0","0","NA","NA","0","0","NA","0","0","0","0","0","1","0","0","0","0","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","0","NA","NA","NA","NA","0","NA","0","0","NA","NA","NA","0","NA","0","0","0","0","0","0","0","0","0","0","0","NA","0","0","0","1","419","The main experiment item","False","650","NA","projects/2024FreeViewingMSCOCO/assets/images/experiment_images_info.csv","free_viewing_experiment.osexp","/home/stimulus/projects/2024FreeViewingMSCOCO/experiment","000000304187_MEG_size.jpg","/scratch/data/coco/coco-2017/train/data/000000304187.jpg","False","False","mono","False","18","no","white","no","yes","5000","1080","psycho","Attribution License","0","0","/home/stimulus/projects/2024FreeViewingMSCOCO/data/sub-099/ses-001/beh/subject-99.csv","psycho","3","Melodramatic Milgram","4.0.13","1","0","space","NA","NA","NA","space","space","NA","NA","NA","NA","space","810.305118560791","NA","NA","NA","588.4740352630615","810.305118560791","NA","NA","NA","NA","398.3440399169922","1945.7039833068848","space","2","psycho","1024","2","48000","-16","experiment","99","odd","14410.46404838562","14407.202959060669","NA","NA","NA","NA","NA","NA","1086.3871574401855","15227.399110794067","NA","NA","14407.315015792847","1647.9620933532715","NA","11160.964012145996","13112.376928329468","14410.552978515625","21306.498050689697","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","15885.131120681763","NA","NA","NA","NA","1086.4861011505127","NA","14100.979089736938","NA","NA","NA","NA","14311.726093292236","NA","NA","NA","NA","21306.37001991272","NA","NA","16304.228067398071","1086.4551067352295","13702.502012252808","16306.323051452637","15885.082006454468","NA","1643.4519290924072","15227.437973022461","11163.213014602661","free_viewing_experiment","44","8","0","810.305118560791","1","400","1920" +"undefined","undefined","671","671","#808080","50","psycho","psycho","303436","psycho","undefined","undefined","undefined","NA","NA","undefined","undefined","1","-2","1","1","0","0","NA","NA","0","1","NA","NA","1","0","NA","0","0","1","1","0","2","1","1","1","1","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","1","NA","NA","NA","NA","0","NA","0","0","NA","NA","NA","0","NA","1","1","0","1","1","1","1","0","0","1","1","NA","0","1","0","1","389","The main experiment item","False","650","NA","projects/2024FreeViewingMSCOCO/assets/images/experiment_images_info.csv","free_viewing_experiment.osexp","/home/stimulus/projects/2024FreeViewingMSCOCO/experiment","000000303436_MEG_size.jpg","/scratch/data/coco/coco-2017/train/data/000000303436.jpg","False","False","mono","False","18","no","white","no","yes","5000","1080","psycho","Attribution-NonCommercial-NoDerivs License","1","1","/home/stimulus/projects/2024FreeViewingMSCOCO/data/sub-099/ses-001/beh/subject-99.csv","psycho","3","Melodramatic Milgram","4.0.13","1","0","space","NA","NA","NA","space","space","NA","NA","NA","NA","space","533.376932144165","NA","NA","NA","588.4740352630615","533.376932144165","NA","NA","NA","NA","398.3440399169922","1945.7039833068848","space","2","psycho","1024","2","48000","-16","experiment","99","odd","21415.446996688843","21412.346124649048","NA","NA","NA","NA","NA","NA","1086.3871574401855","21951.735019683838","NA","NA","21412.39595413208","1647.9620933532715","NA","11160.964012145996","13112.376928329468","21415.497064590454","28000.17809867859","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","22608.845949172974","NA","NA","NA","NA","1086.4861011505127","NA","14100.979089736938","NA","NA","NA","NA","14311.726093292236","NA","NA","NA","NA","28000.051975250244","NA","NA","22997.878074645996","1086.4551067352295","13702.502012252808","22999.948978424072","22608.808040618896","NA","1643.4519290924072","21951.760053634644","11163.213014602661","free_viewing_experiment","44","8","0","1343.682050704956","2","400","1920" +"undefined","undefined","640","640","#808080","50","psycho","psycho","222921","psycho","undefined","undefined","undefined","NA","NA","undefined","undefined","2","-1","2","2","0","0","NA","NA","0","2","NA","NA","2","0","NA","0","0","2","2","0","3","2","2","2","2","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","2","NA","NA","NA","NA","0","NA","0","0","NA","NA","NA","0","NA","2","2","0","2","2","2","2","0","0","2","2","NA","0","2","0","1","375","The main experiment item","False","650","NA","projects/2024FreeViewingMSCOCO/assets/images/experiment_images_info.csv","free_viewing_experiment.osexp","/home/stimulus/projects/2024FreeViewingMSCOCO/experiment","000000222921_MEG_size.jpg","/scratch/data/coco/coco-2017/train/data/000000222921.jpg","False","False","mono","False","18","no","white","no","yes","5000","1080","psycho","Attribution-NonCommercial License","2","2","/home/stimulus/projects/2024FreeViewingMSCOCO/data/sub-099/ses-001/beh/subject-99.csv","psycho","3","Melodramatic Milgram","4.0.13","1","0","space","NA","NA","NA","space","space","NA","NA","NA","NA","space","576.591968536377","NA","NA","NA","588.4740352630615","576.591968536377","NA","NA","NA","NA","398.3440399169922","1945.7039833068848","space","2","psycho","1024","2","48000","-16","experiment","99","odd","28108.245134353638","28105.04913330078","NA","NA","NA","NA","NA","NA","1086.3871574401855","28687.549114227295","NA","NA","28105.098962783813","1647.9620933532715","NA","11160.964012145996","13112.376928329468","28108.291149139404","34722.15914726257","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","29344.93613243103","NA","NA","NA","NA","1086.4861011505127","NA","14100.979089736938","NA","NA","NA","NA","14311.726093292236","NA","NA","NA","NA","34722.04303741455","NA","NA","29719.972133636475","1086.4551067352295","13702.502012252808","29721.93193435669","29344.89893913269","NA","1643.4519290924072","28687.572956085205","11163.213014602661","free_viewing_experiment","44","8","0","1920.274019241333","3","400","1920" diff --git a/tests/test_otherfiles-renaming/data/project_other/test-project/experiment/fake_experiment.osexp b/tests/test_otherfiles-renaming/data/project_other/test-project/experiment/fake_experiment.osexp new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_otherfiles-renaming/data/projects/test-project/dataset.json b/tests/test_otherfiles-renaming/data/projects/test-project/dataset.json new file mode 100644 index 0000000..5e91c22 --- /dev/null +++ b/tests/test_otherfiles-renaming/data/projects/test-project/dataset.json @@ -0,0 +1,76 @@ +{ + "datasetVersion": { + "metadataBlocks": { + "citation": { + "fields": [ + { + "typeName": "title", + "multiple": false, + "typeClass": "primitive", + "value": "test-project" + }, + { + "typeName": "author", + "multiple": true, + "typeClass": "compound", + "value": [ + { + "authorName": { + "typeName": "author", + "multiple": false, + "typeClass": "primitive", + "value": "John Doe, Lina Doe" + } + } + ] + }, + { + "typeName": "datasetContact", + "multiple": true, + "typeClass": "compound", + "value": [ + { + "datasetContactName": { + "typeName": "datasetContactName", + "multiple": false, + "typeClass": "primitive", + "value": "John Doe, Lina Doe" + }, + "datasetContactEmail": { + "typeName": "datasetContactEmail", + "multiple": false, + "typeClass": "primitive", + "value": "john@gmail.com" + } + } + ] + }, + { + "typeName": "dsDescription", + "multiple": true, + "typeClass": "compound", + "value": [ + { + "dsDescriptionValue": { + "typeName": "dsDescriptionValue", + "multiple": false, + "typeClass": "primitive", + "value": "This is a test project to set up the pipeline to convert XDF to BIDS." + } + } + ] + }, + { + "typeName": "subject", + "multiple": true, + "typeClass": "controlledVocabulary", + "value": [ + "Medicine, Health and Life Sciences", + "Engineering" + ] + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/test_otherfiles-renaming/data/projects/test-project/last_run_log.txt b/tests/test_otherfiles-renaming/data/projects/test-project/last_run_log.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_otherfiles-renaming/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-freeviewing_run-002_eeg.xdf b/tests/test_otherfiles-renaming/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-freeviewing_run-002_eeg.xdf new file mode 100644 index 0000000..6239d63 Binary files /dev/null and b/tests/test_otherfiles-renaming/data/projects/test-project/sub-099/ses-001/eeg/sub-099_ses-001_task-freeviewing_run-002_eeg.xdf differ diff --git a/tests/test_otherfiles-renaming/data/projects/test-project/test-project_config.toml b/tests/test_otherfiles-renaming/data/projects/test-project/test-project_config.toml new file mode 100644 index 0000000..56a54d5 --- /dev/null +++ b/tests/test_otherfiles-renaming/data/projects/test-project/test-project_config.toml @@ -0,0 +1,27 @@ +[AuthorsInfo] +authors = "John Doe, Lina Doe" +affiliation = "University of Stuttgart, University of Stuttgart" +email = "john@gmail.com" + +[DataverseDataset] +title = "test-project" +datasetDescription = "This is a test project to set up the pipeline to convert XDF to BIDS." +license = "MIT License" +subject = [ "Medicine, Health and Life Sciences", "Engineering",] +pid = "" + +[OtherFilesInfo] +otherFilesUsed = true + +[FileSelection] +ignoreSubjects = [ "sub-777",] +excludeTasks = [ "sampletask",] + +[BidsConfig] +anonymizationNumber = 123 + +[OtherFilesInfo.expectedOtherFiles] +".*.edf" = "misc/{prefix}_physio.edf" +".*.csv" = "misc/{prefix}_beh.tsv" +".*_labnotebook.tsv" = "misc/{prefix}_labnotebook.tsv" +".*participantform.tsv" = "misc/{prefix}_participantform.tsv" diff --git a/tests/test_otherfiles-renaming/test_otherfiles-renaming.py b/tests/test_otherfiles-renaming/test_otherfiles-renaming.py new file mode 100644 index 0000000..01e8865 --- /dev/null +++ b/tests/test_otherfiles-renaming/test_otherfiles-renaming.py @@ -0,0 +1,50 @@ +import os +import sys +import pytest +import yaml +import shutil +#import lslautobids +import importlib +#import lslautobids.main +# Compute project root (two levels up from current test.py) +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + + +from path_config import get_root_paths,monkeypatch_paths +#from test_utils.path_config import DummyCLIArgs +# Print test file name for traceability +test_file_name = os.path.basename(__file__) +print(f" Running tests in {test_file_name}") + + + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_process_otherfiles_renaming(setup_project, monkeypatch): + """ + Expect the main pipeline to raise RuntimeError when duplicate files are found. + """ + project_name = setup_project # fixture via pytest + paths = get_root_paths(__file__) + + monkeypatch_paths(monkeypatch,paths) + + + # Reset sys.argv to something that lslautobids.main.main() expects + # this effectively removes the -c from setup_project + sys.argv = [ + "lslautobids.main", + "-p", project_name, + # other args expected by lslautobids.main.main + ] + + # run once + import lslautobids.main + lslautobids.main.main() + + # cleanup + shutil.rmtree(paths["bids_root"]) + +