diff --git a/docs/index.rst b/docs/index.rst index dde38dcc1..5608ac5c9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,8 @@ If you use ``phys2bids``, please cite it using the Zenodo DOI as: The phys2bids contributors, Daniel Alcalá, Apoorva Ayyagari, Molly Bright, César Caballero-Gaudes, Vicente Ferrer Gallardo, Soichi Hayashi, Ross Markello, Stefano Moia, Rachael Stickland, Eneko Uruñuela, & Kristina Zvolanek (2020, February 6). physiopy/phys2bids: BIDS formatting of physiological recordings v1.3.0-beta (Version v1.3.0-beta). Zenodo. http://doi.org/10.5281/zenodo.3653153 +We also support gathering all relevant citations via `DueCredit `_. + Contents -------- diff --git a/phys2bids/bids.py b/phys2bids/bids.py index 79cc0abc4..2cfc80fad 100644 --- a/phys2bids/bids.py +++ b/phys2bids/bids.py @@ -1,7 +1,10 @@ import logging import os +from csv import reader from pathlib import Path +import yaml + from phys2bids import utils LGR = logging.getLogger(__name__) @@ -172,3 +175,114 @@ def use_heuristic(heur_file, sub, ses, filename, outdir, run='', 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) + + +def dataset_description_file(outdir): + """ + Create dataset_description.json file if it does not exist. + If it exists, do nothing. + + Parameters + ---------- + outdir: path + Full path to the output directory. + + """ + # dictionary that will be written for the basic dataset description version + data_dict = {"Name": os.path.splitext(os.path.basename(outdir))[0], + "BIDSVersion": "1.4.0", "DatasetType": "raw"} + file_path = os.path.join(outdir, 'dataset_description.json') + # check if dataset_description.json exists, if it doesn't create it + if not os.path.exists(file_path): + LGR.warning('phys2bids could not find dataset_description.json,' + 'generating it with provided info') + utils.writejson(file_path, data_dict) + + +def readme_file(outdir): + """ + Create README file if it does not exist. + If it exists, do nothing. + + Parameters + ---------- + outdir: path + Full path to the output directory. + + """ + file_path = os.path.join(outdir, 'README.md') + if not os.path.exists(file_path): + text = 'Empty README, please fill in describing the dataset in more detail.' + LGR.warning('phys2bids could not find README,' + 'generating it EMPTY, please fill in the necessary info') + utils.writefile(file_path, '', text) diff --git a/phys2bids/cli/run.py b/phys2bids/cli/run.py index bdea8c0e6..497eb4869 100644 --- a/phys2bids/cli/run.py +++ b/phys2bids/cli/run.py @@ -127,10 +127,11 @@ def _get_parser(): type=str, help='Column header (for json file output).', default=[]) - optional.add_argument('-chplot', '--channels-plot', - dest='chplot', + optional.add_argument('-yml', '--participant-yml', + dest='yml', type=str, - help='full path to store channels plot ', + help='full path to file with info needed to generate ' + 'participant.tsv file ', default='') optional.add_argument('-debug', '--debug', dest='debug', diff --git a/phys2bids/due.py b/phys2bids/due.py new file mode 100644 index 000000000..787aae393 --- /dev/null +++ b/phys2bids/due.py @@ -0,0 +1,69 @@ +# emacs: at the end of the file +# ex: set sts=4 ts=4 sw=4 et: +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # +""" +Stub file for a guaranteed safe import of duecredit constructs: if duecredit +is not available. +To use it, place it into your project codebase to be imported, e.g. copy as + cp stub.py /path/tomodule/module/due.py +Note that it might be better to avoid naming it duecredit.py to avoid shadowing +installed duecredit. +Then use in your code as + from .due import due, Doi, BibTeX +See https://github.com/duecredit/duecredit/blob/master/README.md for examples. +Origin: Originally a part of the duecredit +Copyright: 2015-2016 DueCredit developers +License: BSD-2 +""" + +from builtins import str +from builtins import object +__version__ = '0.0.5' + + +class InactiveDueCreditCollector(object): + """Just a stub at the Collector which would not do anything""" + def _donothing(self, *args, **kwargs): + """Perform no good and no bad""" + pass + + def dcite(self, *args, **kwargs): + """If I could cite I would""" + def nondecorating_decorator(func): + return func + return nondecorating_decorator + + cite = load = add = _donothing + + def __repr__(self): + return self.__class__.__name__ + '()' + + +def _donothing_func(*args, **kwargs): + """Perform no good and no bad""" + pass + + +try: + from duecredit import due, BibTeX, Doi, Url + if 'due' in locals() and not hasattr(due, 'cite'): + raise RuntimeError( + "Imported due lacks .cite. DueCredit is now disabled") +except Exception as e: + if type(e).__name__ != 'ImportError': + import logging + logging.getLogger("duecredit").error( + 'Module `duecredit` not successfully imported due to "%s". ' + 'Package functionality unaffected.', str(e)) + + # Initiate due stub + due = InactiveDueCreditCollector() + BibTeX = Doi = Url = _donothing_func + +# Emacs mode definitions +# Local Variables: +# mode: python +# py-indent-offset: 4 +# tab-width: 4 +# indent-tabs-mode: nil +# End: 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 3e82eb690..47eb96e3d 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -26,19 +26,23 @@ """ -import os import datetime import logging +import os +import sys from copy import deepcopy +from shutil import copy as cp import numpy as np -from phys2bids import utils, viz, _version -from phys2bids.bids import bidsify_units, use_heuristic +from phys2bids import utils, viz, _version, bids from phys2bids.cli.run import _get_parser from phys2bids.physio_obj import BlueprintOutput from phys2bids.slice4phys import slice4phys +from . import __version__ +from .due import due, Doi + LGR = logging.getLogger(__name__) @@ -111,16 +115,27 @@ def print_json(outfile, samp_freq, time_offset, ch_name): utils.writejson(outfile, summary, indent=4, sort_keys=False) +@due.dcite( + Doi('10.5281/zenodo.3470091'), + path='phys2bids', + description='Conversion of physiological trace data to BIDS format', + version=__version__, + cite_module=True) +@due.dcite( + Doi('10.1038/sdata.2016.44'), + path='phys2bids', + description='The BIDS specification', + cite_module=True) def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, sub=None, ses=None, chtrig=0, chsel=None, num_timepoints_expected=None, - tr=None, thr=None, pad=9, ch_name=[], chplot='', debug=False, quiet=False): + tr=None, thr=None, pad=9, ch_name=[], yml='', debug=False, quiet=False): """ Run main workflow of phys2bids. Runs the parser, does some checks on input, then imports the right interface file to read the input. If only info is required, it returns a summary onscreen. - Otherwise, it operates on the input to return a .tsv.gz file, possibily + Otherwise, it operates on the input to return a .tsv.gz file, possibly in BIDS format. Raises @@ -132,12 +147,17 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, # #!# This can probably be done while parsing? outdir = utils.check_input_dir(outdir) utils.path_exists_or_make_it(outdir) - + utils.path_exists_or_make_it(os.path.join(outdir, 'code')) + conversion_path = os.path.join(outdir, 'code/conversion') + utils.path_exists_or_make_it(conversion_path) + # generate extra path + extra_dir = os.path.join(outdir, 'bids_ignore') + utils.path_exists_or_make_it(extra_dir) # Create logfile name basename = 'phys2bids_' extension = 'tsv' isotime = datetime.datetime.now().strftime('%Y-%m-%dT%H%M%S') - logname = os.path.join(outdir, (basename + isotime + '.' + extension)) + logname = os.path.join(conversion_path, (basename + isotime + '.' + extension)) # Set logging format log_formatter = logging.Formatter( @@ -163,6 +183,13 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, LGR.info(f'Currently running phys2bids version {version_number}') LGR.info(f'Input file is {filename}') + # Save call.sh + arg_str = ' '.join(sys.argv[1:]) + call_str = f'phys2bids {arg_str}' + f = open(os.path.join(conversion_path, 'call.sh'), "a") + f.write(f'#!bin/bash \n{call_str}') + f.close() + # Check options to make them internally coherent pt. II # #!# This can probably be done while parsing? indir = utils.check_input_dir(indir) @@ -202,14 +229,13 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, LGR.info('Checking that units of measure are BIDS compatible') for index, unit in enumerate(phys_in.units): - phys_in.units[index] = bidsify_units(unit) + phys_in.units[index] = bids.bidsify_units(unit) LGR.info('Reading infos') phys_in.print_info(filename) # #!# Here the function viz.plot_channel should be called - if chplot != '' or info: - viz.plot_all(phys_in.ch_name, phys_in.timeseries, phys_in.units, - phys_in.freq, infile, chplot) + viz.plot_all(phys_in.ch_name, phys_in.timeseries, phys_in.units, + phys_in.freq, infile, conversion_path) # If only info were asked, end here. if info: return @@ -335,6 +361,16 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, if heur_file is not None and sub is not None: 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. + bids.participants_file(outdir, yml, sub) + # Generate dataset_description.json file if it doesn't exist already. + bids.dataset_description_file(outdir) + # Generate README file if it doesn't exist already. + bids.readme_file(outdir) + cp(heur_file, os.path.join(conversion_path, + os.path.splitext(os.path.basename(heur_file))[0] + '.py')) elif heur_file is not None and sub is None: LGR.warning('While "-heur" was specified, option "-sub" was not.\n' 'Skipping BIDS formatting.') @@ -352,7 +388,7 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, if freq_amount > 1: heur_args['record_label'] = f'freq{uniq_freq}' - phys_out[key].filename = use_heuristic(**heur_args) + phys_out[key].filename = bids.use_heuristic(**heur_args) # If any filename exists already because of multirun, append labels # But warn about the non-validity of this BIDS-like name. @@ -385,7 +421,8 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, phys_out[key].ch_name) print_summary(filename, num_timepoints_expected, phys_in[run].num_timepoints_found, uniq_freq, - phys_out[key].start_time, phys_out[key].filename) + phys_out[key].start_time, os.path.join(conversion_path, + phys_out[key].filename)) def _main(argv=None): @@ -394,7 +431,7 @@ def _main(argv=None): if __name__ == '__main__': - _main() + _main(sys.argv[1:]) """ Copyright 2019, The Phys2BIDS community. diff --git a/phys2bids/tests/test_bids.py b/phys2bids/tests/test_bids.py index 828bcd26b..8ba8ecadd 100644 --- a/phys2bids/tests/test_bids.py +++ b/phys2bids/tests/test_bids.py @@ -3,7 +3,7 @@ import pytest -from phys2bids.bids import bidsify_units, use_heuristic +from phys2bids.bids import bidsify_units, use_heuristic, readme_file, dataset_description_file from phys2bids.bids import UNIT_ALIASES @@ -58,3 +58,17 @@ def test_use_heuristic(tmpdir, test_sub, test_ses): f'_task-test_rec-biopac_run-01_recording-test_physio') assert os.path.normpath(test_result) == os.path.normpath(str(heur_path)) + + +@pytest.mark.parametrize('outdir', '.') +def test_README_file(outdir): + readme_file(outdir) + assert os.path.join(outdir, "README.md") + os.remove(os.path.join(outdir, "README.md")) + + +@pytest.mark.parametrize('outdir', '.') +def test_dataset_description_file(outdir): + dataset_description_file(outdir) + assert os.path.join(outdir, "dataset_description.json") + os.remove(os.path.join(outdir, "dataset_description.json")) diff --git a/phys2bids/tests/test_integration.py b/phys2bids/tests/test_integration.py index 25dc0bada..6acf7764d 100644 --- a/phys2bids/tests/test_integration.py +++ b/phys2bids/tests/test_integration.py @@ -3,7 +3,9 @@ import math import os import re +import shutil import subprocess +from csv import reader from pkg_resources import resource_filename from phys2bids._version import get_versions @@ -32,14 +34,17 @@ def test_logger(multifreq_lab_file): test_chtrig = 3 test_ntp = 1 test_outdir = test_path - + conversion_path = os.path.join(test_path, 'code/conversion') # Phys2bids call through terminal subprocess.run(f'phys2bids -in {test_filename} -indir {test_path} ' f'-chtrig {test_chtrig} -ntp {test_ntp} -outdir {test_outdir}', shell=True, check=True) + assert os.path.isfile(os.path.join(conversion_path, 'call.sh')) + # Read logger file - logger_file = sorted(glob.glob(os.path.join(test_path, '*phys2bids*')))[-1] + logger_file = glob.glob(os.path.join(conversion_path, '*phys2bids*'))[0] + # logger_file = sorted(glob.glob(os.path.join(test_path, '*phys2bids*')))[-1] with open(logger_file) as logger_info: logger_info = logger_info.readlines() @@ -49,6 +54,7 @@ def test_logger(multifreq_lab_file): # Removes generated files os.remove(os.path.join(test_path, logger_file)) + shutil.rmtree(conversion_path) def test_integration_txt(samefreq_short_txt_file): @@ -58,16 +64,22 @@ def test_integration_txt(samefreq_short_txt_file): test_path, test_filename = os.path.split(samefreq_short_txt_file) test_chtrig = 2 + conversion_path = os.path.join(test_path, 'code', 'conversion') phys2bids(filename=test_filename, indir=test_path, outdir=test_path, chtrig=test_chtrig, num_timepoints_expected=1, tr=1) # Check that files are generated - for suffix in ['.log', '.json', '.tsv.gz', '_trigger_time.png']: + for suffix in ['.json', '.tsv.gz']: assert os.path.isfile(os.path.join(test_path, 'Test_belt_pulse_samefreq_short' + suffix)) + # Check files in extra are generated + for suffix in ['.log', '_trigger_time.png']: + assert os.path.isfile(os.path.join(conversion_path, + 'Test_belt_pulse_samefreq_short' + suffix)) + # Read log file (note that this file is not the logger file) - with open(os.path.join(test_path, 'Test_belt_pulse_samefreq_short.log')) as log_info: + with open(os.path.join(conversion_path, 'Test_belt_pulse_samefreq_short.log')) as log_info: log_info = log_info.readlines() # Check timepoints expected @@ -95,6 +107,7 @@ def test_integration_txt(samefreq_short_txt_file): os.remove(filename) for filename in glob.glob(os.path.join(test_path, 'Test_belt_pulse_samefreq_short*')): os.remove(filename) + shutil.rmtree(conversion_path) def test_integration_acq(samefreq_full_acq_file): @@ -104,16 +117,21 @@ def test_integration_acq(samefreq_full_acq_file): test_path, test_filename = os.path.split(samefreq_full_acq_file) test_chtrig = 3 + conversion_path = os.path.join(test_path, 'code', 'conversion') phys2bids(filename=test_filename, indir=test_path, outdir=test_path, chtrig=test_chtrig, num_timepoints_expected=1, tr=1) # Check that files are generated - for suffix in ['.log', '.json', '.tsv.gz', '_trigger_time.png']: + for suffix in ['.json', '.tsv.gz']: assert os.path.isfile(os.path.join(test_path, 'Test_belt_pulse_samefreq' + suffix)) + # Check files in extra are generated + for suffix in ['.log', '_trigger_time.png']: + assert os.path.isfile(os.path.join(conversion_path, 'Test_belt_pulse_samefreq' + suffix)) + # Read log file (note that this file is not the logger file) - with open(os.path.join(test_path, 'Test_belt_pulse_samefreq.log')) as log_info: + with open(os.path.join(conversion_path, 'Test_belt_pulse_samefreq.log')) as log_info: log_info = log_info.readlines() # Check timepoints expected @@ -138,10 +156,11 @@ def test_integration_acq(samefreq_full_acq_file): 'MR TRIGGER - Custom, HLT100C - A 5', 'PPG100C', 'CO2', 'O2'] # Remove generated files - for filename in glob.glob(os.path.join(test_path, 'phys2bids*')): + for filename in glob.glob(os.path.join(conversion_path, 'phys2bids*')): os.remove(filename) for filename in glob.glob(os.path.join(test_path, 'Test_belt_pulse_samefreq*')): os.remove(filename) + shutil.rmtree(conversion_path) def test_integration_multifreq(multifreq_lab_file): @@ -151,30 +170,35 @@ def test_integration_multifreq(multifreq_lab_file): test_path, test_filename = os.path.split(multifreq_lab_file) test_chtrig = 3 + conversion_path = os.path.join(test_path, 'code', 'conversion') phys2bids(filename=test_filename, indir=test_path, outdir=test_path, chtrig=test_chtrig, num_timepoints_expected=1, tr=1) # Check that files are generated - for suffix in ['.log', '.json', '.tsv.gz']: + for suffix in ['.json', '.tsv.gz']: assert os.path.isfile(os.path.join(test_path, 'Test1_multifreq_onescan_40.0' + suffix)) - for suffix in ['.log', '.json', '.tsv.gz']: + for suffix in ['.json', '.tsv.gz']: assert os.path.isfile(os.path.join(test_path, 'Test1_multifreq_onescan_100.0' + suffix)) - for suffix in ['.log', '.json', '.tsv.gz']: + for suffix in ['.json', '.tsv.gz']: assert os.path.isfile(os.path.join(test_path, 'Test1_multifreq_onescan_500.0' + suffix)) - for suffix in ['.log', '.json', '.tsv.gz']: + for suffix in ['.json', '.tsv.gz']: assert os.path.isfile(os.path.join(test_path, 'Test1_multifreq_onescan_1000.0' + suffix)) - assert os.path.isfile(os.path.join(test_path, 'Test1_multifreq_onescan_trigger_time.png')) + for freq in ['40', '100', '500', '1000']: + assert os.path.isfile(os.path.join(conversion_path, + 'Test1_multifreq_onescan_' + freq + '.log')) + assert os.path.isfile(os.path.join(conversion_path, + 'Test1_multifreq_onescan_trigger_time.png')) """ Checks 40 Hz output """ # Read log file of frequency 625 (note that this file is not the logger file) - with open(os.path.join(test_path, 'Test1_multifreq_onescan_40.0.log')) as log_info: + with open(os.path.join(conversion_path, 'Test1_multifreq_onescan_40.log')) as log_info: log_info = log_info.readlines() # Check timepoints expected @@ -201,7 +225,7 @@ def test_integration_multifreq(multifreq_lab_file): Checks 100 Hz output """ # Read log file of frequency 625 (note that this file is not the logger file) - with open(os.path.join(test_path, 'Test1_multifreq_onescan_100.0.log')) as log_info: + with open(os.path.join(conversion_path, 'Test1_multifreq_onescan_100.log')) as log_info: log_info = log_info.readlines() # Check timepoints expected @@ -228,7 +252,7 @@ def test_integration_multifreq(multifreq_lab_file): Checks 500 Hz output """ # Read log file of frequency 625 (note that this file is not the logger file) - with open(os.path.join(test_path, 'Test1_multifreq_onescan_500.0.log')) as log_info: + with open(os.path.join(conversion_path, 'Test1_multifreq_onescan_500.log')) as log_info: log_info = log_info.readlines() # Check timepoints expected @@ -252,10 +276,10 @@ def test_integration_multifreq(multifreq_lab_file): assert json_data['Columns'] == ['Belt'] """ - Checks 100 Hz output + Checks 1000 Hz output """ # Read log file of frequency 625 (note that this file is not the logger file) - with open(os.path.join(test_path, 'Test1_multifreq_onescan_1000.0.log')) as log_info: + with open(os.path.join(conversion_path, 'Test1_multifreq_onescan_1000.log')) as log_info: log_info = log_info.readlines() # Check timepoints expected @@ -279,10 +303,11 @@ def test_integration_multifreq(multifreq_lab_file): assert json_data['Columns'] == ['time', 'Trigger'] # Remove generated files - for filename in glob.glob(os.path.join(test_path, 'phys2bids*')): + for filename in glob.glob(os.path.join(conversion_path, 'phys2bids*')): os.remove(filename) for filename in glob.glob(os.path.join(test_path, 'Test_belt_pulse_multifreq*')): os.remove(filename) + shutil.rmtree(conversion_path) def test_integration_heuristic(samefreq_short_txt_file): @@ -294,6 +319,7 @@ def test_integration_heuristic(samefreq_short_txt_file): test_full_path = os.path.join(test_path, test_filename) test_chtrig = 1 test_outdir = test_path + conversion_path = os.path.join(test_path, 'code', 'conversion') test_ntp = 158 test_tr = 1.2 test_thr = 0.735 @@ -303,16 +329,16 @@ def test_integration_heuristic(samefreq_short_txt_file): num_timepoints_expected=test_ntp, tr=test_tr, thr=test_thr, sub='006', ses='01', heur_file=test_heur) - test_path_output = os.path.join(test_path, 'sub-006/ses-01/func') + test_path_output = os.path.join(test_path, 'sub-006', 'ses-01', 'func') # Check that files are generated base_filename = 'sub-006_ses-01_task-test_rec-biopac_run-01_physio' - for suffix in ['.log', '.json', '.tsv.gz']: + for suffix in ['.json', '.tsv.gz']: assert os.path.isfile(os.path.join(test_path_output, base_filename + suffix)) - + assert os.path.isfile(os.path.join(conversion_path, base_filename + '.log')) # Read log file (note that this file is not the logger file) log_filename = 'sub-006_ses-01_task-test_rec-biopac_run-01_physio.log' - with open(os.path.join(test_path_output, log_filename)) as log_info: + with open(os.path.join(conversion_path, log_filename)) as log_info: log_info = log_info.readlines() # Check timepoints expected @@ -336,13 +362,27 @@ 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*')): + for filename in glob.glob(os.path.join(conversion_path, 'phys2bids*')): os.remove(filename) for filename in glob.glob(os.path.join(test_path, 'Test_belt_pulse_samefreq*')): os.remove(filename) for filename in glob.glob(os.path.join(test_path_output, '*')): os.remove(filename) + shutil.rmtree(conversion_path) def test_integration_info(samefreq_short_txt_file): @@ -356,7 +396,7 @@ def test_integration_info(samefreq_short_txt_file): test_ntp = 158 test_tr = 1.2 test_thr = 0.735 - + conversion_path = os.path.join(test_path, 'code/conversion') # Move into folder subprocess.run(f'cd {test_path}', shell=True, check=True) # Phys2bids call through terminal @@ -368,10 +408,11 @@ def test_integration_info(samefreq_short_txt_file): subprocess.run(command_str, shell=True, check=True) # Check that plot all file is generated - assert os.path.isfile('Test_belt_pulse_samefreq_short.png') + assert os.path.isfile(os.path.join(conversion_path, + 'Test_belt_pulse_samefreq_short.png')) # Read logger file - logger_file = glob.glob(os.path.join(test_path, '*phys2bids*'))[0] + logger_file = glob.glob(os.path.join(conversion_path, '*phys2bids*'))[0] with open(logger_file) as logger_info: logger_info = logger_info.readlines() @@ -380,5 +421,6 @@ def test_integration_info(samefreq_short_txt_file): '02. MR TRIGGER - Custom, HLT100C - A 5; sampled at', '10000.0') # Remove generated files - for filename in glob.glob(os.path.join(test_path, 'phys2bids*')): + for filename in glob.glob(os.path.join(conversion_path, 'phys2bids*')): os.remove(filename) + shutil.rmtree(conversion_path) diff --git a/phys2bids/tests/test_viz.py b/phys2bids/tests/test_viz.py index 65b444af7..d7109508b 100644 --- a/phys2bids/tests/test_viz.py +++ b/phys2bids/tests/test_viz.py @@ -8,10 +8,10 @@ def test_plot_all(samefreq_full_acq_file): chtrig = 3 test_path, test_filename = os.path.split(samefreq_full_acq_file) phys_obj = acq.populate_phys_input(samefreq_full_acq_file, chtrig) - out = os.path.join(test_path, 'Test_belt_pulse_samefreq.png') viz.plot_all(phys_obj.ch_name, phys_obj.timeseries, phys_obj.units, - phys_obj.freq, test_filename, outfile=out) - assert os.path.isfile(out) + phys_obj.freq, test_filename, outfile=test_path) + assert os.path.isfile(os.path.join(test_path, + os.path.splitext(os.path.basename(test_filename))[0] + '.png')) def test_plot_trigger(samefreq_full_acq_file): 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/phys2bids/viz.py b/phys2bids/viz.py index 7ae53872c..0cb0938e6 100644 --- a/phys2bids/viz.py +++ b/phys2bids/viz.py @@ -179,7 +179,7 @@ def export_trigger_plot(phys_in, chtrig, fileprefix, tr, num_timepoints_expected filename, figsize, dpi) -def plot_all(ch_name, timeseries, units, freq, infile, outfile='', dpi=SET_DPI, size=FIGSIZE): +def plot_all(ch_name, timeseries, units, freq, infile, outfile, dpi=SET_DPI, size=FIGSIZE): """ Plot all the channels for visualizations and saves them in outfile. @@ -230,7 +230,6 @@ def plot_all(ch_name, timeseries, units, freq, infile, outfile='', dpi=SET_DPI, ax[row].xlim = 30 * 60 * freq[0] # maximum display of half an hour ax[row].grid() ax[row].set_xlabel('seconds') - if outfile == '': - outfile = os.path.splitext(os.path.basename(infile))[0] + '.png' + outfile = os.path.join(outfile, os.path.splitext(os.path.basename(infile))[0] + '.png') LGR.info(f'saving channel plot to {outfile}') fig.savefig(outfile, dpi=dpi, bbox_inches='tight') 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..a941818ba 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 @@ -32,8 +33,10 @@ packages = find: include_package_data = True [options.extras_require] -acq = +acq = bioread >=1.0.5 +duecredit = + duecredit doc = sphinx >=2.0 sphinx-argparse @@ -44,11 +47,12 @@ style = test = pytest >=5.3 pytest-cov -interfaces = +interfaces = %(acq)s all = - %(interfaces)s %(doc)s + %(duecredit)s + %(interfaces)s %(style)s %(test)s