diff --git a/phys2bids/bids.py b/phys2bids/bids.py index a2782faf7..b71d95824 100644 --- a/phys2bids/bids.py +++ b/phys2bids/bids.py @@ -1,8 +1,10 @@ import os import logging - +from csv import reader from pathlib import Path +import yaml + from phys2bids import utils LGR = logging.getLogger(__name__) @@ -170,3 +172,73 @@ def use_heuristic(heur_file, sub, ses, filename, outdir, record_label=''): heurpath = os.path.join(fldr, f'{name}physio') return heurpath + + +def participants_file(outdir, yml, sub): + """ + Create participants.tsv file if it does not exist. + If it exists and the subject is missing, then add it. + Otherwise, do nothing. + + Parameters + ---------- + outdir: path + Full path to the output directory. + yml: path + Full path to the yaml file. + sub: str + Subject ID. + + """ + LGR.info('Updating participants.tsv ...') + file_path = os.path.join(outdir, 'participants.tsv') + if not os.path.exists(file_path): + LGR.warning('phys2bids could not find participants.tsv') + # Read yaml info if file exists + if '.yml' in yml and os.path.exists(yml): + LGR.info('Using yaml data to populate participants.tsv') + with open(yml) as f: + yaml_data = yaml.load(f, Loader=yaml.FullLoader) + p_id = f'sub-{sub}' + p_age = yaml_data['participant']['age'] + p_sex = yaml_data['participant']['sex'] + p_handedness = yaml_data['participant']['handedness'] + else: + LGR.info('No yaml file was provided. Using phys2bids data to ' + 'populate participants.tsv') + # Fill in with data from phys2bids + p_id = f'sub-{sub}' + p_age = 'n/a' + p_sex = 'n/a' + p_handedness = 'n/a' + + # Write to participants.tsv file + header = ['participant_id', 'age', 'sex', 'handedness'] + utils.append_list_as_row(file_path, header) + + participants_data = [p_id, p_age, p_sex, p_handedness] + utils.append_list_as_row(file_path, participants_data) + + else: # If participants.tsv exists only update when subject is not there + LGR.info('phys2bids found participants.tsv. Updating if needed...') + # Find participant_id column in header + pf = open(file_path, 'r') + header = pf.readline().split("\t") + header_length = len(header) + pf.close() + p_id_idx = header.index('participant_id') + + # Check if subject is already in the file + sub_exists = False + with open(file_path) as pf: + tsvreader = reader(pf, delimiter="\t") + for line in tsvreader: + if sub in line[p_id_idx]: + sub_exists = True + break + # Only append to file if subject is not in the file + if not sub_exists: + LGR.info(f'Appending subjet sub-{sub} to participants.tsv ...') + participants_data = ['n/a'] * header_length + participants_data[p_id_idx] = f'sub-{sub}' + utils.append_list_as_row(file_path, participants_data) diff --git a/phys2bids/cli/run.py b/phys2bids/cli/run.py index 8b9118ccd..1aa0f2dd1 100644 --- a/phys2bids/cli/run.py +++ b/phys2bids/cli/run.py @@ -125,6 +125,12 @@ def _get_parser(): type=str, help='full path to store channels plot ', default='') + optional.add_argument('-yml', '--participant-yml', + dest='yml', + type=str, + help='full path to file with info needed to generate ' + 'participant.tsv file ', + default='') optional.add_argument('-debug', '--debug', dest='debug', action='store_true', diff --git a/phys2bids/heuristics/participant.yml b/phys2bids/heuristics/participant.yml new file mode 100644 index 000000000..a509d8c1d --- /dev/null +++ b/phys2bids/heuristics/participant.yml @@ -0,0 +1,5 @@ +participant: + participant_id: # Required + age: n/a + sex: n/a + handedness: n/a \ No newline at end of file diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 06d74fd65..5745d0562 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -34,7 +34,7 @@ from numpy import savetxt from phys2bids import utils, viz, _version -from phys2bids.bids import bidsify_units, use_heuristic +from phys2bids.bids import bidsify_units, use_heuristic, participants_file from phys2bids.cli.run import _get_parser from phys2bids.physio_obj import BlueprintOutput @@ -112,7 +112,8 @@ def print_json(outfile, samp_freq, time_offset, ch_name): def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, sub=None, ses=None, chtrig=0, chsel=None, num_timepoints_expected=0, - tr=1, thr=None, ch_name=[], chplot='', debug=False, quiet=False): + tr=1, thr=None, ch_name=[], chplot='', debug=False, quiet=False, + yml=''): """ Main workflow of phys2bids. @@ -264,6 +265,10 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, if heur_file and sub: LGR.info(f'Preparing BIDS output using {heur_file}') + # Generate participants.tsv file if it doesn't exist already. + # Update the file if the subject is not in the file. + # Do not update if the subject is already in the file. + participants_file(outdir, yml, sub) elif heur_file and not sub: LGR.warning('While "-heur" was specified, option "-sub" was not.\n' 'Skipping BIDS formatting.') diff --git a/phys2bids/tests/test_integration.py b/phys2bids/tests/test_integration.py index cb50eda93..9d285117d 100644 --- a/phys2bids/tests/test_integration.py +++ b/phys2bids/tests/test_integration.py @@ -4,6 +4,7 @@ import os import re import subprocess +from csv import reader from pkg_resources import resource_filename from phys2bids._version import get_versions @@ -336,6 +337,19 @@ def test_integration_heuristic(samefreq_short_txt_file): assert math.isclose(json_data['StartTime'], -189.6,) assert json_data['Columns'] == ['time', 'RESP - RSP100C', 'MR TRIGGER - Custom, HLT100C - A 5'] + # Check that participant.tsv gets updated + phys2bids(filename=test_full_path, chtrig=test_chtrig, outdir=test_outdir, + num_timepoints_expected=test_ntp, tr=test_tr, thr=test_thr, sub='002', + ses='01', heur_file=test_heur) + + counter = 0 + subject_list = ['participant_id', '006', '002'] + with open(os.path.join(test_path, 'participants.tsv')) as pf: + tsvreader = reader(pf, delimiter="\t") + for line in tsvreader: + assert subject_list[counter] in line[0] + counter += 1 + # Remove generated files for filename in glob.glob(os.path.join(test_path, 'phys2bids*')): os.remove(filename) diff --git a/phys2bids/utils.py b/phys2bids/utils.py index bb5954765..47051961f 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -4,6 +4,7 @@ import logging import os import sys +from csv import writer from pathlib import Path LGR = logging.getLogger(__name__) @@ -280,3 +281,12 @@ def load_heuristic(heuristic): except Exception as exc: raise ImportError(f'Failed to import heuristic {heuristic}: {exc}') return mod + + +def append_list_as_row(file_name, list_of_elem): + # Open file in append mode + with open(file_name, 'a+', newline='') as write_obj: + # Create a writer object from csv module + csv_writer = writer(write_obj, delimiter='\t') + # Add contents of list as last row in the csv file + csv_writer.writerow(list_of_elem) diff --git a/requirements.txt b/requirements.txt index 165bf7cc3..3118cec3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ numpy>=1.9.3 matplotlib>=3.1.1 +PyYAML \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index d184c7100..7da2fe982 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ python_requires = >=3.6.1 install_requires = numpy >=1.9.3 matplotlib >=3.1.1 + PyYAML tests_require = pytest >=3.6 test_suite = pytest @@ -47,8 +48,8 @@ test = interfaces = %(acq)s all = - %(interfaces)s %(doc)s + %(interfaces)s %(style)s %(test)s