Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into enh/split4phys
Browse files Browse the repository at this point in the history
  • Loading branch information
Stefano Moia committed Jun 18, 2020
2 parents cc7bacf + 8d42c28 commit 68a77da
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 54 deletions.
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://duecredit.org>`_.


Contents
--------
Expand Down
114 changes: 114 additions & 0 deletions phys2bids/bids.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand Down Expand Up @@ -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)
7 changes: 4 additions & 3 deletions phys2bids/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
69 changes: 69 additions & 0 deletions phys2bids/due.py
Original file line number Diff line number Diff line change
@@ -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:
5 changes: 5 additions & 0 deletions phys2bids/heuristics/participant.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
participant:
participant_id: # Required
age: n/a
sex: n/a
handedness: n/a
65 changes: 51 additions & 14 deletions phys2bids/phys2bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.')
Expand All @@ -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.
Expand Down Expand Up @@ -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):
Expand All @@ -394,7 +431,7 @@ def _main(argv=None):


if __name__ == '__main__':
_main()
_main(sys.argv[1:])

"""
Copyright 2019, The Phys2BIDS community.
Expand Down
Loading

0 comments on commit 68a77da

Please sign in to comment.