Skip to content

Commit f371123

Browse files
author
Benedikt Ehinger
committed
working version of the dict approach. I removed the list appraoch, doesnt have benefits in my view
1 parent aeddfde commit f371123

File tree

15 files changed

+252
-71
lines changed

15 files changed

+252
-71
lines changed

lslautobids/convert_to_bids_and_upload.py

Lines changed: 57 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import shutil
44
import sys
5+
import re
56

67
from pyxdf import match_streaminfos, resolve_streams
78
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)
9293

9394
def _copy_behavioral_files(self, file_base, subject_id, session_id, logger):
9495
"""
95-
Copy behavioral files to the BIDS structure.
96+
Copy behavioral files to the BIDS structure based on regex patterns.
97+
Iterates through patterns and matches files, copying them directly to target locations.
9698
9799
Args:
98100
file_base (str): Base name of the file (without extension).
99101
subject_id (str): Subject ID.
100102
session_id (str): Session ID.
103+
logger: Logger instance.
101104
"""
105+
102106
project_name = cli_args.project_name
103107
logger.info("Copying the behavioral files to BIDS...")
108+
109+
# Get the TOML configuration
110+
toml_path = os.path.join(project_root, cli_args.project_name, cli_args.project_name + '_config.toml')
111+
data = read_toml_file(toml_path)
112+
_expectedotherfiles = data["OtherFilesInfo"]["expectedOtherFiles"]
113+
114+
if not isinstance(_expectedotherfiles, dict):
115+
raise ValueError("expectedOtherFiles must be a dictionary with regex patterns. List format is no longer supported since v0.2.0 .")
116+
104117
# get the source path
105-
behavioural_path = os.path.join(project_other_root,project_name,'data', subject_id,session_id,'beh')
106-
# get the destination path
107-
dest_dir = os.path.join(bids_root , project_name, subject_id , session_id , 'beh')
108-
#check if the directory exists
109-
os.makedirs(dest_dir, exist_ok=True)
110-
111-
processed_files = []
118+
behavioural_path = os.path.join(project_other_root, project_name, 'data', subject_id, session_id, 'beh')
119+
120+
if not os.path.exists(behavioural_path):
121+
raise FileNotFoundError(f"Behavioral path does not exist: {behavioural_path} - did you forget to mount?")
122+
return
123+
112124
# Extract the sub-xxx_ses-yyy part
113125
def extract_prefix(filename):
114126
parts = filename.split("_")
115127
sub = next((p for p in parts if p.startswith("sub-")), None)
116128
ses = next((p for p in parts if p.startswith("ses-")), None)
117129
if sub and ses:
118-
return f"{sub}_{ses}_"
130+
return f"{sub}_{ses}"
119131
return None
120132

121133
prefix = extract_prefix(file_base)
122-
123-
for file in os.listdir(behavioural_path):
124-
# Skip non-files (like directories)
125-
original_path = os.path.join(behavioural_path, file)
126-
if not os.path.isfile(original_path):
127-
continue
128-
129-
if not file.startswith(prefix):
130-
logger.info(f"Renaming {file} to include prefix {prefix}")
131-
renamed_file = prefix + file
132-
else:
133-
renamed_file = file
134+
processed_files = []
134135

135-
processed_files.append(renamed_file)
136-
dest_file = os.path.join(dest_dir, renamed_file)
136+
# Get all files in source directory once
137+
source_files = [f for f in os.listdir(behavioural_path)
138+
if os.path.isfile(os.path.join(behavioural_path, f))]
137139

140+
# Iterate through patterns (not files)
141+
for pattern, target_template in _expectedotherfiles.items():
142+
compiled_regex = re.compile(pattern)
143+
144+
# Find matching files for this pattern
145+
matched_files = [f for f in source_files if compiled_regex.match(f)]
146+
147+
if not matched_files:
148+
raise FileExistsError(f"No files matched pattern '{pattern}' in {behavioural_path}")
149+
150+
if len(matched_files) > 1:
151+
raise ValueError(f"Multiple files matched pattern '{pattern}': {matched_files}. Only one file per pattern is supported - manuall intervention required")
152+
153+
# Process the first matching file
154+
file = matched_files[0]
155+
original_path = os.path.join(behavioural_path, file)
156+
157+
# Format the target path with prefix
158+
target_path = target_template.format(prefix=prefix)
159+
dest_file = os.path.join(bids_root, project_name, subject_id, session_id, target_path)
160+
161+
# Ensure destination directory exists
162+
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
163+
164+
# Track the relative path for checking
165+
processed_files.append(target_path)
166+
138167
if cli_args.redo_other_pc:
139-
logger.info(f"Copying (overwriting if needed) {file} to {dest_file}")
168+
logger.info(f"Copying (overwriting) {file} to {target_path}")
140169
shutil.copy(original_path, dest_file)
141170
else:
142171
if os.path.exists(dest_file):
143-
logger.info(f"Behavioural file {file} already exists in BIDS. Skipping.")
172+
logger.info(f"Behavioural file {target_path} already exists in BIDS. Skipping.")
144173
else:
145-
logger.info(f"Copying new file {file} to {dest_file}")
174+
logger.info(f"Copying {file} to {target_path}")
146175
shutil.copy(original_path, dest_file)
147-
148-
149-
150-
unnecessary_files = self._check_required_behavioral_files(processed_files, prefix, logger)
151-
152-
# remove the unnecessary files
153-
for file in unnecessary_files:
154-
file_path = os.path.join(dest_dir, file)
155-
if os.path.exists(file_path):
156-
logger.info(f"Removing unnecessary file: {file_path}")
157-
os.remove(file_path)
158-
else:
159-
logger.warning(f"File to remove does not exist: {file_path}")
160-
161-
162-
163-
def _check_required_behavioral_files(self, files, prefix, logger):
164-
"""
165-
Check for required behavioral files after copying.
166-
167-
Args:
168-
files (list): List of copied file names.
169-
prefix (str): Expected prefix (e.g., "sub-001_ses-002_").
170-
"""
171-
logger.info("Checking for required behavioral files...")
172-
173-
# Get the expected file names from the toml file
174-
toml_path = os.path.join(project_root, cli_args.project_name, cli_args.project_name + '_config.toml')
175-
data = read_toml_file(toml_path)
176-
177-
required_files = data["OtherFilesInfo"]["expectedOtherFiles"]
178-
179-
180-
for required_file in required_files:
181-
if not any(f.startswith(prefix) and f.endswith(required_file) for f in files):
182-
raise FileNotFoundError(f"Missing required behavioral file: {required_file}")
183176

184-
unnecessary_files = []
185-
# remove everything except the required files
186-
for file in files:
187-
if not any(file.endswith(required_file) for required_file in required_files):
188-
unnecessary_files.append(file)
189-
return unnecessary_files
177+
logger.info(f"Successfully processed {len(processed_files)} behavioral files")
178+
190179

191180

192181
def _copy_experiment_files(self, subject_id, session_id, logger):

lslautobids/gen_project_config.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,19 @@
2121
2222
[OtherFilesInfo]
2323
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
24-
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.
24+
25+
# expectedOtherFiles: Dictionary format with regex patterns
26+
# - The key is a regular expression to match source filenames in the project_other/.../beh/ folder
27+
# - The value is a template path that includes {prefix} (e.g. sub-003_ses-002) and the target folder (beh/ or misc/)
28+
# - Only files matching these patterns will be copied to the BIDS dataset
29+
# the following is a sample configuration, you could also write it in short-hand notation: expectedOtherFiles={ ".*.edf"= "beh/{prefix}_physio.edf", ...}
30+
31+
[OtherFilesInfo.expectedOtherFiles]
32+
".*.edf" = "beh/{prefix}_physio.edf"
33+
".*.csv" = "beh/{prefix}_beh.tsv"
34+
".*_labnotebook.tsv" = "misc/{prefix}_labnotebook.tsv"
35+
".*_participantform.tsv" = "misc/{prefix}_participantform.tsv"
36+
2537
2638
[FileSelection]
2739
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.

tests/data/projects/test-project/test-project_config.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,19 @@
1515

1616
[OtherFilesInfo]
1717
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
18-
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.
18+
19+
# expectedOtherFiles: Dictionary format with regex patterns
20+
# - The key is a regular expression to match source filenames in the project_other/.../beh/ folder
21+
# - The value is a template path that includes {prefix} (e.g. sub-003_ses-002) and the target folder (beh/ or misc/)
22+
# - Only files matching these patterns will be copied to the BIDS dataset
23+
# the following is a sample configuration, you could also write it in short-hand notation: expectedOtherFiles={ ".*.edf"= "beh/{prefix}_physio.edf", ...}
24+
25+
[OtherFilesInfo.expectedOtherFiles]
26+
".*.edf" = "beh/{prefix}_physio.edf"
27+
".*.csv" = "beh/{prefix}_beh.tsv"
28+
".*_labnotebook.tsv" = "misc/{prefix}_labnotebook.tsv"
29+
".*_participantform.tsv" = "misc/{prefix}_participantform.tsv"
30+
1931

2032
[FileSelection]
2133
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.

tests/test_main_functionality/data/projects/test-project/test-project_config.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@ pid = ""
1212

1313
[OtherFilesInfo]
1414
otherFilesUsed = true
15-
expectedOtherFiles = [ ".edf", ".csv", "_labnotebook.tsv", "_participantform.tsv",]
1615

1716
[FileSelection]
1817
ignoreSubjects = [ "sub-777",]
1918
excludeTasks = [ "sampletask",]
2019

2120
[BidsConfig]
2221
anonymizationNumber = 123
22+
23+
[OtherFilesInfo.expectedOtherFiles]
24+
".*.edf" = "beh/{prefix}_physio.edf"
25+
".*.csv" = "beh/{prefix}_beh.tsv"
26+
".*_labnotebook.tsv" = "misc/{prefix}_labnotebook.tsv"
27+
".*_participantform.tsv" = "misc/{prefix}_participantform.tsv"

tests/test_otherfiles-renaming/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
id age gender handedness dom_eye no_preex_conditions visual_acuity_test remarks
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
id age gender handedness dom_eye no_preex_conditions visual_acuity_test remarks
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
time event what
2+
00:00 cap size selection
3+
00:00 camera working y/n
4+

0 commit comments

Comments
 (0)