From ab66ba82985e05da0e3ab4cbdd41d3586a772c14 Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 11 Nov 2020 15:33:28 +0100 Subject: [PATCH 01/26] Add first skeleton draft --- phys2denoise.py | 404 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 phys2denoise.py diff --git a/phys2denoise.py b/phys2denoise.py new file mode 100644 index 0000000..6bf7a47 --- /dev/null +++ b/phys2denoise.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 + +""" +Phys2denoise is a python3 library meant to prepare physiological regressors for fMRI denoising. + +The project is under development. + +Copyright 2020, The Phys2BIDS community. +Please scroll to bottom to read full license. + +""" + +import datetime +import logging +import os +import sys +from copy import deepcopy +from shutil import copy as cp + +import numpy as np + +from phys2denoise import utils, viz, _version +from phys2denoise.cli.run import _get_parser +# from phys2denoise.metrics import cardiac, chest_belt, retroicor + +from . import __version__ +# from .due import due, Doi + +LGR = logging.getLogger(__name__) + + +def print_json(outfile, samp_freq, time_offset, ch_name): + """ + Print the json required by BIDS format. + + Parameters + ---------- + outfile: str or path + Fullpath to output file. + samp_freq: float + Frequency of sampling for the output file. + time_offset: float + Difference between beginning of file and first TR. + ch_name: list of str + List of channel names, as specified by BIDS format. + + Notes + ----- + Outcome: + outfile: .json file + File containing information for BIDS. + """ + start_time = -time_offset + summary = dict(SamplingFrequency=samp_freq, + StartTime=round(start_time, 4), + Columns=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=1, chsel=None, num_timepoints_expected=None, + 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, possibly + in BIDS format. + + Raises + ------ + NotImplementedError + If the file extension is not supported yet. + """ + # Check options to make them internally coherent pt. I + # #!# 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) + + # Create logfile name + basename = 'phys2bids_' + extension = 'tsv' + isotime = datetime.datetime.now().strftime('%Y-%m-%dT%H%M%S') + logname = os.path.join(conversion_path, (basename + isotime + '.' + extension)) + + # Set logging format + log_formatter = logging.Formatter( + '%(asctime)s\t%(name)-12s\t%(levelname)-8s\t%(message)s', + datefmt='%Y-%m-%dT%H:%M:%S') + + # Set up logging file and open it for writing + log_handler = logging.FileHandler(logname) + log_handler.setFormatter(log_formatter) + sh = logging.StreamHandler() + + if quiet: + logging.basicConfig(level=logging.WARNING, + handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') + elif debug: + logging.basicConfig(level=logging.DEBUG, + handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') + else: + logging.basicConfig(level=logging.INFO, + handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') + + version_number = _version.get_versions()['version'] + 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) + if chtrig < 1: + raise Exception('Wrong trigger channel. Channel indexing starts with 1!') + + filename, ftype = utils.check_input_type(filename, + indir) + + if heur_file: + heur_file = utils.check_input_ext(heur_file, '.py') + utils.check_file_exists(heur_file) + + infile = os.path.join(indir, filename) + utils.check_file_exists(infile) + + if isinstance(num_timepoints_expected, int): + num_timepoints_expected = [num_timepoints_expected] + if isinstance(tr, (int, float)): + tr = [tr] + + if tr is not None and num_timepoints_expected is not None: + # If tr and ntp were specified, check that tr is either length one or ntp. + if len(num_timepoints_expected) != len(tr) and len(tr) != 1: + raise Exception('Number of sequence types listed with TR ' + 'doesn\'t match expected number of runs in ' + 'the session') + + # Read file! + if ftype == 'acq': + from phys2bids.interfaces.acq import populate_phys_input + elif ftype == 'txt': + from phys2bids.interfaces.txt import populate_phys_input + + LGR.info(f'Reading the file {infile}') + phys_in = populate_phys_input(infile, chtrig) + + LGR.info('Checking that units of measure are BIDS compatible') + for index, unit in enumerate(phys_in.units): + 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 + 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 + + # The next few lines remove the undesired channels from phys_in. + if chsel: + LGR.info('Dropping unselected channels') + for i in reversed(range(0, phys_in.ch_amount)): + if i not in chsel: + phys_in.delete_at_index(i) + + # If requested, change channel names. + if ch_name: + LGR.info('Renaming channels with given names') + phys_in.rename_channels(ch_name) + + # Checking acquisition type via user's input + if tr is not None and num_timepoints_expected is not None: + + # Multi-run acquisition type section + # Check list length, more than 1 means multi-run + if len(num_timepoints_expected) > 1: + # if multi-run of same sequence type, pad list with ones + # and multiply array with user's input + if len(tr) == 1: + tr = np.ones(len(num_timepoints_expected)) * tr[0] + + # Sum of values in ntp_list should be equivalent to num_timepoints_found + phys_in.check_trigger_amount(thr=thr, + num_timepoints_expected=sum(num_timepoints_expected), + tr=1) + + # Check that sum of tp expected is equivalent to num_timepoints_found, + # if it passes call slice4phys + if phys_in.num_timepoints_found != sum(num_timepoints_expected): + raise Exception('The number of triggers found is different ' + 'than expected. Better stop now than break ' + 'something.') + + # slice the recording based on user's entries + # !!! ATTENTION: PHYS_IN GETS OVERWRITTEN AS DICTIONARY + phys_in = slice4phys(phys_in, num_timepoints_expected, tr, + phys_in.thr, pad) + # returns a dictionary in the form {run_idx: phys_in[startpoint, endpoint]} + + # save a figure for each run | give the right acquisition parameters for runs + fileprefix = os.path.join(conversion_path, + os.path.splitext(os.path.basename(filename))[0]) + for i, run in enumerate(phys_in.keys()): + plot_fileprefix = f'{fileprefix}_{run}' + viz.export_trigger_plot(phys_in[run], chtrig, plot_fileprefix, tr[i], + num_timepoints_expected[i], filename, + sub, ses) + + # Single run acquisition type, or : nothing to split workflow + else: + # Run analysis on trigger channel to get first timepoint + # and the time offset. + phys_in.check_trigger_amount(thr, num_timepoints_expected[0], tr[0]) + # save a figure of the trigger + fileprefix = os.path.join(conversion_path, + os.path.splitext(os.path.basename(filename))[0]) + viz.export_trigger_plot(phys_in, chtrig, fileprefix, tr[0], + num_timepoints_expected[0], filename, + sub, ses) + + # Reassign phys_in as dictionary + # !!! ATTENTION: PHYS_IN GETS OVERWRITTEN AS DICTIONARY + phys_in = {1: phys_in} + + else: + LGR.warning('Skipping trigger pulse count. If you want to run it, ' + 'call phys2bids using both "-ntp" and "-tr" arguments') + # !!! ATTENTION: PHYS_IN GETS OVERWRITTEN AS DICTIONARY + phys_in = {1: phys_in} + + # The next few lines create a dictionary of different BlueprintInput + # objects, one for each unique frequency for each run in phys_in + # they also save the amount of runs and unique frequencies + run_amount = len(phys_in) + uniq_freq_list = set(phys_in[1].freq) + freq_amount = len(uniq_freq_list) + if freq_amount > 1: + LGR.info(f'Found {freq_amount} different frequencies in input!') + + if run_amount > 1: + LGR.info(f'Found {run_amount} different scans in input!') + + LGR.info(f'Preparing {freq_amount*run_amount} output files.') + # Create phys_out dict that will have a blueprint object for each different frequency + phys_out = {} + + if heur_file is not None and sub is not None: + LGR.info(f'Preparing BIDS output using {heur_file}') + # If heuristics are used, init a dict of arguments to pass to use_heuristic + heur_args = {'heur_file': heur_file, 'sub': sub, 'ses': ses, + 'filename': filename, 'outdir': outdir, 'run': '', + 'record_label': ''} + # 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.') + + # Export a (set of) phys_out for each element in phys_in + # run keys start from 1 (human friendly) + for run in phys_in.keys(): + for uniq_freq in uniq_freq_list: + # Initialise the key for the (possibly huge amount of) dictionary entries + key = f'{run}_{uniq_freq}' + # copy the phys_in object to the new dict entry + phys_out[key] = deepcopy(phys_in[run]) + # this counter will take into account how many channels are eliminated + count = 0 + # for each channel in the original phys_in object + # take the frequency + for idx, i in enumerate(phys_in[run].freq): + # if that frequency is different than the frequency of the phys_obj entry + if i != uniq_freq: + # eliminate that channel from the dict since we only want channels + # with the same frequency + phys_out[key].delete_at_index(idx - count) + # take into acount the elimination so in the next eliminated channel we + # eliminate correctly + count += 1 + # Also create a BlueprintOutput object for each unique frequency found. + # Populate it with the corresponding blueprint input and replace it + # in the dictionary. + # Add time channel in the proper frequency. + if uniq_freq != phys_in[run].freq[0]: + phys_out[key].ch_name.insert(0, phys_in[run].ch_name[0]) + phys_out[key].units.insert(0, phys_in[run].units[0]) + phys_out[key].timeseries.insert(0, np.linspace(phys_in[run].timeseries[0][0], + phys_in[run].timeseries[0][-1], + num=phys_out[key].timeseries[0].shape[0])) + # Add trigger channel in the proper frequency. + if uniq_freq != phys_in[run].freq[chtrig]: + phys_out[key].ch_name.insert(1, phys_in[run].ch_name[chtrig]) + phys_out[key].units.insert(1, phys_in[run].units[chtrig]) + phys_out[key].timeseries.insert(1, np.interp(phys_out[key].timeseries[0], + phys_in[run].timeseries[0], + phys_in[run].timeseries[chtrig])) + phys_out[key] = BlueprintOutput.init_from_blueprint(phys_out[key]) + + # Preparing output parameters: name and folder. + for uniq_freq in uniq_freq_list: + key = f'{run}_{uniq_freq}' + # If possible, prepare bids renaming. + if heur_file is not None and sub is not None: + # Add run info to heur_args if more than one run is present + if run_amount > 1: + heur_args['run'] = f'{run:02d}' + + # Append "recording-freq" to filename if more than one freq + if freq_amount > 1: + heur_args['record_label'] = f'{uniq_freq:.0f}Hz' + + 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. + if run_amount > 1: + if any([phys.filename == phys_out[key].filename + for phys in phys_out.values()]): + phys_out[key].filename = (f'{phys_out[key].filename}' + f'_take-{run}') + LGR.warning('Identified multiple outputs with the same name.\n' + 'Appending fake label to avoid overwriting.\n' + '!!! ATTENTION !!! the output is not BIDS compliant.\n' + 'Please check heuristics to solve the problem.') + + else: + phys_out[key].filename = os.path.join(outdir, + os.path.splitext(os.path.basename(filename) + )[0]) + # Append "run" to filename if more than one run + if run_amount > 1: + phys_out[key].filename = f'{phys_out[key].filename}_{run:02d}' + # Append "freq" to filename if more than one freq + if freq_amount > 1: + phys_out[key].filename = f'{phys_out[key].filename}_{uniq_freq:.0f}Hz' + + LGR.info(f'Exporting files for run {run} freq {uniq_freq}') + np.savetxt(phys_out[key].filename + '.tsv.gz', + phys_out[key].timeseries, fmt='%.8e', delimiter='\t') + print_json(phys_out[key].filename, phys_out[key].freq, + phys_out[key].start_time, phys_out[key].ch_name) + print_summary(filename, num_timepoints_expected, + phys_in[run].num_timepoints_found, uniq_freq, + phys_out[key].start_time, + os.path.join(conversion_path, + os.path.splitext(os.path.basename(phys_out[key].filename) + )[0])) + + +def _main(argv=None): + options = _get_parser().parse_args(argv) + phys2bids(**vars(options)) + + +if __name__ == '__main__': + _main(sys.argv[1:]) + +""" +Copyright 2019, The Phys2BIDS community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" From 5f685eb7f73a7b62c976fd99c5e4c89738c5e5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A9s=20Chavarr=C3=ADa?= <72545702+ineschh@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:53:34 +0100 Subject: [PATCH 02/26] add parser --- phys2denoise/cli/run.py | 196 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 phys2denoise/cli/run.py diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py new file mode 100644 index 0000000..7f79b40 --- /dev/null +++ b/phys2denoise/cli/run.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +"""Parser for phys2denoise.""" + + +import argparse + +from phys2denoise import __version__ + + +def _get_parser(): + """ + Parse command line inputs for this function. + + Returns + ------- + parser.parse_args() : argparse dict + + Notes + ----- + # Argument parser follow template provided by RalphyZ. + # https://stackoverflow.com/a/43456577 + """ + parser = argparse.ArgumentParser() + optional = parser._action_groups.pop() + metric = parser._action_groups.pop() + required = parser.add_argument_group('Required Argument:') + required.add_argument('-in', '--input-file', + dest='filename', + type=str, + help='Path/name of the file containing physiological ' + 'data, with or without extension.', + required=True) + metric.add_argument('-crf', '--crf', + dest='metric_list', + action='append_const', + const='crf', + help='Cardiac response function. Needs the following ' + 'inputs:sr, os, tl, onset and tr.', + default=False) + metric.add_argument('-rpv', '--rpv', + dest='metric_list', + action='append_const', + const='rpv', + help='REspiratory pattern variability. Needs the following ' + 'inputs: bts and win.', + default=False) + metric.add_argument('-env', '--env', + dest='metric_list', + action='append_const', + const='env', + help='Respiratory pattern variability calculated across a sliding ' + 'window. Needs the following inputs: bts, sr, osr, win and lags.', + default=False) + metric.add_argument('-rv', '--rv', + dest='metric_list', + action='append_const', + const='rv', + help='Respiratory variance. Needs the following inputs: ' + 'bts, sr, osr, win and lags.', + default=False) + metric.add_argument('-rvt', '--rvt', + dest='metric_list', + action='append_const', + const='rvt', + help='Respiratory volume-per-time. Needs the following inputs: ' + 'bts, sr, osr, win and lags.', + default=False) + metric.add_argument('-rrf', '--rrf', + dest='metric_list', + action='append_const', + const='rrf', + help='Respiratory response function. Needs the following inputs: ' + 'sr, os, tl, onset and tr.', + default=False) + metric.add_argument('-rcard', '--retroicor-card', + dest='metric_list', + action='append_const', + const='r_card', + help='Computes regressors for cardiac signal. Needs the following ' + 'inputs: tr, nscans, slt and n_harm.', + default=False) + metric.add_argument('-rresp', '--retroicor-resp', + dest='metric_list', + action='append_const', + const='r_resp', + help='Computes regressors for respiratory signal. Needs the following ' + 'inputs: tr, nscans, slt and n_harm.', + default=False) + optional.add_argument('-outdir', '--output-dir', + dest='outdir', + type=str, + help='Folder where output should be placed. ' + 'Default is current folder.', + default='.') + optional.add_argument('-sr', '--sample-rate', + dest='sample_rate', + type=float, + help='Sampling rate of the physiological data in Hz.', + default=None) + optional.add_argument('-pk', '--peaks', + dest='peaks', + type=str, + help='Filename of the list with the indexed peaks\' positions' + ' of the physiological data.', + default=None) + optional.add_argument('-thr', '--throughts', + dest='throughts', + type=str, + help='Filename of the list with the indexed peaks\' positions' + ' of the physiological data.', + default=None) + optional.add_argument('-os', '--oversampling', + dest='oversampling', + type=int, + help='Temporal oversampling factor in seconds. ' + 'Default is 50.', + default=50) + optional.add_argument('-tl', '--time-length', + dest='time_length', + type=int, + help='RRF Kernel length in seconds.', + default=None) + optional.add_argument('-onset', '--onset', + dest='onset', + type=float, + help='Onset of the response in seconds. ' + 'Default is 0.', + default=0) + optional.add_argument('-tr', '--tr', + dest='tr', + type=float, + help='TR of sequence in seconds.', + default=None) + optional.add_argument('-bts', '--belt-ts', + dest='belt_ts', + type=str, + help='Filename of the 1D array containing the .' + 'respiratory belt time series.', + default=None) + optional.add_argument('-win', '--window', + dest='window', + type=int, + help='Size of the sliding window in seconds. ' + 'Default is 6 seconds.', + default=6) + optional.add_argument('-osr', '--out-samplerate', + dest='out_samplerate', + type=float, + help='Sampling rate for the output time series ' + 'in seconds. Corresponds to TR in fMRI data.', + default=None) + optional.add_argument('-lags', '--lags', + dest='lags', + nargs='*', + type=int, + action='append', + help='List of lags to apply to the rv estimate ' + 'in seconds.', + default=None) + optional.add_argument('-nscans', '--nscans', + dest='nscans', + type=int, + help='Number of scans. Default is 1.', + default=1) + optional.add_argument('-slt', '--slice-timings', + dest='slice_timings', + type=str, + help='Filename with the slice timings.', + default=None) + optional.add_argument('-nharm', '--number-harmonics', + dest='n_harm', + type=int, + help='Number of harmonics. ', + default=None) + optional.add_argument('-debug', '--debug', + dest='debug', + action='store_true', + help='Only print debugging info to log file. Default is False.', + default=False) + optional.add_argument('-quiet', '--quiet', + dest='quiet', + action='store_true', + help='Only print warnings to log file. Default is False.', + default=False) + optional.add_argument('-v', '--version', action='version', + version=('%(prog)s ' + __version__)) + + parser._action_groups.append(optional) + + return parser + + +if __name__ == '__main__': + raise RuntimeError('phys2denoise/cli/run.py should not be run directly;\n' + 'Please `pip install` phys2denoise and use the ' + '`phys2denoise` command') From 2fa576faf3afee16d97be66ed1ad987028a15238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A9s=20Chavarr=C3=ADa?= <72545702+ineschh@users.noreply.github.com> Date: Thu, 12 Nov 2020 19:09:48 +0100 Subject: [PATCH 03/26] Update phys2denoise/cli/run.py Co-authored-by: Stefano Moia --- phys2denoise/cli/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index 7f79b40..d3f3384 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -41,7 +41,7 @@ def _get_parser(): dest='metric_list', action='append_const', const='rpv', - help='REspiratory pattern variability. Needs the following ' + help='Respiratory pattern variability. Needs the following ' 'inputs: bts and win.', default=False) metric.add_argument('-env', '--env', From 6142d30787c63fc5d9a090ef6beb34cc6d93ab49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A9s=20Chavarr=C3=ADa?= <72545702+ineschh@users.noreply.github.com> Date: Thu, 12 Nov 2020 20:31:26 +0100 Subject: [PATCH 04/26] Update --- phys2denoise/cli/run.py | 245 +++++++++++++++++++--------------------- 1 file changed, 115 insertions(+), 130 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index 7f79b40..bfee4ac 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -23,174 +23,159 @@ def _get_parser(): parser = argparse.ArgumentParser() optional = parser._action_groups.pop() metric = parser._action_groups.pop() - required = parser.add_argument_group('Required Argument:') - required.add_argument('-in', '--input-file', - dest='filename', + required = parser.add_argument_group("Required Argument:") + required.add_argument("-in", "--input-file", + dest="filename", type=str, - help='Path/name of the file containing physiological ' - 'data, with or without extension.', + help="Full path and name of the file containing " + "physiological data, with or without extension.", required=True) - metric.add_argument('-crf', '--crf', - dest='metric_list', - action='append_const', - const='crf', - help='Cardiac response function. Needs the following ' - 'inputs:sr, os, tl, onset and tr.', + metric.add_argument("-crf", "--cardiac-response-function", + dest="metrics", + action="append_const", + const="crf", + help="Cardiac response function. Needs the following " + "inputs:sample-rate, oversampling, time-length, " + "onset and tr.", default=False) - metric.add_argument('-rpv', '--rpv', - dest='metric_list', - action='append_const', - const='rpv', - help='REspiratory pattern variability. Needs the following ' - 'inputs: bts and win.', + metric.add_argument("-rpv", "--respiratory-pattern-variability", + dest="metrics", + action="append_const", + const="rpv", + help="Respiratory pattern variability. Needs the following " + "input: window.", default=False) - metric.add_argument('-env', '--env', - dest='metric_list', - action='append_const', - const='env', - help='Respiratory pattern variability calculated across a sliding ' - 'window. Needs the following inputs: bts, sr, osr, win and lags.', + metric.add_argument("-env", "--envelope", + dest="metrics", + action="append_const", + const="env", + help="Respiratory pattern variability calculated across a sliding " + "window. Needs the following inputs: sample-rate, window and lags.", default=False) - metric.add_argument('-rv', '--rv', - dest='metric_list', - action='append_const', - const='rv', - help='Respiratory variance. Needs the following inputs: ' - 'bts, sr, osr, win and lags.', + metric.add_argument("-rv", "--respiratory-variance", + dest="metrics", + action="append_const", + const="rv", + help="Respiratory variance. Needs the following inputs: " + "sample-rate, window and lags.", default=False) - metric.add_argument('-rvt', '--rvt', - dest='metric_list', - action='append_const', - const='rvt', - help='Respiratory volume-per-time. Needs the following inputs: ' - 'bts, sr, osr, win and lags.', + metric.add_argument("-rvt", "--respiratory-volume-per-time", + dest="metrics", + action="append_const", + const="rvt", + help="Respiratory volume-per-time. Needs the following inputs: " + "sample-rate, window and lags.", default=False) - metric.add_argument('-rrf', '--rrf', - dest='metric_list', - action='append_const', - const='rrf', - help='Respiratory response function. Needs the following inputs: ' - 'sr, os, tl, onset and tr.', + metric.add_argument("-rrf", "--respiratory-response-function", + dest="metrics", + action="append_const", + const="rrf", + help="Respiratory response function. Needs the following inputs: " + "sample-rate, oversampling, time-length, onset and tr.", default=False) - metric.add_argument('-rcard', '--retroicor-card', - dest='metric_list', - action='append_const', - const='r_card', - help='Computes regressors for cardiac signal. Needs the following ' - 'inputs: tr, nscans, slt and n_harm.', + metric.add_argument("-rcard", "--retroicor-card", + dest="metrics", + action="append_const", + const="r_card", + help="Computes regressors for cardiac signal. Needs the following " + "inputs: tr, nscans and n_harm.", default=False) - metric.add_argument('-rresp', '--retroicor-resp', - dest='metric_list', - action='append_const', - const='r_resp', - help='Computes regressors for respiratory signal. Needs the following ' - 'inputs: tr, nscans, slt and n_harm.', + metric.add_argument("-rresp", "--retroicor-resp", + dest="metrics", + action="append_const", + const="r_resp", + help="Computes regressors for respiratory signal. Needs the following " + "inputs: tr, nscans and n_harm.", default=False) - optional.add_argument('-outdir', '--output-dir', - dest='outdir', + optional.add_argument("-outdir", "--output-dir", + dest="outdir", type=str, - help='Folder where output should be placed. ' - 'Default is current folder.', - default='.') - optional.add_argument('-sr', '--sample-rate', - dest='sample_rate', + help="Folder where output should be placed. " + "Default is current folder.", + default=".") + optional.add_argument("-sr", "--sample-rate", + dest="sample_rate", type=float, - help='Sampling rate of the physiological data in Hz.', + help="Sampling rate of the physiological data in Hz.", default=None) - optional.add_argument('-pk', '--peaks', - dest='peaks', + optional.add_argument("-pk", "--peaks", + dest="peaks", type=str, - help='Filename of the list with the indexed peaks\' positions' - ' of the physiological data.', + help="Full path and filename of the list with the indexed peaks' " + "positions of the physiological data.", default=None) - optional.add_argument('-thr', '--throughts', - dest='throughts', + optional.add_argument("-tg", "--throughts", + dest="throughts", type=str, - help='Filename of the list with the indexed peaks\' positions' - ' of the physiological data.', + help="Full path and filename of the list with the indexed peaks' " + "positions of the physiological data.", default=None) - optional.add_argument('-os', '--oversampling', - dest='oversampling', + optional.add_argument("-os", "--oversampling", + dest="oversampling", type=int, - help='Temporal oversampling factor in seconds. ' - 'Default is 50.', + help="Temporal oversampling factor in seconds. " + "Default is 50.", default=50) - optional.add_argument('-tl', '--time-length', - dest='time_length', + optional.add_argument("-tl", "--time-length", + dest="time_length", type=int, - help='RRF Kernel length in seconds.', + help="RRF Kernel length in seconds.", default=None) - optional.add_argument('-onset', '--onset', - dest='onset', + optional.add_argument("-onset", "--onset", + dest="onset", type=float, - help='Onset of the response in seconds. ' - 'Default is 0.', + help="Onset of the response in seconds. " + "Default is 0.", default=0) - optional.add_argument('-tr', '--tr', - dest='tr', + optional.add_argument("-tr", "--tr", + dest="tr", type=float, - help='TR of sequence in seconds.', + help="TR of sequence in seconds.", default=None) - optional.add_argument('-bts', '--belt-ts', - dest='belt_ts', - type=str, - help='Filename of the 1D array containing the .' - 'respiratory belt time series.', - default=None) - optional.add_argument('-win', '--window', - dest='window', + optional.add_argument("-win", "--window", + dest="window", type=int, - help='Size of the sliding window in seconds. ' - 'Default is 6 seconds.', + help="Size of the sliding window in seconds. " + "Default is 6 seconds.", default=6) - optional.add_argument('-osr', '--out-samplerate', - dest='out_samplerate', - type=float, - help='Sampling rate for the output time series ' - 'in seconds. Corresponds to TR in fMRI data.', - default=None) - optional.add_argument('-lags', '--lags', - dest='lags', - nargs='*', + optional.add_argument("-lags", "--lags", + dest="lags", + nargs="*", type=int, - action='append', - help='List of lags to apply to the rv estimate ' - 'in seconds.', + action="append", + help="List of lags to apply to the RV estimate " + "in seconds.", default=None) - optional.add_argument('-nscans', '--nscans', - dest='nscans', + optional.add_argument("-nscans", "--number-scans", + dest="nscans", type=int, - help='Number of scans. Default is 1.', + help="Number of scans. Default is 1.", default=1) - optional.add_argument('-slt', '--slice-timings', - dest='slice_timings', - type=str, - help='Filename with the slice timings.', - default=None) - optional.add_argument('-nharm', '--number-harmonics', - dest='n_harm', + optional.add_argument("-nharm", "--number-harmonics", + dest="n_harm", type=int, - help='Number of harmonics. ', + help="Number of harmonics.", default=None) - optional.add_argument('-debug', '--debug', - dest='debug', - action='store_true', - help='Only print debugging info to log file. Default is False.', + optional.add_argument("-debug", "--debug", + dest="debug", + action="store_true", + help="Only print debugging info to log file. Default is False.", default=False) - optional.add_argument('-quiet', '--quiet', - dest='quiet', - action='store_true', - help='Only print warnings to log file. Default is False.', + optional.add_argument("-quiet", "--quiet", + dest="quiet", + action="store_true", + help="Only print warnings to log file. Default is False.", default=False) - optional.add_argument('-v', '--version', action='version', - version=('%(prog)s ' + __version__)) + optional.add_argument("-v", "--version", action="version", + version=("%(prog)s " + __version__)) parser._action_groups.append(optional) + parser._action_groups.append(metric) return parser -if __name__ == '__main__': - raise RuntimeError('phys2denoise/cli/run.py should not be run directly;\n' - 'Please `pip install` phys2denoise and use the ' - '`phys2denoise` command') +if __name__ == "__main__": + raise RuntimeError("phys2denoise/cli/run.py should not be run directly;\n" + "Please `pip install` phys2denoise and use the " + "`phys2denoise` command") From 8e521f3208ba8eea3b73a478b1c19e52e0f952d1 Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 13 Nov 2020 00:51:33 +0100 Subject: [PATCH 05/26] More skeleton --- phys2denoise.py | 314 ++++++------------------------------------------ 1 file changed, 36 insertions(+), 278 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 6bf7a47..17a6ed5 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -5,7 +5,7 @@ The project is under development. -Copyright 2020, The Phys2BIDS community. +Copyright 2020, The physiopy community. Please scroll to bottom to read full license. """ @@ -24,7 +24,7 @@ # from phys2denoise.metrics import cardiac, chest_belt, retroicor from . import __version__ -# from .due import due, Doi +from .due import due, Doi LGR = logging.getLogger(__name__) @@ -58,43 +58,32 @@ def print_json(outfile, samp_freq, time_offset, ch_name): @due.dcite( - Doi('10.5281/zenodo.3470091'), - path='phys2bids', - description='Conversion of physiological trace data to BIDS format', + Doi(''), + path='phys2denoise', + description='Creation of regressors for physiological denoising', 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=1, chsel=None, num_timepoints_expected=None, - tr=None, thr=None, pad=9, ch_name=[], yml='', debug=False, quiet=False): +def phys2denoise(filename, outdir='.', 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, possibly - in BIDS format. - - Raises - ------ - NotImplementedError - If the file extension is not supported yet. + Run main workflow of phys2denoise. + + Runs the parser, does some checks on input, then computes the required metrics. + + Notes + ----- + The code was greatly copied from phys2bids (copyright the physiopy community) + """ # Check options to make them internally coherent pt. I # #!# 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')) + outdir = os.path.abspath(outdir) + os.makedirs(outdir) + os.makedirs(os.path.join(outdir, 'code')) conversion_path = os.path.join(outdir, 'code', 'conversion') - utils.path_exists_or_make_it(conversion_path) + os.makedirs(conversion_path) # Create logfile name - basename = 'phys2bids_' + basename = 'phys2denoise_' extension = 'tsv' isotime = datetime.datetime.now().strftime('%Y-%m-%dT%H%M%S') logname = os.path.join(conversion_path, (basename + isotime + '.' + extension)) @@ -120,275 +109,44 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None, handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') version_number = _version.get_versions()['version'] - LGR.info(f'Currently running phys2bids version {version_number}') + LGR.info(f'Currently running phys2denoise 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}' + call_str = f'phys2denoise {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) - if chtrig < 1: - raise Exception('Wrong trigger channel. Channel indexing starts with 1!') - - filename, ftype = utils.check_input_type(filename, - indir) - - if heur_file: - heur_file = utils.check_input_ext(heur_file, '.py') - utils.check_file_exists(heur_file) - - infile = os.path.join(indir, filename) - utils.check_file_exists(infile) - - if isinstance(num_timepoints_expected, int): - num_timepoints_expected = [num_timepoints_expected] - if isinstance(tr, (int, float)): - tr = [tr] - - if tr is not None and num_timepoints_expected is not None: - # If tr and ntp were specified, check that tr is either length one or ntp. - if len(num_timepoints_expected) != len(tr) and len(tr) != 1: - raise Exception('Number of sequence types listed with TR ' - 'doesn\'t match expected number of runs in ' - 'the session') - - # Read file! - if ftype == 'acq': - from phys2bids.interfaces.acq import populate_phys_input - elif ftype == 'txt': - from phys2bids.interfaces.txt import populate_phys_input - - LGR.info(f'Reading the file {infile}') - phys_in = populate_phys_input(infile, chtrig) - - LGR.info('Checking that units of measure are BIDS compatible') - for index, unit in enumerate(phys_in.units): - 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 - 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 - - # The next few lines remove the undesired channels from phys_in. - if chsel: - LGR.info('Dropping unselected channels') - for i in reversed(range(0, phys_in.ch_amount)): - if i not in chsel: - phys_in.delete_at_index(i) - - # If requested, change channel names. - if ch_name: - LGR.info('Renaming channels with given names') - phys_in.rename_channels(ch_name) - - # Checking acquisition type via user's input - if tr is not None and num_timepoints_expected is not None: - - # Multi-run acquisition type section - # Check list length, more than 1 means multi-run - if len(num_timepoints_expected) > 1: - # if multi-run of same sequence type, pad list with ones - # and multiply array with user's input - if len(tr) == 1: - tr = np.ones(len(num_timepoints_expected)) * tr[0] - - # Sum of values in ntp_list should be equivalent to num_timepoints_found - phys_in.check_trigger_amount(thr=thr, - num_timepoints_expected=sum(num_timepoints_expected), - tr=1) - - # Check that sum of tp expected is equivalent to num_timepoints_found, - # if it passes call slice4phys - if phys_in.num_timepoints_found != sum(num_timepoints_expected): - raise Exception('The number of triggers found is different ' - 'than expected. Better stop now than break ' - 'something.') - - # slice the recording based on user's entries - # !!! ATTENTION: PHYS_IN GETS OVERWRITTEN AS DICTIONARY - phys_in = slice4phys(phys_in, num_timepoints_expected, tr, - phys_in.thr, pad) - # returns a dictionary in the form {run_idx: phys_in[startpoint, endpoint]} - - # save a figure for each run | give the right acquisition parameters for runs - fileprefix = os.path.join(conversion_path, - os.path.splitext(os.path.basename(filename))[0]) - for i, run in enumerate(phys_in.keys()): - plot_fileprefix = f'{fileprefix}_{run}' - viz.export_trigger_plot(phys_in[run], chtrig, plot_fileprefix, tr[i], - num_timepoints_expected[i], filename, - sub, ses) - - # Single run acquisition type, or : nothing to split workflow - else: - # Run analysis on trigger channel to get first timepoint - # and the time offset. - phys_in.check_trigger_amount(thr, num_timepoints_expected[0], tr[0]) - # save a figure of the trigger - fileprefix = os.path.join(conversion_path, - os.path.splitext(os.path.basename(filename))[0]) - viz.export_trigger_plot(phys_in, chtrig, fileprefix, tr[0], - num_timepoints_expected[0], filename, - sub, ses) - - # Reassign phys_in as dictionary - # !!! ATTENTION: PHYS_IN GETS OVERWRITTEN AS DICTIONARY - phys_in = {1: phys_in} + # filename, ftype = utils.check_input_type(filename) + + if not os.path.isfile(filename) and filename is not None: + raise FileNotFoundError(f'The file {filename} does not exist!') + + # Read input file + phys_in = np.genfromtxt(filename) + + + + + + - else: - LGR.warning('Skipping trigger pulse count. If you want to run it, ' - 'call phys2bids using both "-ntp" and "-tr" arguments') - # !!! ATTENTION: PHYS_IN GETS OVERWRITTEN AS DICTIONARY - phys_in = {1: phys_in} - - # The next few lines create a dictionary of different BlueprintInput - # objects, one for each unique frequency for each run in phys_in - # they also save the amount of runs and unique frequencies - run_amount = len(phys_in) - uniq_freq_list = set(phys_in[1].freq) - freq_amount = len(uniq_freq_list) - if freq_amount > 1: - LGR.info(f'Found {freq_amount} different frequencies in input!') - - if run_amount > 1: - LGR.info(f'Found {run_amount} different scans in input!') - - LGR.info(f'Preparing {freq_amount*run_amount} output files.') - # Create phys_out dict that will have a blueprint object for each different frequency - phys_out = {} - - if heur_file is not None and sub is not None: - LGR.info(f'Preparing BIDS output using {heur_file}') - # If heuristics are used, init a dict of arguments to pass to use_heuristic - heur_args = {'heur_file': heur_file, 'sub': sub, 'ses': ses, - 'filename': filename, 'outdir': outdir, 'run': '', - 'record_label': ''} - # 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.') - - # Export a (set of) phys_out for each element in phys_in - # run keys start from 1 (human friendly) - for run in phys_in.keys(): - for uniq_freq in uniq_freq_list: - # Initialise the key for the (possibly huge amount of) dictionary entries - key = f'{run}_{uniq_freq}' - # copy the phys_in object to the new dict entry - phys_out[key] = deepcopy(phys_in[run]) - # this counter will take into account how many channels are eliminated - count = 0 - # for each channel in the original phys_in object - # take the frequency - for idx, i in enumerate(phys_in[run].freq): - # if that frequency is different than the frequency of the phys_obj entry - if i != uniq_freq: - # eliminate that channel from the dict since we only want channels - # with the same frequency - phys_out[key].delete_at_index(idx - count) - # take into acount the elimination so in the next eliminated channel we - # eliminate correctly - count += 1 - # Also create a BlueprintOutput object for each unique frequency found. - # Populate it with the corresponding blueprint input and replace it - # in the dictionary. - # Add time channel in the proper frequency. - if uniq_freq != phys_in[run].freq[0]: - phys_out[key].ch_name.insert(0, phys_in[run].ch_name[0]) - phys_out[key].units.insert(0, phys_in[run].units[0]) - phys_out[key].timeseries.insert(0, np.linspace(phys_in[run].timeseries[0][0], - phys_in[run].timeseries[0][-1], - num=phys_out[key].timeseries[0].shape[0])) - # Add trigger channel in the proper frequency. - if uniq_freq != phys_in[run].freq[chtrig]: - phys_out[key].ch_name.insert(1, phys_in[run].ch_name[chtrig]) - phys_out[key].units.insert(1, phys_in[run].units[chtrig]) - phys_out[key].timeseries.insert(1, np.interp(phys_out[key].timeseries[0], - phys_in[run].timeseries[0], - phys_in[run].timeseries[chtrig])) - phys_out[key] = BlueprintOutput.init_from_blueprint(phys_out[key]) - - # Preparing output parameters: name and folder. - for uniq_freq in uniq_freq_list: - key = f'{run}_{uniq_freq}' - # If possible, prepare bids renaming. - if heur_file is not None and sub is not None: - # Add run info to heur_args if more than one run is present - if run_amount > 1: - heur_args['run'] = f'{run:02d}' - - # Append "recording-freq" to filename if more than one freq - if freq_amount > 1: - heur_args['record_label'] = f'{uniq_freq:.0f}Hz' - - 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. - if run_amount > 1: - if any([phys.filename == phys_out[key].filename - for phys in phys_out.values()]): - phys_out[key].filename = (f'{phys_out[key].filename}' - f'_take-{run}') - LGR.warning('Identified multiple outputs with the same name.\n' - 'Appending fake label to avoid overwriting.\n' - '!!! ATTENTION !!! the output is not BIDS compliant.\n' - 'Please check heuristics to solve the problem.') - - else: - phys_out[key].filename = os.path.join(outdir, - os.path.splitext(os.path.basename(filename) - )[0]) - # Append "run" to filename if more than one run - if run_amount > 1: - phys_out[key].filename = f'{phys_out[key].filename}_{run:02d}' - # Append "freq" to filename if more than one freq - if freq_amount > 1: - phys_out[key].filename = f'{phys_out[key].filename}_{uniq_freq:.0f}Hz' - - LGR.info(f'Exporting files for run {run} freq {uniq_freq}') - np.savetxt(phys_out[key].filename + '.tsv.gz', - phys_out[key].timeseries, fmt='%.8e', delimiter='\t') - print_json(phys_out[key].filename, phys_out[key].freq, - phys_out[key].start_time, phys_out[key].ch_name) - print_summary(filename, num_timepoints_expected, - phys_in[run].num_timepoints_found, uniq_freq, - phys_out[key].start_time, - os.path.join(conversion_path, - os.path.splitext(os.path.basename(phys_out[key].filename) - )[0])) def _main(argv=None): options = _get_parser().parse_args(argv) - phys2bids(**vars(options)) + phys2denoise(**vars(options)) if __name__ == '__main__': _main(sys.argv[1:]) """ -Copyright 2019, The Phys2BIDS community. +Copyright 2019, The phys2denoise community. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 4c265e3cca715104b9f155a0e58c6b9831d059fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A9s=20Chavarr=C3=ADa?= <72545702+ineschh@users.noreply.github.com> Date: Fri, 13 Nov 2020 10:10:53 +0100 Subject: [PATCH 06/26] More updates --- phys2denoise/cli/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index bfee4ac..4a75ac7 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -113,13 +113,13 @@ def _get_parser(): optional.add_argument("-os", "--oversampling", dest="oversampling", type=int, - help="Temporal oversampling factor in seconds. " + help="Temporal oversampling factor. " "Default is 50.", default=50) optional.add_argument("-tl", "--time-length", dest="time_length", type=int, - help="RRF Kernel length in seconds.", + help="RRF or CRF Kernel length in seconds.", default=None) optional.add_argument("-onset", "--onset", dest="onset", From 8bd4b8f0fa1351cab026bf180b3bc6f8ed14a6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A9s=20Chavarr=C3=ADa?= <72545702+ineschh@users.noreply.github.com> Date: Fri, 13 Nov 2020 13:34:56 +0100 Subject: [PATCH 07/26] Suggested changes --- phys2denoise/cli/run.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index 4a75ac7..09e0ae5 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -59,13 +59,15 @@ def _get_parser(): help="Respiratory variance. Needs the following inputs: " "sample-rate, window and lags.", default=False) + """ metric.add_argument("-rvt", "--respiratory-volume-per-time", dest="metrics", action="append_const", const="rvt", help="Respiratory volume-per-time. Needs the following inputs: " - "sample-rate, window and lags.", + "sample-rate, window, lags, peaks and troughs.", default=False) + """ metric.add_argument("-rrf", "--respiratory-response-function", dest="metrics", action="append_const", @@ -104,10 +106,10 @@ def _get_parser(): help="Full path and filename of the list with the indexed peaks' " "positions of the physiological data.", default=None) - optional.add_argument("-tg", "--throughts", - dest="throughts", + optional.add_argument("-tg", "--troughs", + dest="troughs", type=str, - help="Full path and filename of the list with the indexed peaks' " + help="Full path and filename of the list with the indexed troughs' " "positions of the physiological data.", default=None) optional.add_argument("-os", "--oversampling", From 7b950b62754a3d5105d3d382d2814527a9e7972c Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 13 Nov 2020 15:49:13 +0100 Subject: [PATCH 08/26] Import metrics --- phys2denoise.py | 40 +++++++++------------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 17a6ed5..760eb8f 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -19,9 +19,11 @@ import numpy as np -from phys2denoise import utils, viz, _version +from phys2denoise import utils, _version from phys2denoise.cli.run import _get_parser -# from phys2denoise.metrics import cardiac, chest_belt, retroicor +from phys2denoise.metrics.cardiac import crf +from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf +from phys2denoise.metrics.retroicor import compute_retroicor_regressors from . import __version__ from .due import due, Doi @@ -29,41 +31,13 @@ LGR = logging.getLogger(__name__) -def print_json(outfile, samp_freq, time_offset, ch_name): - """ - Print the json required by BIDS format. - - Parameters - ---------- - outfile: str or path - Fullpath to output file. - samp_freq: float - Frequency of sampling for the output file. - time_offset: float - Difference between beginning of file and first TR. - ch_name: list of str - List of channel names, as specified by BIDS format. - - Notes - ----- - Outcome: - outfile: .json file - File containing information for BIDS. - """ - start_time = -time_offset - summary = dict(SamplingFrequency=samp_freq, - StartTime=round(start_time, 4), - Columns=ch_name) - utils.writejson(outfile, summary, indent=4, sort_keys=False) - - @due.dcite( Doi(''), path='phys2denoise', description='Creation of regressors for physiological denoising', version=__version__, cite_module=True) -def phys2denoise(filename, outdir='.', debug=False, quiet=False): +def phys2denoise(filename, outdir='.', metrics=[], debug=False, quiet=False): """ Run main workflow of phys2denoise. @@ -129,7 +103,11 @@ def phys2denoise(filename, outdir='.', debug=False, quiet=False): # Read input file phys_in = np.genfromtxt(filename) + # Goes through the list of metrics and calls them + if not metrics: + metrics = ['crf', 'rpv', 'rv', 'rvt', 'rrf', 'rcard', 'r'] + for From 6090b6f5bfd746ccf6a50cdf08df48b1258ab9df Mon Sep 17 00:00:00 2001 From: smoia Date: Sat, 14 Nov 2020 11:38:06 +0100 Subject: [PATCH 09/26] First skeleton version --- phys2denoise.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 760eb8f..f89d005 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -18,6 +18,7 @@ from shutil import copy as cp import numpy as np +import pandas as pd from phys2denoise import utils, _version from phys2denoise.cli.run import _get_parser @@ -101,18 +102,34 @@ def phys2denoise(filename, outdir='.', metrics=[], debug=False, quiet=False): raise FileNotFoundError(f'The file {filename} does not exist!') # Read input file - phys_in = np.genfromtxt(filename) - - # Goes through the list of metrics and calls them - if not metrics: - metrics = ['crf', 'rpv', 'rv', 'rvt', 'rrf', 'rcard', 'r'] - - for - - + physio = np.genfromtxt(filename) + # Prepare pandas dataset + regr = pd.DataFrame() + # If no metrics was specified, calls all of them. + if not metrics: + metrics = ['crf', 'rpv', 'rv', 'rvt', 'rrf', 'retroicor_card', 'retroicor_resp'] + # Goes through the list of metrics and calls them + for metric in metrics: + if metrics == 'retroicor_card': + regr['retroicor_card'] = compute_retroicor_regressors(physio, + vars(metric_args), + card=True) + elif metrics == 'retroicor_resp': + regr['retroicor_resp'] = compute_retroicor_regressors(physio, + vars(metric_args), + resp=True) + else: + regr[f'{metric}'] = metric(physio, vars(metric_args)) + + #!# Add regressors visualisation + + # Export regressors and sidecar + out_filename = os.join(outdir, 'derivatives', filename) + regr.to_csv(out_filename, sep='\t', index=False, float_format='%.6e') + #!# Add sidecar export def _main(argv=None): From 195c369bbe0761a9624b561fba16ff005f3aabc2 Mon Sep 17 00:00:00 2001 From: smoia Date: Sat, 14 Nov 2020 13:35:30 +0100 Subject: [PATCH 10/26] Remove unused libraries and improve function argument --- phys2denoise.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index f89d005..74db30a 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -14,13 +14,11 @@ import logging import os import sys -from copy import deepcopy -from shutil import copy as cp import numpy as np import pandas as pd -from phys2denoise import utils, _version +from phys2denoise import _version from phys2denoise.cli.run import _get_parser from phys2denoise.metrics.cardiac import crf from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf @@ -38,7 +36,9 @@ description='Creation of regressors for physiological denoising', version=__version__, cite_module=True) -def phys2denoise(filename, outdir='.', metrics=[], debug=False, quiet=False): +def phys2denoise(filename, outdir='.', + metrics=[crf, rpv, rv, rvt, rrf, 'retroicor_card', 'retroicor_resp'], + debug=False, quiet=False): """ Run main workflow of phys2denoise. @@ -107,10 +107,6 @@ def phys2denoise(filename, outdir='.', metrics=[], debug=False, quiet=False): # Prepare pandas dataset regr = pd.DataFrame() - # If no metrics was specified, calls all of them. - if not metrics: - metrics = ['crf', 'rpv', 'rv', 'rvt', 'rrf', 'retroicor_card', 'retroicor_resp'] - # Goes through the list of metrics and calls them for metric in metrics: if metrics == 'retroicor_card': From 298acaf4162996db41f5b2747c2984351accc06c Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 25 Nov 2020 12:21:24 +0100 Subject: [PATCH 11/26] Input args to metrics using inspect.signature() --- phys2denoise.py | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 74db30a..514ba07 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -14,6 +14,7 @@ import logging import os import sys +from inspect import signature import numpy as np import pandas as pd @@ -30,6 +31,40 @@ LGR = logging.getLogger(__name__) +def select_input_args(metric, metric_args): + """ + Retrieve required args for metric from a dictionary of possible arguments. + + Parameters + ---------- + metric : function + Metric function to retrieve arguments for + metric_args : dict + Dictionary containing all arguments for all functions requested by the + user + + Returns + ------- + args : dict + Arguments to provide as input to metric + + Raises + ------ + ValueError + If a required argument is missing + + """ + req_args = [str(arg) for arg in signature(metric).parameters.values()] + + for arg in req_args: + if arg not in metric_args: + raise ValueError(f'Missing parameter {arg} required to run {metric}') + + args = {arg: metric_args[arg] for arg in req_args} + + return args + + @due.dcite( Doi(''), path='phys2denoise', @@ -110,15 +145,18 @@ def phys2denoise(filename, outdir='.', # Goes through the list of metrics and calls them for metric in metrics: if metrics == 'retroicor_card': + args = select_input_args(compute_retroicor_regressors, metric_args) regr['retroicor_card'] = compute_retroicor_regressors(physio, - vars(metric_args), + **args, card=True) elif metrics == 'retroicor_resp': + args = select_input_args(compute_retroicor_regressors, metric_args) regr['retroicor_resp'] = compute_retroicor_regressors(physio, - vars(metric_args), + **args, resp=True) else: - regr[f'{metric}'] = metric(physio, vars(metric_args)) + args = select_input_args(metric, metric_args) + regr[f'{metric}'] = metric(physio, **args) #!# Add regressors visualisation From bd340be1324203496a4a16e42141b536f8e67c22 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 25 Nov 2020 12:27:33 +0100 Subject: [PATCH 12/26] Change log path --- phys2denoise.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 514ba07..262b395 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -71,7 +71,7 @@ def select_input_args(metric, metric_args): description='Creation of regressors for physiological denoising', version=__version__, cite_module=True) -def phys2denoise(filename, outdir='.', +def phys2denoise(filename, metric_args, outdir='.', metrics=[crf, rpv, rv, rvt, rrf, 'retroicor_card', 'retroicor_resp'], debug=False, quiet=False): """ @@ -89,14 +89,14 @@ def phys2denoise(filename, outdir='.', outdir = os.path.abspath(outdir) os.makedirs(outdir) os.makedirs(os.path.join(outdir, 'code')) - conversion_path = os.path.join(outdir, 'code', 'conversion') - os.makedirs(conversion_path) + log_path = os.path.join(outdir, 'code', 'logs') + os.makedirs(log_path) # Create logfile name basename = 'phys2denoise_' extension = 'tsv' isotime = datetime.datetime.now().strftime('%Y-%m-%dT%H%M%S') - logname = os.path.join(conversion_path, (basename + isotime + '.' + extension)) + logname = os.path.join(log_path, (basename + isotime + '.' + extension)) # Set logging format log_formatter = logging.Formatter( @@ -110,13 +110,16 @@ def phys2denoise(filename, outdir='.', if quiet: logging.basicConfig(level=logging.WARNING, - handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') + handlers=[log_handler, sh], + format='%(levelname)-10s %(message)s') elif debug: logging.basicConfig(level=logging.DEBUG, - handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') + handlers=[log_handler, sh], + format='%(levelname)-10s %(message)s') else: logging.basicConfig(level=logging.INFO, - handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') + handlers=[log_handler, sh], + format='%(levelname)-10s %(message)s') version_number = _version.get_versions()['version'] LGR.info(f'Currently running phys2denoise version {version_number}') @@ -125,7 +128,7 @@ def phys2denoise(filename, outdir='.', # Save call.sh arg_str = ' '.join(sys.argv[1:]) call_str = f'phys2denoise {arg_str}' - f = open(os.path.join(conversion_path, 'call.sh'), "a") + f = open(os.path.join(log_path, 'call.sh'), "a") f.write(f'#!bin/bash \n{call_str}') f.close() From cb8b42ef99870fe5d46931073bcbcaeb747267d4 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 25 Nov 2020 23:13:00 +0100 Subject: [PATCH 13/26] Change inspection of metrics and add a logger function --- phys2denoise.py | 62 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 262b395..3e2354d 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -14,7 +14,7 @@ import logging import os import sys -from inspect import signature +from inspect import signature, _empty import numpy as np import pandas as pd @@ -35,6 +35,12 @@ def select_input_args(metric, metric_args): """ Retrieve required args for metric from a dictionary of possible arguments. + This function checks what parameters are accepted by a metric. + Then, for each parameter, check if the user provided it or not. + If they did not, but the parameter is required, throw an error - + unless it's "physio", reserved name for the timeseries input to a metric. + Otherwise, use the default. + Parameters ---------- metric : function @@ -54,15 +60,46 @@ def select_input_args(metric, metric_args): If a required argument is missing """ - req_args = [str(arg) for arg in signature(metric).parameters.values()] + args = {} + + # Check the parameters required by the metric and given by the user (see docstring) + for param in signature(metric).parameters.values(): + if param.name not in metric_args: + if param.default == _empty and param.name != 'physio': + raise ValueError(f'Missing parameter {param} required ' + f'to run {metric}') + else: + args[param.name] = param.default + else: + args[param.name] = metric_args[param.name] - for arg in req_args: - if arg not in metric_args: - raise ValueError(f'Missing parameter {arg} required to run {metric}') + return args - args = {arg: metric_args[arg] for arg in req_args} - return args +def print_metric_call(metric, args): + """ + Log a message to describe how a metric is being called. + + Parameters + ---------- + metric : function + Metric function that is being called + args : dict + Dictionary containing all arguments that are used to parametrise metric + + Notes + ----- + Outcome + An info-level message for the logger. + """ + msg = f'The {metric} regressor will be computed using the following parameters:' + + for arg in args: + msg = f'{msg}\n {arg} = {args[arg]}' + + msg = f'{msg}\n' + + LGR.info(msg) @due.dcite( @@ -149,16 +186,19 @@ def phys2denoise(filename, metric_args, outdir='.', for metric in metrics: if metrics == 'retroicor_card': args = select_input_args(compute_retroicor_regressors, metric_args) + args['card'] = True + print_metric_call(metric, args) regr['retroicor_card'] = compute_retroicor_regressors(physio, - **args, - card=True) + **args) elif metrics == 'retroicor_resp': args = select_input_args(compute_retroicor_regressors, metric_args) + args['resp'] = True + print_metric_call(metric, args) regr['retroicor_resp'] = compute_retroicor_regressors(physio, - **args, - resp=True) + **args) else: args = select_input_args(metric, metric_args) + print_metric_call(metric, args) regr[f'{metric}'] = metric(physio, **args) #!# Add regressors visualisation From 9c1bd601fe6abaac97c6e8ceb6f001449fd8a0a8 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 25 Nov 2020 23:17:21 +0100 Subject: [PATCH 14/26] Recognising planning contribution to @62442katieb Co-authored-by: @62442katieb From 1541149d95b7b7268af88b85604d274ceb738579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A9s=20Chavarr=C3=ADa?= <72545702+ineschh@users.noreply.github.com> Date: Thu, 26 Nov 2020 15:03:48 +0100 Subject: [PATCH 15/26] metric_args into dict --- phys2denoise/cli/run.py | 188 +++++++++++++++++++++++----------------- 1 file changed, 109 insertions(+), 79 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index 09e0ae5..2776756 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -5,6 +5,24 @@ import argparse from phys2denoise import __version__ +from phys2denoise.metrics.cardiac import crf +from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf, env + + +class MetricsArgDict(argparse.Action): + """ + Custom Argparse Action to create a dictionary with the metrics' arguments in parser's output. + + """ + def __call__(self, parser, namespace, values, option_strings): + if not hasattr(namespace, "metrics_arg"): + setattr(namespace, "metrics_arg", dict()) + Keys = ["sample_rate", "peaks", "throughs", "oversampling", "time_length", "onset", + "tr", "window", "lags", "nscans", "nharm"] + Vals = ["None", "None", "None", "50", "None", "0", "None", "6", "None", "1", "None"] + for k, v in zip(Keys, Vals): + getattr(namespace, "metrics_arg")[k] = v + getattr(namespace, "metrics_arg")[self.dest] = values def _get_parser(): @@ -17,13 +35,16 @@ def _get_parser(): Notes ----- + Default values must be updated in __call__ method from MetricsArgDict class. # Argument parser follow template provided by RalphyZ. # https://stackoverflow.com/a/43456577 """ + parser = argparse.ArgumentParser() optional = parser._action_groups.pop() - metric = parser._action_groups.pop() - required = parser.add_argument_group("Required Argument:") + required = parser.add_argument_group("Required Argument") + metric = parser.add_argument_group("Metrics") + metric_arg = parser.add_argument_group("Metrics Arguments") required.add_argument("-in", "--input-file", dest="filename", type=str, @@ -33,32 +54,32 @@ def _get_parser(): metric.add_argument("-crf", "--cardiac-response-function", dest="metrics", action="append_const", - const="crf", + const=crf, help="Cardiac response function. Needs the following " "inputs:sample-rate, oversampling, time-length, " "onset and tr.", - default=False) + default=[]) metric.add_argument("-rpv", "--respiratory-pattern-variability", dest="metrics", action="append_const", - const="rpv", + const=rpv, help="Respiratory pattern variability. Needs the following " "input: window.", - default=False) + default=[]) metric.add_argument("-env", "--envelope", dest="metrics", action="append_const", - const="env", + const=env, help="Respiratory pattern variability calculated across a sliding " "window. Needs the following inputs: sample-rate, window and lags.", - default=False) + default=[]) metric.add_argument("-rv", "--respiratory-variance", dest="metrics", action="append_const", - const="rv", + const=rv, help="Respiratory variance. Needs the following inputs: " "sample-rate, window and lags.", - default=False) + default=[]) """ metric.add_argument("-rvt", "--respiratory-volume-per-time", dest="metrics", @@ -66,98 +87,108 @@ def _get_parser(): const="rvt", help="Respiratory volume-per-time. Needs the following inputs: " "sample-rate, window, lags, peaks and troughs.", - default=False) + default=[]) """ metric.add_argument("-rrf", "--respiratory-response-function", dest="metrics", action="append_const", - const="rrf", + const=rrf, help="Respiratory response function. Needs the following inputs: " "sample-rate, oversampling, time-length, onset and tr.", - default=False) + default=[]) metric.add_argument("-rcard", "--retroicor-card", dest="metrics", action="append_const", const="r_card", help="Computes regressors for cardiac signal. Needs the following " "inputs: tr, nscans and n_harm.", - default=False) + default=[]) metric.add_argument("-rresp", "--retroicor-resp", dest="metrics", action="append_const", const="r_resp", help="Computes regressors for respiratory signal. Needs the following " "inputs: tr, nscans and n_harm.", - default=False) + default=[]) optional.add_argument("-outdir", "--output-dir", dest="outdir", type=str, help="Folder where output should be placed. " "Default is current folder.", default=".") - optional.add_argument("-sr", "--sample-rate", - dest="sample_rate", - type=float, - help="Sampling rate of the physiological data in Hz.", - default=None) - optional.add_argument("-pk", "--peaks", - dest="peaks", - type=str, - help="Full path and filename of the list with the indexed peaks' " - "positions of the physiological data.", - default=None) - optional.add_argument("-tg", "--troughs", - dest="troughs", - type=str, - help="Full path and filename of the list with the indexed troughs' " - "positions of the physiological data.", - default=None) - optional.add_argument("-os", "--oversampling", - dest="oversampling", - type=int, - help="Temporal oversampling factor. " - "Default is 50.", - default=50) - optional.add_argument("-tl", "--time-length", - dest="time_length", - type=int, - help="RRF or CRF Kernel length in seconds.", - default=None) - optional.add_argument("-onset", "--onset", - dest="onset", - type=float, - help="Onset of the response in seconds. " - "Default is 0.", - default=0) - optional.add_argument("-tr", "--tr", - dest="tr", - type=float, - help="TR of sequence in seconds.", - default=None) - optional.add_argument("-win", "--window", - dest="window", - type=int, - help="Size of the sliding window in seconds. " - "Default is 6 seconds.", - default=6) - optional.add_argument("-lags", "--lags", - dest="lags", - nargs="*", - type=int, - action="append", - help="List of lags to apply to the RV estimate " - "in seconds.", - default=None) - optional.add_argument("-nscans", "--number-scans", - dest="nscans", - type=int, - help="Number of scans. Default is 1.", - default=1) - optional.add_argument("-nharm", "--number-harmonics", - dest="n_harm", - type=int, - help="Number of harmonics.", - default=None) + metric_arg.add_argument("-sr", "--sample-rate", + dest="sample_rate", + type=float, + action=MetricsArgDict, + help="Sampling rate of the physiological data in Hz.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-pk", "--peaks", + dest="peaks", + type=str, + action=MetricsArgDict, + help="Full path and filename of the list with the indexed peaks' " + "positions of the physiological data.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-tg", "--troughs", + dest="troughs", + type=str, + action=MetricsArgDict, + help="Full path and filename of the list with the indexed troughs' " + "positions of the physiological data.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-os", "--oversampling", + dest="oversampling", + type=int, + action=MetricsArgDict, + help="Temporal oversampling factor. " + "Default is 50.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-tl", "--time-length", + dest="time_length", + type=int, + action=MetricsArgDict, + help="RRF or CRF Kernel length in seconds.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-onset", "--onset", + dest="onset", + type=float, + action=MetricsArgDict, + help="Onset of the response in seconds. " + "Default is 0.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-tr", "--tr", + dest="tr", + type=float, + action=MetricsArgDict, + help="TR of sequence in seconds.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-win", "--window", + dest="window", + type=int, + action=MetricsArgDict, + help="Size of the sliding window in seconds. " + "Default is 6 seconds.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-lags", "--lags", + dest="lags", + nargs="*", + type=int, + action=MetricsArgDict, + help="List of lags to apply to the RV estimate " + "in seconds.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-nscans", "--number-scans", + dest="nscans", + type=int, + action=MetricsArgDict, + help="Number of scans. Default is 1.", + default=argparse.SUPPRESS) + metric_arg.add_argument("-nharm", "--number-harmonics", + dest="n_harm", + type=int, + action=MetricsArgDict, + help="Number of harmonics.", + default=argparse.SUPPRESS) optional.add_argument("-debug", "--debug", dest="debug", action="store_true", @@ -172,7 +203,6 @@ def _get_parser(): version=("%(prog)s " + __version__)) parser._action_groups.append(optional) - parser._action_groups.append(metric) return parser From 636f585e8c4cfb1b5f65971b36eae46886515342 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 15:46:43 +0100 Subject: [PATCH 16/26] Remove extra "version" imports --- phys2denoise.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 3e2354d..b1cc125 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -19,7 +19,6 @@ import numpy as np import pandas as pd -from phys2denoise import _version from phys2denoise.cli.run import _get_parser from phys2denoise.metrics.cardiac import crf from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf @@ -158,7 +157,7 @@ def phys2denoise(filename, metric_args, outdir='.', handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') - version_number = _version.get_versions()['version'] + version_number = __version__ LGR.info(f'Currently running phys2denoise version {version_number}') LGR.info(f'Input file is {filename}') From 36abfb80fac870bf569be8c90ed823d53fb80207 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 15:46:57 +0100 Subject: [PATCH 17/26] Change metric_args into **kwargs --- phys2denoise.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index b1cc125..2fe6bd1 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -107,9 +107,9 @@ def print_metric_call(metric, args): description='Creation of regressors for physiological denoising', version=__version__, cite_module=True) -def phys2denoise(filename, metric_args, outdir='.', +def phys2denoise(filename, outdir='.', metrics=[crf, rpv, rv, rvt, rrf, 'retroicor_card', 'retroicor_resp'], - debug=False, quiet=False): + debug=False, quiet=False, **kwargs): """ Run main workflow of phys2denoise. @@ -117,14 +117,13 @@ def phys2denoise(filename, metric_args, outdir='.', Notes ----- + Any metric argument should go into kwargs! The code was greatly copied from phys2bids (copyright the physiopy community) """ # Check options to make them internally coherent pt. I # #!# This can probably be done while parsing? outdir = os.path.abspath(outdir) - os.makedirs(outdir) - os.makedirs(os.path.join(outdir, 'code')) log_path = os.path.join(outdir, 'code', 'logs') os.makedirs(log_path) @@ -184,19 +183,19 @@ def phys2denoise(filename, metric_args, outdir='.', # Goes through the list of metrics and calls them for metric in metrics: if metrics == 'retroicor_card': - args = select_input_args(compute_retroicor_regressors, metric_args) + args = select_input_args(compute_retroicor_regressors, kwargs) args['card'] = True print_metric_call(metric, args) regr['retroicor_card'] = compute_retroicor_regressors(physio, **args) elif metrics == 'retroicor_resp': - args = select_input_args(compute_retroicor_regressors, metric_args) + args = select_input_args(compute_retroicor_regressors, kwargs) args['resp'] = True print_metric_call(metric, args) regr['retroicor_resp'] = compute_retroicor_regressors(physio, **args) else: - args = select_input_args(metric, metric_args) + args = select_input_args(metric, kwargs) print_metric_call(metric, args) regr[f'{metric}'] = metric(physio, **args) From 46c1e0c9c4c2ebe9e80f3bc7d35cb1ed23de47c2 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 15:57:29 +0100 Subject: [PATCH 18/26] Move metric call log to metrics.utils instead of in the main workflow. --- phys2denoise.py | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 2fe6bd1..37325ac 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -28,6 +28,7 @@ from .due import due, Doi LGR = logging.getLogger(__name__) +LGR.setLevel(logging.INFO) def select_input_args(metric, metric_args): @@ -75,32 +76,6 @@ def select_input_args(metric, metric_args): return args -def print_metric_call(metric, args): - """ - Log a message to describe how a metric is being called. - - Parameters - ---------- - metric : function - Metric function that is being called - args : dict - Dictionary containing all arguments that are used to parametrise metric - - Notes - ----- - Outcome - An info-level message for the logger. - """ - msg = f'The {metric} regressor will be computed using the following parameters:' - - for arg in args: - msg = f'{msg}\n {arg} = {args[arg]}' - - msg = f'{msg}\n' - - LGR.info(msg) - - @due.dcite( Doi(''), path='phys2denoise', @@ -182,21 +157,18 @@ def phys2denoise(filename, outdir='.', # Goes through the list of metrics and calls them for metric in metrics: - if metrics == 'retroicor_card': + if metric == 'retroicor_card': args = select_input_args(compute_retroicor_regressors, kwargs) args['card'] = True - print_metric_call(metric, args) regr['retroicor_card'] = compute_retroicor_regressors(physio, **args) - elif metrics == 'retroicor_resp': + elif metric == 'retroicor_resp': args = select_input_args(compute_retroicor_regressors, kwargs) args['resp'] = True - print_metric_call(metric, args) regr['retroicor_resp'] = compute_retroicor_regressors(physio, **args) else: args = select_input_args(metric, kwargs) - print_metric_call(metric, args) regr[f'{metric}'] = metric(physio, **args) #!# Add regressors visualisation From 44fde8c54f745214487c74f3d7e7632ed301ff4f Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 16:45:29 +0100 Subject: [PATCH 19/26] Move the bash call saving to a function on its own out of the main workflow --- phys2denoise.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/phys2denoise.py b/phys2denoise.py index 37325ac..b9cd65e 100644 --- a/phys2denoise.py +++ b/phys2denoise.py @@ -31,6 +31,28 @@ LGR.setLevel(logging.INFO) +def save_bash_call(outdir): + """ + Save the bash call into file `p2d_call.sh`. + + Parameters + ---------- + metric : function + Metric function to retrieve arguments for + metric_args : dict + Dictionary containing all arguments for all functions requested by the + user + """ + arg_str = ' '.join(sys.argv[1:]) + call_str = f'phys2denoise {arg_str}' + outdir = os.path.abspath(outdir) + log_path = os.path.join(outdir, 'code', 'logs') + os.makedirs(log_path) + f = open(os.path.join(log_path, 'p2d_call.sh'), "a") + f.write(f'#!bin/bash \n{call_str}') + f.close() + + def select_input_args(metric, metric_args): """ Retrieve required args for metric from a dictionary of possible arguments. @@ -135,13 +157,6 @@ def phys2denoise(filename, outdir='.', LGR.info(f'Currently running phys2denoise version {version_number}') LGR.info(f'Input file is {filename}') - # Save call.sh - arg_str = ' '.join(sys.argv[1:]) - call_str = f'phys2denoise {arg_str}' - f = open(os.path.join(log_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? # filename, ftype = utils.check_input_type(filename) @@ -181,6 +196,9 @@ def phys2denoise(filename, outdir='.', def _main(argv=None): options = _get_parser().parse_args(argv) + + save_bash_call(options.outdir) + phys2denoise(**vars(options)) From 570619dc0119b3958296094219198ba10cb074e6 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 16:49:04 +0100 Subject: [PATCH 20/26] Move main workflow in the right place --- phys2denoise.py => phys2denoise/phys2denoise.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename phys2denoise.py => phys2denoise/phys2denoise.py (100%) diff --git a/phys2denoise.py b/phys2denoise/phys2denoise.py similarity index 100% rename from phys2denoise.py rename to phys2denoise/phys2denoise.py From 3a66afacb181e3ba267a675c5c7098225619b42c Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 17:07:59 +0100 Subject: [PATCH 21/26] Add parser --- phys2denoise/cli/run.py | 164 ++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 83 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index 2776756..b6a7248 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -43,152 +43,150 @@ def _get_parser(): parser = argparse.ArgumentParser() optional = parser._action_groups.pop() required = parser.add_argument_group("Required Argument") - metric = parser.add_argument_group("Metrics") + metrics = parser.add_argument_group("Metrics") metric_arg = parser.add_argument_group("Metrics Arguments") + # Required arguments required.add_argument("-in", "--input-file", dest="filename", type=str, help="Full path and name of the file containing " "physiological data, with or without extension.", required=True) - metric.add_argument("-crf", "--cardiac-response-function", - dest="metrics", - action="append_const", - const=crf, - help="Cardiac response function. Needs the following " - "inputs:sample-rate, oversampling, time-length, " - "onset and tr.", - default=[]) - metric.add_argument("-rpv", "--respiratory-pattern-variability", - dest="metrics", - action="append_const", - const=rpv, - help="Respiratory pattern variability. Needs the following " - "input: window.", - default=[]) - metric.add_argument("-env", "--envelope", - dest="metrics", - action="append_const", - const=env, - help="Respiratory pattern variability calculated across a sliding " - "window. Needs the following inputs: sample-rate, window and lags.", - default=[]) - metric.add_argument("-rv", "--respiratory-variance", - dest="metrics", - action="append_const", - const=rv, - help="Respiratory variance. Needs the following inputs: " - "sample-rate, window and lags.", - default=[]) - """ - metric.add_argument("-rvt", "--respiratory-volume-per-time", - dest="metrics", - action="append_const", - const="rvt", - help="Respiratory volume-per-time. Needs the following inputs: " - "sample-rate, window, lags, peaks and troughs.", - default=[]) - """ - metric.add_argument("-rrf", "--respiratory-response-function", - dest="metrics", - action="append_const", - const=rrf, - help="Respiratory response function. Needs the following inputs: " - "sample-rate, oversampling, time-length, onset and tr.", - default=[]) - metric.add_argument("-rcard", "--retroicor-card", - dest="metrics", - action="append_const", - const="r_card", - help="Computes regressors for cardiac signal. Needs the following " - "inputs: tr, nscans and n_harm.", - default=[]) - metric.add_argument("-rresp", "--retroicor-resp", - dest="metrics", - action="append_const", - const="r_resp", - help="Computes regressors for respiratory signal. Needs the following " - "inputs: tr, nscans and n_harm.", - default=[]) + # Important optional arguments optional.add_argument("-outdir", "--output-dir", dest="outdir", type=str, help="Folder where output should be placed. " "Default is current folder.", default=".") + # Metric selection + metrics.add_argument("-crf", "--cardiac-response-function", + dest="metrics", + action="append_const", + const=crf, + help="Cardiac response function. Requires the following " + "inputs:sample-rate, oversampling, time-length, " + "onset and tr.", + default=[]) + metrics.add_argument("-rpv", "--respiratory-pattern-variability", + dest="metrics", + action="append_const", + const=rpv, + help="Respiratory pattern variability. Requires the following " + "input: window.", + default=[]) + metrics.add_argument("-env", "--envelope", + dest="metrics", + action="append_const", + const=env, + help="Respiratory pattern variability calculated across a sliding " + "window. Requires the following inputs: sample-rate, window and lags.", + default=[]) + metrics.add_argument("-rv", "--respiratory-variance", + dest="metrics", + action="append_const", + const=rv, + help="Respiratory variance. Requires the following inputs: " + "sample-rate, window and lags. If the input file " + "not a .phys file, it also requires peaks and troughs", + default=[]) + """ + metrics.add_argument("-rvt", "--respiratory-volume-per-time", + dest="metrics", + action="append_const", + const="rvt", + help="Respiratory volume-per-time. Requires the following inputs: " + "sample-rate, window, lags, peaks and troughs.", + default=[]) + """ + metrics.add_argument("-rrf", "--respiratory-response-function", + dest="metrics", + action="append_const", + const=rrf, + help="Respiratory response function. Requires the following inputs: " + "sample-rate, oversampling, time-length, onset and tr.", + default=[]) + metrics.add_argument("-rcard", "--retroicor-card", + dest="metrics", + action="append_const", + const="r_card", + help="Computes regressors for cardiac signal. Requires the following " + "inputs: tr, nscans and n_harm.", + default=[]) + metrics.add_argument("-rresp", "--retroicor-resp", + dest="metrics", + action="append_const", + const="r_resp", + help="Computes regressors for respiratory signal. Requires the following " + "inputs: tr, nscans and n_harm.", + default=[]) + # Metric arguments metric_arg.add_argument("-sr", "--sample-rate", dest="sample_rate", type=float, - action=MetricsArgDict, help="Sampling rate of the physiological data in Hz.", - default=argparse.SUPPRESS) + default=None) metric_arg.add_argument("-pk", "--peaks", dest="peaks", type=str, - action=MetricsArgDict, help="Full path and filename of the list with the indexed peaks' " "positions of the physiological data.", - default=argparse.SUPPRESS) + default=None) metric_arg.add_argument("-tg", "--troughs", dest="troughs", type=str, - action=MetricsArgDict, help="Full path and filename of the list with the indexed troughs' " "positions of the physiological data.", - default=argparse.SUPPRESS) + default=None) metric_arg.add_argument("-os", "--oversampling", dest="oversampling", type=int, - action=MetricsArgDict, help="Temporal oversampling factor. " "Default is 50.", - default=argparse.SUPPRESS) + default=50) metric_arg.add_argument("-tl", "--time-length", dest="time_length", type=int, - action=MetricsArgDict, help="RRF or CRF Kernel length in seconds.", - default=argparse.SUPPRESS) + default=None) metric_arg.add_argument("-onset", "--onset", dest="onset", type=float, - action=MetricsArgDict, help="Onset of the response in seconds. " "Default is 0.", - default=argparse.SUPPRESS) + default=0) metric_arg.add_argument("-tr", "--tr", dest="tr", type=float, - action=MetricsArgDict, help="TR of sequence in seconds.", - default=argparse.SUPPRESS) + default=None) metric_arg.add_argument("-win", "--window", dest="window", type=int, - action=MetricsArgDict, help="Size of the sliding window in seconds. " "Default is 6 seconds.", - default=argparse.SUPPRESS) + default=6) metric_arg.add_argument("-lags", "--lags", dest="lags", nargs="*", type=int, - action=MetricsArgDict, help="List of lags to apply to the RV estimate " "in seconds.", - default=argparse.SUPPRESS) + default=None) metric_arg.add_argument("-nscans", "--number-scans", dest="nscans", type=int, - action=MetricsArgDict, - help="Number of scans. Default is 1.", - default=argparse.SUPPRESS) + help="Number of timepoints in the imaging data. " + "Also called sub-bricks, TRs, scans, volumes." + "Default is 1.", + default=1) metric_arg.add_argument("-nharm", "--number-harmonics", dest="n_harm", type=int, - action=MetricsArgDict, help="Number of harmonics.", - default=argparse.SUPPRESS) + default=None) + + # Other optional arguments optional.add_argument("-debug", "--debug", dest="debug", action="store_true", From 5adc4f50a9d8ae479363bb7d9b86bfd93fee1bf9 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 17:33:08 +0100 Subject: [PATCH 22/26] Change call to retroicor into argument selection of type --- phys2denoise/cli/run.py | 40 +++++++++++++----------------------- phys2denoise/phys2denoise.py | 15 ++------------ 2 files changed, 16 insertions(+), 39 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index b6a7248..eaea2d1 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -9,22 +9,6 @@ from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf, env -class MetricsArgDict(argparse.Action): - """ - Custom Argparse Action to create a dictionary with the metrics' arguments in parser's output. - - """ - def __call__(self, parser, namespace, values, option_strings): - if not hasattr(namespace, "metrics_arg"): - setattr(namespace, "metrics_arg", dict()) - Keys = ["sample_rate", "peaks", "throughs", "oversampling", "time_length", "onset", - "tr", "window", "lags", "nscans", "nharm"] - Vals = ["None", "None", "None", "50", "None", "0", "None", "6", "None", "1", "None"] - for k, v in zip(Keys, Vals): - getattr(namespace, "metrics_arg")[k] = v - getattr(namespace, "metrics_arg")[self.dest] = values - - def _get_parser(): """ Parse command line inputs for this function. @@ -39,7 +23,6 @@ def _get_parser(): # Argument parser follow template provided by RalphyZ. # https://stackoverflow.com/a/43456577 """ - parser = argparse.ArgumentParser() optional = parser._action_groups.pop() required = parser.add_argument_group("Required Argument") @@ -106,19 +89,12 @@ def _get_parser(): help="Respiratory response function. Requires the following inputs: " "sample-rate, oversampling, time-length, onset and tr.", default=[]) - metrics.add_argument("-rcard", "--retroicor-card", + metrics.add_argument("-rcor", "--retroicor", dest="metrics", action="append_const", const="r_card", help="Computes regressors for cardiac signal. Requires the following " - "inputs: tr, nscans and n_harm.", - default=[]) - metrics.add_argument("-rresp", "--retroicor-resp", - dest="metrics", - action="append_const", - const="r_resp", - help="Computes regressors for respiratory signal. Requires the following " - "inputs: tr, nscans and n_harm.", + "inputs: either card or resp, tr, nscans and n_harm.", default=[]) # Metric arguments metric_arg.add_argument("-sr", "--sample-rate", @@ -185,6 +161,18 @@ def _get_parser(): type=int, help="Number of harmonics.", default=None) + metric_arg.add_argument("-card", "--cardiac", + dest="card", + type=bool, + action="store_true", + help="Compute *cardiac* RETROICOR.", + default=False) + metric_arg.add_argument("-resp", "--resp", + dest="resp", + type=bool, + action="store_true", + help="Compute *respiratory* RETROICOR.", + default=False) # Other optional arguments optional.add_argument("-debug", "--debug", diff --git a/phys2denoise/phys2denoise.py b/phys2denoise/phys2denoise.py index b9cd65e..9a6a9b3 100644 --- a/phys2denoise/phys2denoise.py +++ b/phys2denoise/phys2denoise.py @@ -172,19 +172,8 @@ def phys2denoise(filename, outdir='.', # Goes through the list of metrics and calls them for metric in metrics: - if metric == 'retroicor_card': - args = select_input_args(compute_retroicor_regressors, kwargs) - args['card'] = True - regr['retroicor_card'] = compute_retroicor_regressors(physio, - **args) - elif metric == 'retroicor_resp': - args = select_input_args(compute_retroicor_regressors, kwargs) - args['resp'] = True - regr['retroicor_resp'] = compute_retroicor_regressors(physio, - **args) - else: - args = select_input_args(metric, kwargs) - regr[f'{metric}'] = metric(physio, **args) + args = select_input_args(metric, kwargs) + regr[f'{metric}'] = metric(physio, **args) #!# Add regressors visualisation From 6e7f7ee1d496056142ced2030a38d63d4430befd Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 18:21:04 +0100 Subject: [PATCH 23/26] Revert "Change call to retroicor into argument selection of type" This reverts commit 5adc4f50a9d8ae479363bb7d9b86bfd93fee1bf9. --- phys2denoise/cli/run.py | 40 +++++++++++++++++++++++------------- phys2denoise/phys2denoise.py | 15 ++++++++++++-- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index eaea2d1..b6a7248 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -9,6 +9,22 @@ from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf, env +class MetricsArgDict(argparse.Action): + """ + Custom Argparse Action to create a dictionary with the metrics' arguments in parser's output. + + """ + def __call__(self, parser, namespace, values, option_strings): + if not hasattr(namespace, "metrics_arg"): + setattr(namespace, "metrics_arg", dict()) + Keys = ["sample_rate", "peaks", "throughs", "oversampling", "time_length", "onset", + "tr", "window", "lags", "nscans", "nharm"] + Vals = ["None", "None", "None", "50", "None", "0", "None", "6", "None", "1", "None"] + for k, v in zip(Keys, Vals): + getattr(namespace, "metrics_arg")[k] = v + getattr(namespace, "metrics_arg")[self.dest] = values + + def _get_parser(): """ Parse command line inputs for this function. @@ -23,6 +39,7 @@ def _get_parser(): # Argument parser follow template provided by RalphyZ. # https://stackoverflow.com/a/43456577 """ + parser = argparse.ArgumentParser() optional = parser._action_groups.pop() required = parser.add_argument_group("Required Argument") @@ -89,12 +106,19 @@ def _get_parser(): help="Respiratory response function. Requires the following inputs: " "sample-rate, oversampling, time-length, onset and tr.", default=[]) - metrics.add_argument("-rcor", "--retroicor", + metrics.add_argument("-rcard", "--retroicor-card", dest="metrics", action="append_const", const="r_card", help="Computes regressors for cardiac signal. Requires the following " - "inputs: either card or resp, tr, nscans and n_harm.", + "inputs: tr, nscans and n_harm.", + default=[]) + metrics.add_argument("-rresp", "--retroicor-resp", + dest="metrics", + action="append_const", + const="r_resp", + help="Computes regressors for respiratory signal. Requires the following " + "inputs: tr, nscans and n_harm.", default=[]) # Metric arguments metric_arg.add_argument("-sr", "--sample-rate", @@ -161,18 +185,6 @@ def _get_parser(): type=int, help="Number of harmonics.", default=None) - metric_arg.add_argument("-card", "--cardiac", - dest="card", - type=bool, - action="store_true", - help="Compute *cardiac* RETROICOR.", - default=False) - metric_arg.add_argument("-resp", "--resp", - dest="resp", - type=bool, - action="store_true", - help="Compute *respiratory* RETROICOR.", - default=False) # Other optional arguments optional.add_argument("-debug", "--debug", diff --git a/phys2denoise/phys2denoise.py b/phys2denoise/phys2denoise.py index 9a6a9b3..b9cd65e 100644 --- a/phys2denoise/phys2denoise.py +++ b/phys2denoise/phys2denoise.py @@ -172,8 +172,19 @@ def phys2denoise(filename, outdir='.', # Goes through the list of metrics and calls them for metric in metrics: - args = select_input_args(metric, kwargs) - regr[f'{metric}'] = metric(physio, **args) + if metric == 'retroicor_card': + args = select_input_args(compute_retroicor_regressors, kwargs) + args['card'] = True + regr['retroicor_card'] = compute_retroicor_regressors(physio, + **args) + elif metric == 'retroicor_resp': + args = select_input_args(compute_retroicor_regressors, kwargs) + args['resp'] = True + regr['retroicor_resp'] = compute_retroicor_regressors(physio, + **args) + else: + args = select_input_args(metric, kwargs) + regr[f'{metric}'] = metric(physio, **args) #!# Add regressors visualisation From 76fe4371c4b33a58928a60f792d7ee316bb70ea4 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 18:31:14 +0100 Subject: [PATCH 24/26] Better retroicor call --- phys2denoise/phys2denoise.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/phys2denoise/phys2denoise.py b/phys2denoise/phys2denoise.py index b9cd65e..284b36c 100644 --- a/phys2denoise/phys2denoise.py +++ b/phys2denoise/phys2denoise.py @@ -175,16 +175,18 @@ def phys2denoise(filename, outdir='.', if metric == 'retroicor_card': args = select_input_args(compute_retroicor_regressors, kwargs) args['card'] = True - regr['retroicor_card'] = compute_retroicor_regressors(physio, - **args) + retroicor_regrs = compute_retroicor_regressors(physio, **args) + for vslice in range(len(args['slice_timings'])): + regr[f'retroicor_card_slice-{vslice}'] = retroicor_regrs[vslice] elif metric == 'retroicor_resp': args = select_input_args(compute_retroicor_regressors, kwargs) args['resp'] = True - regr['retroicor_resp'] = compute_retroicor_regressors(physio, - **args) + retroicor_regrs = compute_retroicor_regressors(physio, **args) + for vslice in range(len(args['slice_timings'])): + regr[f'retroicor_resp_slice-{vslice}'] = retroicor_regrs[vslice] else: args = select_input_args(metric, kwargs) - regr[f'{metric}'] = metric(physio, **args) + regr[metric.__name__] = metric(physio, **args) #!# Add regressors visualisation From d76735930806d5aef617891bcf73343a38fd9c35 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 18:49:26 +0100 Subject: [PATCH 25/26] Deal better with retroicor outputs --- phys2denoise/phys2denoise.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/phys2denoise/phys2denoise.py b/phys2denoise/phys2denoise.py index 284b36c..a5ff33e 100644 --- a/phys2denoise/phys2denoise.py +++ b/phys2denoise/phys2denoise.py @@ -177,13 +177,19 @@ def phys2denoise(filename, outdir='.', args['card'] = True retroicor_regrs = compute_retroicor_regressors(physio, **args) for vslice in range(len(args['slice_timings'])): - regr[f'retroicor_card_slice-{vslice}'] = retroicor_regrs[vslice] + for harm in range(args['n_harm']): + key = f'rcor-card_s-{vslice}_hrm-{harm}' + regr[f'{key}_cos'] = retroicor_regrs[vslice][:, harm*2] + regr[f'{key}_sin'] = retroicor_regrs[vslice][:, harm*2+1] elif metric == 'retroicor_resp': args = select_input_args(compute_retroicor_regressors, kwargs) args['resp'] = True retroicor_regrs = compute_retroicor_regressors(physio, **args) for vslice in range(len(args['slice_timings'])): - regr[f'retroicor_resp_slice-{vslice}'] = retroicor_regrs[vslice] + for harm in range(args['n_harm']): + key = f'rcor-resp_s-{vslice}_hrm-{harm}' + regr[f'{key}_cos'] = retroicor_regrs[vslice][:, harm*2] + regr[f'{key}_sin'] = retroicor_regrs[vslice][:, harm*2+1] else: args = select_input_args(metric, kwargs) regr[metric.__name__] = metric(physio, **args) From 1ca87800c22cf5666329f028b964dd4047ce0c13 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Thu, 25 Feb 2021 18:51:39 +0100 Subject: [PATCH 26/26] Remove unused code --- phys2denoise/cli/run.py | 16 ---------------- phys2denoise/phys2denoise.py | 10 +++++----- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/phys2denoise/cli/run.py b/phys2denoise/cli/run.py index b6a7248..4103513 100644 --- a/phys2denoise/cli/run.py +++ b/phys2denoise/cli/run.py @@ -9,22 +9,6 @@ from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf, env -class MetricsArgDict(argparse.Action): - """ - Custom Argparse Action to create a dictionary with the metrics' arguments in parser's output. - - """ - def __call__(self, parser, namespace, values, option_strings): - if not hasattr(namespace, "metrics_arg"): - setattr(namespace, "metrics_arg", dict()) - Keys = ["sample_rate", "peaks", "throughs", "oversampling", "time_length", "onset", - "tr", "window", "lags", "nscans", "nharm"] - Vals = ["None", "None", "None", "50", "None", "0", "None", "6", "None", "1", "None"] - for k, v in zip(Keys, Vals): - getattr(namespace, "metrics_arg")[k] = v - getattr(namespace, "metrics_arg")[self.dest] = values - - def _get_parser(): """ Parse command line inputs for this function. diff --git a/phys2denoise/phys2denoise.py b/phys2denoise/phys2denoise.py index a5ff33e..0cc681e 100644 --- a/phys2denoise/phys2denoise.py +++ b/phys2denoise/phys2denoise.py @@ -22,7 +22,7 @@ from phys2denoise.cli.run import _get_parser from phys2denoise.metrics.cardiac import crf from phys2denoise.metrics.chest_belt import rpv, rv, rvt, rrf -from phys2denoise.metrics.retroicor import compute_retroicor_regressors +from phys2denoise.metrics.retroicor import retroicor from . import __version__ from .due import due, Doi @@ -173,18 +173,18 @@ def phys2denoise(filename, outdir='.', # Goes through the list of metrics and calls them for metric in metrics: if metric == 'retroicor_card': - args = select_input_args(compute_retroicor_regressors, kwargs) + args = select_input_args(retroicor, kwargs) args['card'] = True - retroicor_regrs = compute_retroicor_regressors(physio, **args) + retroicor_regrs = retroicor(physio, **args) for vslice in range(len(args['slice_timings'])): for harm in range(args['n_harm']): key = f'rcor-card_s-{vslice}_hrm-{harm}' regr[f'{key}_cos'] = retroicor_regrs[vslice][:, harm*2] regr[f'{key}_sin'] = retroicor_regrs[vslice][:, harm*2+1] elif metric == 'retroicor_resp': - args = select_input_args(compute_retroicor_regressors, kwargs) + args = select_input_args(retroicor, kwargs) args['resp'] = True - retroicor_regrs = compute_retroicor_regressors(physio, **args) + retroicor_regrs = retroicor(physio, **args) for vslice in range(len(args['slice_timings'])): for harm in range(args['n_harm']): key = f'rcor-resp_s-{vslice}_hrm-{harm}'