From e2dec6afd3740c352da91b1b5a5cef5435ca9a24 Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 20 Nov 2019 23:54:46 +0100 Subject: [PATCH 01/42] Added heudiconv link --- phys2bids/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phys2bids/utils.py b/phys2bids/utils.py index 1bbba65ce..4d0802909 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -80,7 +80,7 @@ def load_heuristic(heuristic): References ---------- - Copied from nipy/heudiconv + Copied from [nipy/heudiconv](https://github.com/nipy/heudiconv) """ if os.path.sep in heuristic or os.path.lexists(heuristic): heuristic_file = os.path.realpath(heuristic) From f5dfaf0ebb1c90eab4780911672e7e06175c6afc Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 20 Nov 2019 23:59:39 +0100 Subject: [PATCH 02/42] Added main call --- phys2bids/phys2bids.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index e729ff1fc..3fac9bc66 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -276,3 +276,7 @@ def _main(argv=None): print_json(outfile, samp_freq, time_offset, options.table_header) print_summary(options.filename, options.num_tps_expected, num_tps_found, samp_freq, time_offset, outfile) + + +if __name__ == '__main__': + _main() \ No newline at end of file From 89240f90b93679102ebb6956848af3fc8d8ebdb1 Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 01:11:55 +0100 Subject: [PATCH 03/42] Started writing classes for phys2bids --- phys2bids/physio_obj.py | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 phys2bids/physio_obj.py diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py new file mode 100644 index 000000000..4548ec0f0 --- /dev/null +++ b/phys2bids/physio_obj.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +""" +I/O objects for phys2bids +""" + +import numpy as np + + +class phys_io(): + """ + Parent class for i/o physio objects. + + Properties + ---------- + ch_name: (ch, ) list + List of channel names - one per channel + units: (ch, ) list + list of channel frequencies - one per channel + """ + def __init__(self, ch_name, units): + self.ch_name = ch_name + self.units = units + + +class phys_input(phys_io): + """ + Main input object for phys2bids. + Contains the schema to be populated. + + Properties + ---------- + timeseries : (ch, [tps]) list + List of numpy 1d arrays - one for channel. + Contains all the timeseries recorded. + Supports different frequencies! + freq : (ch, ) list + List of floats - one per channel. + Contains all the frequencies of the recorded channel. + Support different frequencies! + """ + def __init__(self, diff_timeseries, diff_freq, ch_name, units): + super().__init__(ch_name, units) + self.timeseries = diff_timeseries + self.freq = diff_freq + + +class phys_output(phys_io): + """ + Main output object for phys2bids. + Contains the schema to be exported. + + Properties + ---------- + timeseries : (ch x tps) :obj:`numpy.ndarray` + Numpy 2d array of timeseries + Contains all the timeseries recorded. + Impose same frequency! + freq : float + Shared frequency of the object. + """ + def __init__(self, timeseries, freq, ch_name, units): + super().__init__(ch_name, ch_name, units) + self.timeseries = timeseries + self.freq = freq + \ No newline at end of file From e936b582647059e513660b8ae397cdc410aec335 Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 15:38:56 +0100 Subject: [PATCH 04/42] Start writing methods --- phys2bids/physio_obj.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 4548ec0f0..45a037df5 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -6,6 +6,14 @@ import numpy as np +def is_valid(var, type, list_type): + """ + Checks that the var is of a certain type. + If type is list and list_type is specified, + checks that the list contains + """ + + class phys_io(): """ @@ -30,7 +38,7 @@ class phys_input(phys_io): Properties ---------- - timeseries : (ch, [tps]) list + timeseries : (ch, [tps]) list List of numpy 1d arrays - one for channel. Contains all the timeseries recorded. Supports different frequencies! @@ -43,7 +51,7 @@ def __init__(self, diff_timeseries, diff_freq, ch_name, units): super().__init__(ch_name, units) self.timeseries = diff_timeseries self.freq = diff_freq - + class phys_output(phys_io): """ @@ -53,7 +61,7 @@ class phys_output(phys_io): Properties ---------- timeseries : (ch x tps) :obj:`numpy.ndarray` - Numpy 2d array of timeseries + Numpy 2d array of timeseries Contains all the timeseries recorded. Impose same frequency! freq : float From 9bc82f405b5ded060c3791d50a692acf021ac8ee Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 17:19:45 +0100 Subject: [PATCH 05/42] Turned a string into f string --- phys2bids/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phys2bids/utils.py b/phys2bids/utils.py index 4d0802909..effb9bcdd 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -32,7 +32,7 @@ def check_file_exists(file, hardexit=True): Check if file exists. """ if not os.path.isfile(file) and file is not None: - raise FileNotFoundError('The file ' + file + ' does not exist!') + raise FileNotFoundError(f'The file {file} does not exist!') def move_file(oldpath, newpath, ext=''): From afad556c999bc233bf00b2b582b64f508458edd1 Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 18:47:34 +0100 Subject: [PATCH 06/42] Finished object organisation --- phys2bids/physio_obj.py | 83 +++++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 45a037df5..6fa9fbb97 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -1,18 +1,74 @@ #!/usr/bin/env python3 """ -I/O objects for phys2bids +I/O objects for phys2bids. """ -import numpy as np +from numpy import ndarray, shape -def is_valid(var, type, list_type): + +def is_valid(var, type, list_type=None, return_var=True): """ Checks that the var is of a certain type. If type is list and list_type is specified, - checks that the list contains + checks that the list contains list_type. + Input + ----- + var: + Variable to be checked. + type: type + Type the variable is assumed to be. + list_type: type + As type. + + Output + ------ + var: + Variable to be checked (same as input). """ + if not isinstance(var, type): + raise AttributeError('Something went wrong while populating physio_io!') + + if type is list and list_type is not None: + for element in var: + is_valid(element, list_type, return_var=False) + + if return_var: + return var + + +def has_data_size(var, data, token): + """ + Checks that the var has the same dimension of the data + If it's not the case, fill in the var or removes exceding var entry. + Input + ----- + var: + Variable to be checked. + data: (ch, [tps]) list or :obj:`numpy.ndarray` + Actual timeseries data. + token: + What to be used in case of completion. + + Output + ------ + var: + Variable to be checked (same as input). + """ + if isinstance(data, list): + data_size = len(data) + elif isinstance(data, ndarray): + data_size = data.shape[0] + else: + raise Exception('Something went wrong assessing data size') + + if len(var) > data_size: + var = var[:data_size] + + if len(var) < data_size: + var = var + [token] * (data_size - len(var)) + return var class phys_io(): @@ -27,8 +83,8 @@ class phys_io(): list of channel frequencies - one per channel """ def __init__(self, ch_name, units): - self.ch_name = ch_name - self.units = units + self.ch_name = is_valid(ch_name, list, list_type=str) + self.units = is_valid(units, list, list_type=str) class phys_input(phys_io): @@ -49,8 +105,12 @@ class phys_input(phys_io): """ def __init__(self, diff_timeseries, diff_freq, ch_name, units): super().__init__(ch_name, units) - self.timeseries = diff_timeseries - self.freq = diff_freq + self.timeseries = is_valid(diff_timeseries, list, list_type=ndarray) + self.freq = has_data_size(is_valid(diff_freq, list, + list_type=(int, float)), + self.timeseries, 0) + self.ch_name = has_data_size(self.ch_name, self.timeseries, 'missing') + self.units = has_data_size(self.units, self.timeseries, '[]') class phys_output(phys_io): @@ -69,6 +129,7 @@ class phys_output(phys_io): """ def __init__(self, timeseries, freq, ch_name, units): super().__init__(ch_name, ch_name, units) - self.timeseries = timeseries - self.freq = freq - \ No newline at end of file + self.timeseries = is_valid(timeseries, ndarray) + self.freq = has_data_size(is_valid(freq, (int, float)), [1], 0) + self.ch_name = has_data_size(self.ch_name, self.timeseries, 'missing') + self.units = has_data_size(self.units, self.timeseries, '[]') From dcb24197d2a767b3898d49ca3abda24f996a6dba Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 22:12:25 +0100 Subject: [PATCH 07/42] Just flake8ed the eof --- phys2bids/phys2bids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 3fac9bc66..f9ce661d1 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -279,4 +279,4 @@ def _main(argv=None): if __name__ == '__main__': - _main() \ No newline at end of file + _main() From 79acc2764fb84db512ebd53bbde1a47e87821633 Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 22:58:37 +0100 Subject: [PATCH 08/42] Started writing interface for acq files --- phys2bids/interfaces/acq.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 phys2bids/interfaces/acq.py diff --git a/phys2bids/interfaces/acq.py b/phys2bids/interfaces/acq.py new file mode 100644 index 000000000..ea731a554 --- /dev/null +++ b/phys2bids/interfaces/acq.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +phys2bids interface for acqknowledge files. +""" +from bioread import read_file + +from phys2bids.physio_obj import blueprint_input, blueprint_output + + +def print_info_acq(filename, data): + """ + Print the info of acq files + """ + print(f'File {filename} contains:\n') + for ch in range(0, len(data)): + print(str(ch) + ': ' + data[ch].name) + + +def populate_phys_input(filename, chtrig): + """ + Populate object phys_input + """ + + data = read_file(filename).channels + phys_input = blueprint_input() + + From c6d69e378523771ea6400e8502fd2e7412350367 Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 22:59:01 +0100 Subject: [PATCH 09/42] changed name of objects --- phys2bids/physio_obj.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 6fa9fbb97..acce68157 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ I/O objects for phys2bids. @@ -71,7 +72,7 @@ def has_data_size(var, data, token): return var -class phys_io(): +class blueprint_io(): """ Parent class for i/o physio objects. @@ -87,10 +88,10 @@ def __init__(self, ch_name, units): self.units = is_valid(units, list, list_type=str) -class phys_input(phys_io): +class blueprint_input(blueprint_io): """ Main input object for phys2bids. - Contains the schema to be populated. + Contains the blueprint to be populated. Properties ---------- @@ -113,10 +114,10 @@ def __init__(self, diff_timeseries, diff_freq, ch_name, units): self.units = has_data_size(self.units, self.timeseries, '[]') -class phys_output(phys_io): +class blueprint_output(blueprint_io): """ Main output object for phys2bids. - Contains the schema to be exported. + Contains the blueprint to be exported. Properties ---------- From b94def5ae28a19083b6a557aab4f73b0110355ee Mon Sep 17 00:00:00 2001 From: smoia Date: Thu, 21 Nov 2019 23:00:25 +0100 Subject: [PATCH 10/42] Started moving acq related code into proper interface file --- phys2bids/phys2bids.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index f9ce661d1..c389dc0f0 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -31,18 +31,13 @@ from phys2bids import utils, viz from phys2bids.cli.run import _get_parser +from phys2bids.interfaces import acq # #!# This is hardcoded until we find a better solution HEADERLENGTH = 9 # #!# Different frequencies == different files! -def print_info_acq(filename, data): - print('File ' + filename + ' contains:\n') - for ch in range(0, len(data)): - print(str(ch) + ': ' + data[ch].name) - - def print_info_txt(filename): with open(filename) as txtfile: header = [next(txtfile) for x in range(HEADERLENGTH - 2)] @@ -145,7 +140,7 @@ def _main(argv=None): from bioread import read_file data = read_file(infile).channels - print_info_acq(options.filename, data) + acq.print_info_acq(options.filename, data) elif ftype == 'txt': header = print_info_txt(options.filename) @@ -256,8 +251,8 @@ def _main(argv=None): if table_width < n_headers - ignored_headers: print(f'Too many table headers specified!\n' f'{options.table_header}\n' - f'Ignoring the last' - '{n_headers - table_width - ignored_headers}') + f'Ignoring the last ' + f'{n_headers - table_width - ignored_headers}') options.table_header = options.table_header[:(table_width + ignored_headers)] elif table_width > n_headers - ignored_headers: missing_headers = n_headers - table_width - ignored_headers From b603285c4dda0f7c24f9d3a48838fa863b376b97 Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 22 Nov 2019 13:01:06 +0100 Subject: [PATCH 11/42] Removed parent class, added start_time in output --- phys2bids/physio_obj.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index acce68157..3a022510b 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -72,23 +72,7 @@ def has_data_size(var, data, token): return var -class blueprint_io(): - """ - Parent class for i/o physio objects. - - Properties - ---------- - ch_name: (ch, ) list - List of channel names - one per channel - units: (ch, ) list - list of channel frequencies - one per channel - """ - def __init__(self, ch_name, units): - self.ch_name = is_valid(ch_name, list, list_type=str) - self.units = is_valid(units, list, list_type=str) - - -class blueprint_input(blueprint_io): +class blueprint_input(): """ Main input object for phys2bids. Contains the blueprint to be populated. @@ -105,7 +89,6 @@ class blueprint_input(blueprint_io): Support different frequencies! """ def __init__(self, diff_timeseries, diff_freq, ch_name, units): - super().__init__(ch_name, units) self.timeseries = is_valid(diff_timeseries, list, list_type=ndarray) self.freq = has_data_size(is_valid(diff_freq, list, list_type=(int, float)), @@ -114,7 +97,7 @@ def __init__(self, diff_timeseries, diff_freq, ch_name, units): self.units = has_data_size(self.units, self.timeseries, '[]') -class blueprint_output(blueprint_io): +class blueprint_output(): """ Main output object for phys2bids. Contains the blueprint to be exported. @@ -128,9 +111,9 @@ class blueprint_output(blueprint_io): freq : float Shared frequency of the object. """ - def __init__(self, timeseries, freq, ch_name, units): - super().__init__(ch_name, ch_name, units) + def __init__(self, timeseries, freq, ch_name, units, start_time): self.timeseries = is_valid(timeseries, ndarray) self.freq = has_data_size(is_valid(freq, (int, float)), [1], 0) self.ch_name = has_data_size(self.ch_name, self.timeseries, 'missing') self.units = has_data_size(self.units, self.timeseries, '[]') + self.start_time = start_time From 70e69657489b464b5f8af4476cfde667334d9e7f Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 22 Nov 2019 15:55:58 +0100 Subject: [PATCH 12/42] Adapted print_info to general input object, moved into utils, improved objects, started rewriting of main for objects --- phys2bids/interfaces/acq.py | 28 +++++++++++++----------- phys2bids/phys2bids.py | 43 +++++++++++-------------------------- phys2bids/physio_obj.py | 9 ++++---- phys2bids/utils.py | 10 +++++++++ 4 files changed, 43 insertions(+), 47 deletions(-) diff --git a/phys2bids/interfaces/acq.py b/phys2bids/interfaces/acq.py index ea731a554..f6f358de8 100644 --- a/phys2bids/interfaces/acq.py +++ b/phys2bids/interfaces/acq.py @@ -9,21 +9,25 @@ from phys2bids.physio_obj import blueprint_input, blueprint_output -def print_info_acq(filename, data): - """ - Print the info of acq files - """ - print(f'File {filename} contains:\n') - for ch in range(0, len(data)): - print(str(ch) + ': ' + data[ch].name) - - def populate_phys_input(filename, chtrig): """ Populate object phys_input """ data = read_file(filename).channels - phys_input = blueprint_input() - - + + freq = [data[chtrig].samples_per_second] * 2 + timeseries = [data[chtrig].time_index, data[chtrig].data] + units = ['s', data[chtrig].units] + names = ['time','trigger'] + + k = 0 + for ch in data: + if k != chtrig: + print(f'{k:02d}. {ch}') + timeseries.append(ch.data) + freq.append(ch.samples_per_second) + units.append(ch.units) + k += 1 + + return phys_input = blueprint_input(timeseries, freq, names, units) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index c389dc0f0..27b999568 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -31,27 +31,12 @@ from phys2bids import utils, viz from phys2bids.cli.run import _get_parser -from phys2bids.interfaces import acq + # #!# This is hardcoded until we find a better solution HEADERLENGTH = 9 -# #!# Different frequencies == different files! -def print_info_txt(filename): - with open(filename) as txtfile: - header = [next(txtfile) for x in range(HEADERLENGTH - 2)] - - del header[1:4] - del header[2] - - print('File ' + filename + ' contains:\n') - for line in header: - print(line) - - return header - - def print_summary(filename, ntp_expected, ntp_found, samp_freq, time_offset, outfile): start_time = -time_offset summary = (f'------------------------------------------------\n' @@ -135,14 +120,16 @@ def _main(argv=None): utils.check_file_exists(infile) print('File exists') - # Read infos from file + # Read file! if ftype == 'acq': - from bioread import read_file - - data = read_file(infile).channels - acq.print_info_acq(options.filename, data) + from phys2bids.interfaces.acq import populate_phys_input elif ftype == 'txt': - header = print_info_txt(options.filename) + raise Exception('txt not yet supported') + else: + raise Exception('File type not yet supported') + + phys_input = populate_phys_input(infile, options.chtrig) + utils.print_info(options.filename, phys_input) # If file has to be processed, process it if not options.info: @@ -157,16 +144,8 @@ def _main(argv=None): f'Skipping BIDS formatting.') # #!# Get option of no trigger! (which is wrong practice or Respiract) - print('Reading trigger data and time index') - if ftype == 'acq': - trigger = data[options.chtrig].data - time = data[options.chtrig].time_index - elif ftype == 'txt': - # Read full file and extract right lines. - data = np.genfromtxt(options.filename, skip_header=HEADERLENGTH) - trigger = data[:, options.chtrig + 1] - time = data[:, 0] + # #!# MOVE THIS TO OBJECT METHOD! FROM HERE print('Counting trigger points') trigger_deriv = np.diff(trigger) tps = trigger_deriv > options.thr @@ -201,6 +180,8 @@ def _main(argv=None): time = time - time_offset # time = data[options.chtrig].time_index - time_offset + # #!# TO HERE + utils.path_exists_or_make_it(options.outdir) viz.plot_trigger(time, trigger, outfile, options) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 3a022510b..f661a1fe2 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -93,8 +93,9 @@ def __init__(self, diff_timeseries, diff_freq, ch_name, units): self.freq = has_data_size(is_valid(diff_freq, list, list_type=(int, float)), self.timeseries, 0) - self.ch_name = has_data_size(self.ch_name, self.timeseries, 'missing') - self.units = has_data_size(self.units, self.timeseries, '[]') + self.ch_name = has_data_size(ch_name, self.timeseries, 'missing') + self.units = has_data_size(units, self.timeseries, '[]') + self.ch_num = len(self.timeseries) class blueprint_output(): @@ -114,6 +115,6 @@ class blueprint_output(): def __init__(self, timeseries, freq, ch_name, units, start_time): self.timeseries = is_valid(timeseries, ndarray) self.freq = has_data_size(is_valid(freq, (int, float)), [1], 0) - self.ch_name = has_data_size(self.ch_name, self.timeseries, 'missing') - self.units = has_data_size(self.units, self.timeseries, '[]') + self.ch_name = has_data_size(ch_name, self.timeseries, 'missing') + self.units = has_data_size(units, self.timeseries, '[]') self.start_time = start_time diff --git a/phys2bids/utils.py b/phys2bids/utils.py index effb9bcdd..937cebc46 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -35,6 +35,16 @@ def check_file_exists(file, hardexit=True): raise FileNotFoundError(f'The file {file} does not exist!') +def print_info(filename, phys_object): + """ + Print the info of the input files, using blueprint_input object + """ + print(f'File {filename} contains:\n') + + for ch in range(2, phys_object.ch_num): + print(f'{(ch-2):02d}. {phys_object.ch_name[ch]} sampled at {phys_object.freq[ch]} Hz') + + def move_file(oldpath, newpath, ext=''): """ Moves file from oldpath to newpath. From 4497f82ddae691ef4a432b9c50c307a3038f35aa Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 22 Nov 2019 19:59:53 +0100 Subject: [PATCH 13/42] Changed default tr --- phys2bids/cli/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phys2bids/cli/run.py b/phys2bids/cli/run.py index 463b6ba57..803083315 100644 --- a/phys2bids/cli/run.py +++ b/phys2bids/cli/run.py @@ -89,7 +89,7 @@ def _get_parser(): dest='tr', type=float, help='TR of sequence in seconds.', - default=1) + default=0) optional.add_argument('-thr', '--threshold', dest='thr', type=float, From 9f4ea24436daa292d04a0591f265ed05218cd025 Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 22 Nov 2019 20:00:33 +0100 Subject: [PATCH 14/42] removed unnecessary steps and corrected export error --- phys2bids/interfaces/acq.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/phys2bids/interfaces/acq.py b/phys2bids/interfaces/acq.py index f6f358de8..97405408c 100644 --- a/phys2bids/interfaces/acq.py +++ b/phys2bids/interfaces/acq.py @@ -6,20 +6,20 @@ """ from bioread import read_file -from phys2bids.physio_obj import blueprint_input, blueprint_output +from phys2bids.physio_obj import blueprint_input def populate_phys_input(filename, chtrig): """ Populate object phys_input - """ + """ data = read_file(filename).channels - + freq = [data[chtrig].samples_per_second] * 2 timeseries = [data[chtrig].time_index, data[chtrig].data] units = ['s', data[chtrig].units] - names = ['time','trigger'] + names = ['time', 'trigger'] k = 0 for ch in data: @@ -30,4 +30,4 @@ def populate_phys_input(filename, chtrig): units.append(ch.units) k += 1 - return phys_input = blueprint_input(timeseries, freq, names, units) + return blueprint_input(timeseries, freq, names, units) From 6bb89fae22480ca866024ea5b03182d25b7410f5 Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 22 Nov 2019 20:05:09 +0100 Subject: [PATCH 15/42] Changed function has_data_size into has_size, added ch_amount to objects, added method to find offset in time in blueprint_input. --- phys2bids/physio_obj.py | 87 ++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index f661a1fe2..66f487dcf 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -5,10 +5,10 @@ I/O objects for phys2bids. """ -from numpy import ndarray, shape +import numpy as np -def is_valid(var, type, list_type=None, return_var=True): +def is_valid(var, var_type, list_type=None, return_var=True): """ Checks that the var is of a certain type. If type is list and list_type is specified, @@ -17,7 +17,7 @@ def is_valid(var, type, list_type=None, return_var=True): ----- var: Variable to be checked. - type: type + var_type: type Type the variable is assumed to be. list_type: type As type. @@ -27,10 +27,10 @@ def is_valid(var, type, list_type=None, return_var=True): var: Variable to be checked (same as input). """ - if not isinstance(var, type): - raise AttributeError('Something went wrong while populating physio_io!') + if not isinstance(var, var_type): + raise AttributeError('Something went wrong while populating blueprint') - if type is list and list_type is not None: + if var_type is list and list_type is not None: for element in var: is_valid(element, list_type, return_var=False) @@ -38,7 +38,7 @@ def is_valid(var, type, list_type=None, return_var=True): return var -def has_data_size(var, data, token): +def has_size(var, data_size, token): """ Checks that the var has the same dimension of the data If it's not the case, fill in the var or removes exceding var entry. @@ -46,8 +46,8 @@ def has_data_size(var, data, token): ----- var: Variable to be checked. - data: (ch, [tps]) list or :obj:`numpy.ndarray` - Actual timeseries data. + data_size: int + Size of data of interest. token: What to be used in case of completion. @@ -56,13 +56,6 @@ def has_data_size(var, data, token): var: Variable to be checked (same as input). """ - if isinstance(data, list): - data_size = len(data) - elif isinstance(data, ndarray): - data_size = data.shape[0] - else: - raise Exception('Something went wrong assessing data size') - if len(var) > data_size: var = var[:data_size] @@ -80,7 +73,8 @@ class blueprint_input(): Properties ---------- timeseries : (ch, [tps]) list - List of numpy 1d arrays - one for channel. + List of numpy 1d arrays - one for channel, plus one for time. + Time channel has to be the first, trigger the second. Contains all the timeseries recorded. Supports different frequencies! freq : (ch, ) list @@ -89,13 +83,51 @@ class blueprint_input(): Support different frequencies! """ def __init__(self, diff_timeseries, diff_freq, ch_name, units): - self.timeseries = is_valid(diff_timeseries, list, list_type=ndarray) - self.freq = has_data_size(is_valid(diff_freq, list, + self.timeseries = is_valid(diff_timeseries, list, list_type=np.ndarray) + self.ch_amount = len(self.timeseries) + self.freq = has_size(is_valid(diff_freq, list, list_type=(int, float)), - self.timeseries, 0) - self.ch_name = has_data_size(ch_name, self.timeseries, 'missing') - self.units = has_data_size(units, self.timeseries, '[]') - self.ch_num = len(self.timeseries) + self.ch_amount, 0) + self.ch_name = has_size(ch_name, self.ch_amount, 'unknown') + self.units = has_size(units, self.ch_amount, '[]') + + def check_trigger_amount(self, thr=2.5, num_tps_expected=0, tr=0): + """ + + """ + print('Counting trigger points') + trigger_deriv = np.diff(self.timeseries[1]) + tps = trigger_deriv > thr + num_tps_found = tps.sum() + time_offset = self.timeseries[0][tps.argmax()] + + if num_tps_expected: + print('Checking number of tps') + if num_tps_found > num_tps_expected: + tps_extra = num_tps_found - num_tps_expected + print(f'Found {tps_extra} tps more than expected!\n' + 'Assuming extra tps are at the end (try again with a ' + 'more conservative thr)') + + elif num_tps_found < num_tps_expected: + tps_missing = num_tps_expected - num_tps_found + print(f'Found {tps_missing} tps less than expected!') + if tr: + print('Correcting time offset, assuming missing tps ' + 'are at the beginning (try again with ' + 'a more liberal thr') + time_offset -= (tps_missing * tr) + else: + print('Can\'t correct time offset, (try again specifying ' + 'tr or with a more liberal thr') + + else: + print('Found just the right amount of tps!') + + else: + print('Cannot check the number of tps') + + self.timeseries[0] -= time_offset class blueprint_output(): @@ -113,8 +145,9 @@ class blueprint_output(): Shared frequency of the object. """ def __init__(self, timeseries, freq, ch_name, units, start_time): - self.timeseries = is_valid(timeseries, ndarray) - self.freq = has_data_size(is_valid(freq, (int, float)), [1], 0) - self.ch_name = has_data_size(ch_name, self.timeseries, 'missing') - self.units = has_data_size(units, self.timeseries, '[]') + self.timeseries = is_valid(timeseries, np.ndarray) + self.ch_amount = self.timeseries.shape[0] + self.freq = has_size(is_valid(freq, (int, float)), 1, 0) + self.ch_name = has_size(ch_name, self.ch_amount, 'unkown') + self.units = has_size(units, self.ch_amount, '[]') self.start_time = start_time From 600c0f9221b2f97574027b6220fa60a333bb8b38 Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 22 Nov 2019 20:06:17 +0100 Subject: [PATCH 16/42] Created new function to automatically detect extension of input file and check it's supported by phys2bids --- phys2bids/utils.py | 54 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/phys2bids/utils.py b/phys2bids/utils.py index 937cebc46..8a85fe991 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -4,19 +4,49 @@ import os import sys +SUPPORTED_FTYPES = ('acq') # , 'txt', 'mat', ... + def check_input_dir(indir): + """ + Checks that the given indir doesn't have a trailing '/' + """ if indir[-1:] == '/': indir = indir[:-1] return indir -def check_input_ext(file, ext): - if file[-len(ext):] != ext: - file = file + ext +def check_input_ext(filename, ext): + """ + Checks that the given file has the given extension + """ + if filename[-len(ext):] != ext: + filename = filename + ext - return file + return filename + + +def check_input_type(filename, indir): + """ + Check which supported type is the filename. + Alternatively, raise an error if file not found or type not supported. + """ + fftype_found = False + for ftype in SUPPORTED_FTYPES: + filename = check_input_ext(filename, ftype) + if os.path.isfile(os.path.join(indir, filename)): + fftype_found = True + break + + if fftype_found: + print(f'File extension is .{ftype}') + return filename, ftype + else: + raise Exception(f'The file {filename} wasn\'t found in {indir}' + f' or {ftype} is not supported yet.\n' + f'phys2bids currently supports:' + f' {", ".join(SUPPORTED_FTYPES)}') def path_exists_or_make_it(fldr): @@ -27,12 +57,12 @@ def path_exists_or_make_it(fldr): os.makedirs(fldr) -def check_file_exists(file, hardexit=True): +def check_file_exists(filename): """ Check if file exists. """ - if not os.path.isfile(file) and file is not None: - raise FileNotFoundError(f'The file {file} does not exist!') + if not os.path.isfile(filename) and filename is not None: + raise FileNotFoundError(f'The file {filename} does not exist!') def print_info(filename, phys_object): @@ -40,9 +70,10 @@ def print_info(filename, phys_object): Print the info of the input files, using blueprint_input object """ print(f'File {filename} contains:\n') - - for ch in range(2, phys_object.ch_num): - print(f'{(ch-2):02d}. {phys_object.ch_name[ch]} sampled at {phys_object.freq[ch]} Hz') + + for ch in range(2, phys_object.ch_amount): + print(f'{(ch-2):02d}. {phys_object.ch_name[ch]};' + f' sampled at {phys_object.freq[ch]} Hz') def move_file(oldpath, newpath, ext=''): @@ -106,7 +137,8 @@ def load_heuristic(heuristic): from importlib import import_module try: mod = import_module(f'phys2bids.heuristics.{heuristic}') - mod.filename = mod.__file__.rstrip('co') # remove c or o from pyc/pyo + # remove c or o from pyc/pyo + mod.filename = mod.__file__.rstrip('co') except Exception as exc: raise ImportError(f'Failed to import heuristic {heuristic}: {exc}') return mod From be37a345a36d9ed8de1dc83c8b238f3ddd1df73b Mon Sep 17 00:00:00 2001 From: smoia Date: Fri, 22 Nov 2019 20:06:57 +0100 Subject: [PATCH 17/42] Started adjusting to new objects and functions. --- phys2bids/phys2bids.py | 64 ++++++++---------------------------------- 1 file changed, 11 insertions(+), 53 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 27b999568..52731dcce 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -26,7 +26,6 @@ import os -import numpy as np import pandas as pd from phys2bids import utils, viz @@ -99,17 +98,10 @@ def _main(argv=None): options = _get_parser().parse_args(argv) # Check options to make them internally coherent # #!# This can probably be done while parsing? - # #!# Make filename check better somehow. options.indir = utils.check_input_dir(options.indir) options.outdir = utils.check_input_dir(options.outdir) - options.filename = utils.check_input_ext(options.filename, '.acq') - ftype = 'acq' - if not os.path.isfile(os.path.join(options.indir, options.filename)): - options.filename = utils.check_input_ext(options.filename[:-4], '.txt') - ftype = 'txt' - - # #!# Change this to cases of and better message - print(f'File extension is .{ftype}') + options.filename, ftype = utils.check_input_type(options.filename, + options.indir) if options.heur_file: options.heur_file = utils.check_input_ext(options.heur_file, '.py') @@ -117,16 +109,14 @@ def _main(argv=None): infile = os.path.join(options.indir, options.filename) outfile = os.path.join(options.outdir, os.path.basename(options.filename[:-4])) - utils.check_file_exists(infile) - print('File exists') - # Read file! if ftype == 'acq': from phys2bids.interfaces.acq import populate_phys_input elif ftype == 'txt': raise Exception('txt not yet supported') else: - raise Exception('File type not yet supported') + raise Exception('This shouldn\'t happen, check out the last few' + 'lines of code') phys_input = populate_phys_input(infile, options.chtrig) utils.print_info(options.filename, phys_input) @@ -144,49 +134,17 @@ def _main(argv=None): f'Skipping BIDS formatting.') # #!# Get option of no trigger! (which is wrong practice or Respiract) - - # #!# MOVE THIS TO OBJECT METHOD! FROM HERE - print('Counting trigger points') - trigger_deriv = np.diff(trigger) - tps = trigger_deriv > options.thr - num_tps_found = tps.sum() - time_offset = time[tps.argmax()] - - if options.num_tps_expected: - print('Checking number of tps') - if num_tps_found > options.num_tps_expected: - tps_extra = num_tps_found - options.num_tps_expected - print('Found ' + str(tps_extra) + ' tps more than expected!\n', - 'Assuming extra tps are at the end (try again with a ', - 'more conservative thr)') - elif num_tps_found < options.num_tps_expected: - tps_missing = options.num_tps_expected - num_tps_found - print('Found ' + str(tps_missing) + ' tps less than expected!') - if options.tr: - print('Correcting time offset, assuming missing tps' - 'are at the beginning') - # time_offset = time_offset - (tps_missing * options.tr) - time_offset = time[tps.argmax()] - (tps_missing * options.tr) - else: - print('Can\'t correct time offset, (try again specifying', - 'tr or with a more liberal thr') - - else: - print('Found just the right amount of tps!') - - else: - print('Not checking the number of tps') - - time = time - time_offset - # time = data[options.chtrig].time_index - time_offset - - # #!# TO HERE + phys_input.check_trigger_amount(options.thr, options.num_tps_expected, + options.tr) utils.path_exists_or_make_it(options.outdir) - viz.plot_trigger(time, trigger, outfile, options) + viz.plot_trigger(phys_input.timeseries[0], phys_input.timeseries[1], + outfile, options) - # #!# The following few lines could be a function on its own for use in python + ##### + ### + # #!# This part has to become the "output object" population table = pd.DataFrame(index=time) if ftype == 'txt': From 07f4245ae33415eb5e88ed03d8a9d42763c5e0f9 Mon Sep 17 00:00:00 2001 From: smoia Date: Mon, 25 Nov 2019 12:55:32 +0100 Subject: [PATCH 18/42] Pseudo code and docstrings --- phys2bids/phys2bids.py | 6 ++++++ phys2bids/physio_obj.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 52731dcce..669d784a0 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -145,6 +145,12 @@ def _main(argv=None): ##### ### # #!# This part has to become the "output object" population + + # Check how many different frequencies there are in the input + # Create a dictionary that has one entry per frequence + # Create an output object per entry + + table = pd.DataFrame(index=time) if ftype == 'txt': diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 66f487dcf..830812401 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -72,28 +72,49 @@ class blueprint_input(): Properties ---------- - timeseries : (ch, [tps]) list + diff_timeseries : (ch, [tps]) list List of numpy 1d arrays - one for channel, plus one for time. Time channel has to be the first, trigger the second. Contains all the timeseries recorded. Supports different frequencies! - freq : (ch, ) list + diff_freq : (ch) list of floats List of floats - one per channel. Contains all the frequencies of the recorded channel. Support different frequencies! + ch_name : (ch) list of strings + List of names of the channels - can be the header of the columns + in the output files. + units : (ch) list of strings + List of the units of the channels. + + Methods + ------- + check_trigger_amount : + Method that counts the amounts of triggers and corrects time offset. + """ def __init__(self, diff_timeseries, diff_freq, ch_name, units): self.timeseries = is_valid(diff_timeseries, list, list_type=np.ndarray) self.ch_amount = len(self.timeseries) self.freq = has_size(is_valid(diff_freq, list, - list_type=(int, float)), - self.ch_amount, 0) + list_type=(int, float)), + self.ch_amount, 0) self.ch_name = has_size(ch_name, self.ch_amount, 'unknown') self.units = has_size(units, self.ch_amount, '[]') def check_trigger_amount(self, thr=2.5, num_tps_expected=0, tr=0): """ - + Method that counts trigger points and corrects time offset. + + Input + ----- + thr: float + threshold to be used to detect trigger points. + Default is 2.5 + num_tps_expected: int + number of expected triggers (num of TRs in fMRI) + tr: float + the Repetition Time of the fMRI data. """ print('Counting trigger points') trigger_deriv = np.diff(self.timeseries[1]) From 294e1225b4719f119a5002c8e3d9ebab48177545 Mon Sep 17 00:00:00 2001 From: smoia Date: Mon, 25 Nov 2019 13:41:05 +0100 Subject: [PATCH 19/42] Completed docstrings --- phys2bids/physio_obj.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 830812401..785ce1197 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -164,6 +164,16 @@ class blueprint_output(): Impose same frequency! freq : float Shared frequency of the object. + Properties + ch_name : (ch) list of strings + List of names of the channels - can be the header of the columns + in the output files. + units : (ch) list of strings + List of the units of the channels. + start_time : float + Starting time of acquisition (equivalent to first TR, + or to the opposite sign of the time offset). + """ def __init__(self, timeseries, freq, ch_name, units, start_time): self.timeseries = is_valid(timeseries, np.ndarray) From f4223db1ef57166f57babb8d0eb0fc144ff8ae09 Mon Sep 17 00:00:00 2001 From: smoia Date: Mon, 25 Nov 2019 17:25:20 +0100 Subject: [PATCH 20/42] Added blueprint output and method to populate it from blueprint input --- phys2bids/physio_obj.py | 78 ++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 785ce1197..9603ce703 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -69,15 +69,17 @@ class blueprint_input(): """ Main input object for phys2bids. Contains the blueprint to be populated. + !!! Pay attention: there's rules on how to populate this object. + See below ("Attention") !!! - Properties - ---------- - diff_timeseries : (ch, [tps]) list + Input (Properties) + ------------------ + timeseries : (ch, [tps]) list List of numpy 1d arrays - one for channel, plus one for time. Time channel has to be the first, trigger the second. Contains all the timeseries recorded. Supports different frequencies! - diff_freq : (ch) list of floats + freq : (ch) list of floats List of floats - one per channel. Contains all the frequencies of the recorded channel. Support different frequencies! @@ -87,24 +89,47 @@ class blueprint_input(): units : (ch) list of strings List of the units of the channels. + Properties + --------------------- + ch_amount: int + Number of channels (ch). + Methods ------- check_trigger_amount : - Method that counts the amounts of triggers and corrects time offset. - + Method that counts the amounts of triggers and corrects time offset + in "time" ndarray. + + Attention + --------- + The timeseries (and as a consequence, all the other properties) + should start with an entry for time and an entry for trigger. + Both should have the same length - hence same sampling. Meaning: + - timeseries[0] → ndarray representing time + - timeseries[1] → ndarray representing trigger + - timeseries[0].shape == timeseries[1].shape + + As a consequence: + - freq[0] == freq[1] + - ch_name[0] = 'time' + - ch_name[1] = 'trigger' + - units[0] = 's' + - Actual number of channels (ANC) +1 <= ch_amount <= ANC +2 """ - def __init__(self, diff_timeseries, diff_freq, ch_name, units): - self.timeseries = is_valid(diff_timeseries, list, list_type=np.ndarray) + def __init__(self, timeseries, freq, ch_name, units): + self.timeseries = is_valid(timeseries, list, list_type=np.ndarray) self.ch_amount = len(self.timeseries) - self.freq = has_size(is_valid(diff_freq, list, + self.freq = has_size(is_valid(freq, list, list_type=(int, float)), self.ch_amount, 0) self.ch_name = has_size(ch_name, self.ch_amount, 'unknown') self.units = has_size(units, self.ch_amount, '[]') - def check_trigger_amount(self, thr=2.5, num_tps_expected=0, tr=0): + @classmethod + def check_trigger_amount(cls, thr=2.5, num_tps_expected=0, tr=0): """ - Method that counts trigger points and corrects time offset. + Method that counts trigger points and corrects time offset in + the list representing time. Input ----- @@ -117,10 +142,10 @@ def check_trigger_amount(self, thr=2.5, num_tps_expected=0, tr=0): the Repetition Time of the fMRI data. """ print('Counting trigger points') - trigger_deriv = np.diff(self.timeseries[1]) + trigger_deriv = np.diff(cls.timeseries[1]) tps = trigger_deriv > thr num_tps_found = tps.sum() - time_offset = self.timeseries[0][tps.argmax()] + time_offset = cls.timeseries[0][tps.argmax()] if num_tps_expected: print('Checking number of tps') @@ -148,7 +173,7 @@ def check_trigger_amount(self, thr=2.5, num_tps_expected=0, tr=0): else: print('Cannot check the number of tps') - self.timeseries[0] -= time_offset + cls.timeseries[0] -= time_offset class blueprint_output(): @@ -156,8 +181,8 @@ class blueprint_output(): Main output object for phys2bids. Contains the blueprint to be exported. - Properties - ---------- + Properties - Input + ------------------ timeseries : (ch x tps) :obj:`numpy.ndarray` Numpy 2d array of timeseries Contains all the timeseries recorded. @@ -174,6 +199,10 @@ class blueprint_output(): Starting time of acquisition (equivalent to first TR, or to the opposite sign of the time offset). + Methods + ------- + init_from_blueprint: + method to populate from input blueprint instead of init """ def __init__(self, timeseries, freq, ch_name, units, start_time): self.timeseries = is_valid(timeseries, np.ndarray) @@ -182,3 +211,20 @@ def __init__(self, timeseries, freq, ch_name, units, start_time): self.ch_name = has_size(ch_name, self.ch_amount, 'unkown') self.units = has_size(units, self.ch_amount, '[]') self.start_time = start_time + + @classmethod + def init_from_blueprint(cls, blueprint): + """ + Method to populate the output blueprint using blueprint_input. + + Input + ----- + blueprint: :obj: blueprint_input + the input blueprint object + """ + timeseries = np.asarray(blueprint.timeseries) + freq = blueprint.freq[0] + ch_name = blueprint.ch_name + units = blueprint.units + start_time = timeseries[0, 0] + return cls(timeseries, freq, ch_name, units, start_time) From 2d57c5a56e10bf30f86e4570e8da9c0101da44c5 Mon Sep 17 00:00:00 2001 From: smoia Date: Mon, 25 Nov 2019 21:09:56 +0100 Subject: [PATCH 21/42] added methods to delete an indexed element and to return an indexed element --- phys2bids/physio_obj.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 9603ce703..64abd8ddb 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -125,7 +125,24 @@ def __init__(self, timeseries, freq, ch_name, units): self.ch_name = has_size(ch_name, self.ch_amount, 'unknown') self.units = has_size(units, self.ch_amount, '[]') - @classmethod + def return_index(cls, idx): + """ + Method that returns all the proper list entry of the + properties of the object, given an index. + """ + return (cls.timeseries[idx], cls.ch_amount, cls.freq[idx], + cls.ch_name[idx], cls.units[idx]) + + def delete_at_index(cls, idx): + """ + Method that returns all the proper list entry of the + properties of the object, given an index. + """ + del(cls.timeseries[idx]) + del(cls.freq[idx]) + del(cls.ch_name[idx]) + del(cls.units[idx]) + def check_trigger_amount(cls, thr=2.5, num_tps_expected=0, tr=0): """ Method that counts trigger points and corrects time offset in @@ -207,11 +224,28 @@ class blueprint_output(): def __init__(self, timeseries, freq, ch_name, units, start_time): self.timeseries = is_valid(timeseries, np.ndarray) self.ch_amount = self.timeseries.shape[0] - self.freq = has_size(is_valid(freq, (int, float)), 1, 0) + self.freq = is_valid(freq, (int, float)) self.ch_name = has_size(ch_name, self.ch_amount, 'unkown') self.units = has_size(units, self.ch_amount, '[]') self.start_time = start_time + def return_index(cls, idx): + """ + Method that returns all the proper list entry of the + properties of the object, given an index. + """ + return (cls.timeseries[idx], cls.ch_amount, cls.freq, + cls.ch_name[idx], cls.units[idx], cls.start_time) + + def delete_at_index(cls, idx): + """ + Method that returns all the proper list entry of the + properties of the object, given an index. + """ + del(cls.timeseries[idx]) + del(cls.ch_name[idx]) + del(cls.units[idx]) + @classmethod def init_from_blueprint(cls, blueprint): """ From 11c689c46438a26e1fdd3590f97782e5490ff7e4 Mon Sep 17 00:00:00 2001 From: smoia Date: Mon, 25 Nov 2019 21:10:30 +0100 Subject: [PATCH 22/42] Added creation of one class property (number of tr found) --- phys2bids/physio_obj.py | 1 + 1 file changed, 1 insertion(+) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 64abd8ddb..370428b19 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -191,6 +191,7 @@ def check_trigger_amount(cls, thr=2.5, num_tps_expected=0, tr=0): print('Cannot check the number of tps') cls.timeseries[0] -= time_offset + cls.num_tps_found = num_tps_found class blueprint_output(): From 75e638729aadbc7897f2b0193da16abc412256a8 Mon Sep 17 00:00:00 2001 From: smoia Date: Mon, 25 Nov 2019 21:11:23 +0100 Subject: [PATCH 23/42] Changed heuristic to add "recording" label --- phys2bids/phys2bids.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 669d784a0..25fd644a0 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -86,7 +86,10 @@ def use_heuristic(heur_file, sub, ses, filename, outdir): heur = utils.load_heuristic(heur_file) name = heur.heur(filename[:-4], name) - heurpath = fldr + '/' + name + '_physio' + if record_label: + recording = f'_recording-{record_label}' + + heurpath = fldr + '/' + name + recording + '_physio' # for ext in ['.tsv.gz', '.json', '.log']: # move_file(outfile, heurpath, ext) os.chdir(cwd) From fd3413ea59e5308b5c4f941f00cac0876948877f Mon Sep 17 00:00:00 2001 From: smoia Date: Mon, 25 Nov 2019 21:13:03 +0100 Subject: [PATCH 24/42] Adapted channel selection for new objects, added frequency splitting and output object population, started writing real output. --- phys2bids/phys2bids.py | 158 ++++++++++++++++++++--------------------- 1 file changed, 77 insertions(+), 81 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 25fd644a0..f69191920 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -26,10 +26,12 @@ import os -import pandas as pd +from copy import deepcopy +from numpy import savetxt from phys2bids import utils, viz from phys2bids.cli.run import _get_parser +from phys2bids.physio_obj import blueprint_output # #!# This is hardcoded until we find a better solution @@ -59,7 +61,7 @@ def print_json(filename, samp_freq, time_offset, table_header): utils.writejson(filename, summary, indent=4, sort_keys=False) -def use_heuristic(heur_file, sub, ses, filename, outdir): +def use_heuristic(heur_file, sub, ses, filename, outdir, record_label=''): utils.check_file_exists(heur_file) if sub[:4] != 'sub-': @@ -121,104 +123,98 @@ def _main(argv=None): raise Exception('This shouldn\'t happen, check out the last few' 'lines of code') - phys_input = populate_phys_input(infile, options.chtrig) - utils.print_info(options.filename, phys_input) + phys_in = populate_phys_input(infile, options.chtrig) + utils.print_info(options.filename, phys_in) # If file has to be processed, process it if not options.info: + # If possible, prepare bids renaming. if options.heur_file and options.sub: utils.check_file_exists(options.heur_file) print(f'Preparing BIDS output using {options.heur_file}') outfile = use_heuristic(options.heur_file, options.sub, options.ses, options.filename, - options.outdir) + options.outdir, ) elif options.heur_file and not options.sub: print(f'While "-heur" was specified, option "-sub" was not.\n' f'Skipping BIDS formatting.') # #!# Get option of no trigger! (which is wrong practice or Respiract) - phys_input.check_trigger_amount(options.thr, options.num_tps_expected, - options.tr) + phys_in.check_trigger_amount(options.thr, options.num_tps_expected, + options.tr) utils.path_exists_or_make_it(options.outdir) - viz.plot_trigger(phys_input.timeseries[0], phys_input.timeseries[1], + viz.plot_trigger(phys_in.timeseries[0], phys_in.timeseries[1], outfile, options) - ##### - ### - # #!# This part has to become the "output object" population - - # Check how many different frequencies there are in the input - # Create a dictionary that has one entry per frequence - # Create an output object per entry - - - table = pd.DataFrame(index=time) - - if ftype == 'txt': - col_names = header[1].split('\t') - col_names[-1] = col_names[-1][:-1] - + # The next few lines remove the undesired channels from phys_in. if options.chsel: - print('Extracting desired channels') - for ch in options.chsel: - if ftype == 'acq': - table[data[ch].name] = data[ch].data - elif ftype == 'txt': - # preparing channel names from txt file - table[col_names[ch + 1]] = data[:, ch + 1] - - else: - # #!# Needs a check on different channel frequency! - print('Extracting all channels') - if ftype == 'acq': - for ch in range(0, len(data)): - table[data[ch].name] = data[ch].data - elif ftype == 'txt': - for ch in range(0, (data.shape[1] - 1)): - table[col_names[ch + 1]] = data[:, ch + 1] - - print('Extracting minor informations') - if ftype == 'acq': - samp_freq = data[0].samples_per_second - elif ftype == 'txt': - freq_list = header[0].split('\t') - samp_freq = 1 / float(freq_list[-1][:-2]) - - table.index.names = ['time'] - table_width = len(table.columns) - - if options.table_header: - if 'time' in options.table_header: - ignored_headers = 1 - else: - ignored_headers = 0 - - n_headers = len(options.table_header) - if table_width < n_headers - ignored_headers: - print(f'Too many table headers specified!\n' - f'{options.table_header}\n' - f'Ignoring the last ' - f'{n_headers - table_width - ignored_headers}') - options.table_header = options.table_header[:(table_width + ignored_headers)] - elif table_width > n_headers - ignored_headers: - missing_headers = n_headers - table_width - ignored_headers - print(f'Not enough table headers specified!\n' - f'{options.table_header}\n' - f'Tailing {missing_headers} headers') - for i in range(missing_headers): - options.table_header.append(f'missing n.{i+1}') - - table.columns = options.table_header[ignored_headers:] - # #!# Here the function viz.plot_channel should be called for the desired channels. - - print('Printing file') - table.to_csv(outfile + '.tsv.gz', sep='\t', index=True, header=False, compression='gzip') - # #!# Definitely needs check on samp_freq! - print_json(outfile, samp_freq, time_offset, options.table_header) - print_summary(options.filename, options.num_tps_expected, - num_tps_found, samp_freq, time_offset, outfile) + for i in [x for x in reversed(range(0, phys_in.ch_amount)) + if x not in options.chsel]: + phys_in.delete_at_index(i) + + # The next few lines create a dictionary of different blueprint_input + # objects, one for each unique frequency in phys_in + uniq_freq_list = set(phys_in.freq) + phys_out = {} + for uniq_freq in uniq_freq_list: + phys_out[uniq_freq] = deepcopy(phys_in) + for i in [i for i, x in enumerate(reversed(phys_in.freq)) + if x != uniq_freq]: + phys_out[uniq_freq].delete_at_index(phys_in.ch_amount-i-1) + + for uniq_freq in uniq_freq_list: + phys_out[uniq_freq] = blueprint_output.init_from_blueprint(phys_out[uniq_freq]) + + # #### + # ### This is to change channel names by manual input. + # # It definitely needs to go before! + # + # if options.table_header: + # if 'time' in options.table_header: + # ignored_headers = 1 + # else: + # ignored_headers = 0 + + # n_headers = len(options.table_header) + # if table_width < n_headers - ignored_headers: + # print(f'Too many table headers specified!\n' + # f'{options.table_header}\n' + # f'Ignoring the last ' + # f'{n_headers - table_width - ignored_headers}') + # options.table_header = options.table_header[:(table_width + + # ignored_headers)] + # elif table_width > n_headers - ignored_headers: + # missing_headers = n_headers - table_width - ignored_headers + # print(f'Not enough table headers specified!\n' + # f'{options.table_header}\n' + # f'Tailing {missing_headers} headers') + # for i in range(missing_headers): + # options.table_header.append(f'missing n.{i+1}') + + # table.columns = options.table_header[ignored_headers:] + # #!# Here the function viz.plot_channel should be called + # for the desired channels. + + output_amount = len(uniq_freq_list) + if output_amount > 1: + print(f'Found {output_amount} different frequencies in input!\n' + f'Consequently, preparing {output_amount} of output files') + + for uniq_freq in uniq_freq_list: + # ### + # ## Needs dealing with names + print('Printing file') + savetxt(outfile + '.tsv.gz', phys_out[uniq_freq].timeseries, + fmt='%.8e', delimiter='\t') + print_json(outfile, phys_out[uniq_freq].freq, + phys_out[uniq_freq].start_time, + phys_out[uniq_freq].ch_name) + print_summary(options.filename, options.num_tps_expected, + phys_out[uniq_freq].num_tps_found, + phys_in.freq, phys_out[uniq_freq].start_time, + outfile) if __name__ == '__main__': From 4f806c030a7d2a5e893902ede32e3a8e59f8de9b Mon Sep 17 00:00:00 2001 From: smoia Date: Tue, 26 Nov 2019 01:22:58 +0100 Subject: [PATCH 25/42] Renamed parser argument and changed default --- phys2bids/cli/run.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/phys2bids/cli/run.py b/phys2bids/cli/run.py index 803083315..101d84a5d 100644 --- a/phys2bids/cli/run.py +++ b/phys2bids/cli/run.py @@ -95,14 +95,12 @@ def _get_parser(): type=float, help='Threshold used for trigger detection.', default=2.5) - optional.add_argument('-tbhd', '--table-header', - dest='table_header', + optional.add_argument('-chnames', '--channel-names', + dest='ch_name', nargs='*', type=str, help='Columns header (for json file).', - # #!# Has to go to empty list - default=['time', 'respiratory_chest', 'trigger', - 'cardiac', 'respiratory_CO2', 'respiratory_O2']) + default=['']) optional.add_argument('-v', '--version', action='version', version=('%(prog)s ' + __version__)) From 28e0406ed39005f069d4cf3c19b2b0dd85bee2ab Mon Sep 17 00:00:00 2001 From: smoia Date: Tue, 26 Nov 2019 01:23:33 +0100 Subject: [PATCH 26/42] Improved docstrings, added method to rename channels in blueprint_input --- phys2bids/physio_obj.py | 82 ++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 370428b19..cf151af5c 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -96,9 +96,15 @@ class blueprint_input(): Methods ------- - check_trigger_amount : - Method that counts the amounts of triggers and corrects time offset - in "time" ndarray. + return_index: + Returns the proper list entry of all the + properties of the object, given an index. + delete_at_index: + Returns all the proper list entry of the + properties of the object, given an index. + check_trigger_amount: + Counts the amounts of triggers and corrects time offset + in "time" ndarray. Also adds property ch_amount. Attention --------- @@ -125,19 +131,53 @@ def __init__(self, timeseries, freq, ch_name, units): self.ch_name = has_size(ch_name, self.ch_amount, 'unknown') self.units = has_size(units, self.ch_amount, '[]') + def rename_channels(cls, new_names, ch_trigger=None): + """ + Renames the channels. + + Input + ----- + new_names: list of str + New names for channels. + ch_trigger: + Number of the channel containing the trigger. + """ + if 'time' in new_names: + del(new_names[new_names.index['time']]) + + if 'trigger' in new_names: + del(new_names[new_names.index['trigger']]) + elif ch_trigger: + del(new_names[ch_trigger]) + + new_names = ['time', 'trigger'] + new_names + + cls.ch_name = has_size(is_valid(new_names, list, list_type=str), + cls.ch_amount, 'unknown') + def return_index(cls, idx): """ - Method that returns all the proper list entry of the + Returns the proper list entry of all the properties of the object, given an index. + + Input + ----- + idx: int + Index of elements to return """ return (cls.timeseries[idx], cls.ch_amount, cls.freq[idx], cls.ch_name[idx], cls.units[idx]) def delete_at_index(cls, idx): """ - Method that returns all the proper list entry of the + Returns all the proper list entry of the properties of the object, given an index. - """ + + Input + ----- + idx: int or range + Index of elements to delete from all lists + """ del(cls.timeseries[idx]) del(cls.freq[idx]) del(cls.ch_name[idx]) @@ -145,18 +185,18 @@ def delete_at_index(cls, idx): def check_trigger_amount(cls, thr=2.5, num_tps_expected=0, tr=0): """ - Method that counts trigger points and corrects time offset in + Counts trigger points and corrects time offset in the list representing time. Input ----- thr: float - threshold to be used to detect trigger points. + Threshold to be used to detect trigger points. Default is 2.5 num_tps_expected: int - number of expected triggers (num of TRs in fMRI) + Number of expected triggers (num of TRs in fMRI) tr: float - the Repetition Time of the fMRI data. + The Repetition Time of the fMRI data. """ print('Counting trigger points') trigger_deriv = np.diff(cls.timeseries[1]) @@ -219,6 +259,12 @@ class blueprint_output(): Methods ------- + return_index: + Returns the proper list entry of all the + properties of the object, given an index. + delete_at_index: + Returns all the proper list entry of the + properties of the object, given an index. init_from_blueprint: method to populate from input blueprint instead of init """ @@ -232,16 +278,26 @@ def __init__(self, timeseries, freq, ch_name, units, start_time): def return_index(cls, idx): """ - Method that returns all the proper list entry of the + Returns all the proper list entry of the properties of the object, given an index. + + Input + ----- + idx: int + Index of elements to return """ return (cls.timeseries[idx], cls.ch_amount, cls.freq, cls.ch_name[idx], cls.units[idx], cls.start_time) def delete_at_index(cls, idx): """ - Method that returns all the proper list entry of the + Returns all the proper list entry of the properties of the object, given an index. + + Input + ----- + idx: int or range + Index of elements to delete from all lists """ del(cls.timeseries[idx]) del(cls.ch_name[idx]) @@ -255,7 +311,7 @@ def init_from_blueprint(cls, blueprint): Input ----- blueprint: :obj: blueprint_input - the input blueprint object + The input blueprint object """ timeseries = np.asarray(blueprint.timeseries) freq = blueprint.freq[0] From cfc333e7d85666aeec25b58a6983b506221ecd78 Mon Sep 17 00:00:00 2001 From: smoia Date: Tue, 26 Nov 2019 01:27:10 +0100 Subject: [PATCH 27/42] renamed "table_header" into "ch_name", added a check on input file existence, moved heuristics around, added renaming of channels using new classes, improved bids naming and output in case of multiple frequencies. --- phys2bids/phys2bids.py | 94 +++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index f69191920..4fc72c267 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -53,11 +53,11 @@ def print_summary(filename, ntp_expected, ntp_found, samp_freq, time_offset, out utils.writefile(outfile, '.log', summary) -def print_json(filename, samp_freq, time_offset, table_header): +def print_json(filename, samp_freq, time_offset, ch_name): start_time = -time_offset summary = dict(SamplingFrequency=samp_freq, StartTime=start_time, - Columns=table_header) + Columns=ch_name) utils.writejson(filename, summary, indent=4, sort_keys=False) @@ -110,8 +110,10 @@ def _main(argv=None): if options.heur_file: options.heur_file = utils.check_input_ext(options.heur_file, '.py') + utils.check_file_exists(options.heur_file) infile = os.path.join(options.indir, options.filename) + utils.check_file_exists(infile) outfile = os.path.join(options.outdir, os.path.basename(options.filename[:-4])) # Read file! @@ -120,43 +122,44 @@ def _main(argv=None): elif ftype == 'txt': raise Exception('txt not yet supported') else: - raise Exception('This shouldn\'t happen, check out the last few' + raise Exception('This shouldn\'t happen, check out the last few ' 'lines of code') + print('Reading the file') phys_in = populate_phys_input(infile, options.chtrig) + print('Reading infos') utils.print_info(options.filename, phys_in) + # #!# Here the function viz.plot_channel should be called + # for the desired channels. # If file has to be processed, process it if not options.info: - # If possible, prepare bids renaming. - if options.heur_file and options.sub: - utils.check_file_exists(options.heur_file) - print(f'Preparing BIDS output using {options.heur_file}') - outfile = use_heuristic(options.heur_file, options.sub, - options.ses, options.filename, - options.outdir, ) - elif options.heur_file and not options.sub: - print(f'While "-heur" was specified, option "-sub" was not.\n' - f'Skipping BIDS formatting.') # #!# Get option of no trigger! (which is wrong practice or Respiract) phys_in.check_trigger_amount(options.thr, options.num_tps_expected, options.tr) - + print('Checking that the output folder exists') utils.path_exists_or_make_it(options.outdir) - + print('Plot trigger') viz.plot_trigger(phys_in.timeseries[0], phys_in.timeseries[1], outfile, options) # The next few lines remove the undesired channels from phys_in. if options.chsel: + print('Dropping unselected channels') for i in [x for x in reversed(range(0, phys_in.ch_amount)) if x not in options.chsel]: phys_in.delete_at_index(i) + # If requested, change channel names. + if options.ch_name: + print('Renaming channels with given names') + phys_in.rename_channels(options.ch_name) + # The next few lines create a dictionary of different blueprint_input # objects, one for each unique frequency in phys_in uniq_freq_list = set(phys_in.freq) + print(f'Found {len(uniq_freq_list)} unique frequencies.') phys_out = {} for uniq_freq in uniq_freq_list: phys_out[uniq_freq] = deepcopy(phys_in) @@ -167,54 +170,43 @@ def _main(argv=None): for uniq_freq in uniq_freq_list: phys_out[uniq_freq] = blueprint_output.init_from_blueprint(phys_out[uniq_freq]) - # #### - # ### This is to change channel names by manual input. - # # It definitely needs to go before! - # - # if options.table_header: - # if 'time' in options.table_header: - # ignored_headers = 1 - # else: - # ignored_headers = 0 - - # n_headers = len(options.table_header) - # if table_width < n_headers - ignored_headers: - # print(f'Too many table headers specified!\n' - # f'{options.table_header}\n' - # f'Ignoring the last ' - # f'{n_headers - table_width - ignored_headers}') - # options.table_header = options.table_header[:(table_width + - # ignored_headers)] - # elif table_width > n_headers - ignored_headers: - # missing_headers = n_headers - table_width - ignored_headers - # print(f'Not enough table headers specified!\n' - # f'{options.table_header}\n' - # f'Tailing {missing_headers} headers') - # for i in range(missing_headers): - # options.table_header.append(f'missing n.{i+1}') - - # table.columns = options.table_header[ignored_headers:] - # #!# Here the function viz.plot_channel should be called - # for the desired channels. - output_amount = len(uniq_freq_list) if output_amount > 1: print(f'Found {output_amount} different frequencies in input!\n' f'Consequently, preparing {output_amount} of output files') + if options.heur_file and options.sub: + print(f'Preparing BIDS output using {options.heur_file}') + elif options.heur_file and not options.sub: + print(f'While "-heur" was specified, option "-sub" was not.\n' + f'Skipping BIDS formatting.') + for uniq_freq in uniq_freq_list: - # ### - # ## Needs dealing with names - print('Printing file') + # If possible, prepare bids renaming. + if options.heur_file and options.sub: + if output_amount > 1: + # Add "recording-freq" to filename if more than one freq + outfile = use_heuristic(options.heur_file, options.sub, + options.ses, options.filename, + options.outdir, uniq_freq) + else: + outfile = use_heuristic(options.heur_file, options.sub, + options.ses, options.filename, + options.outdir) + + elif output_amount > 1: + # Append "freq" to filename if more than one freq + outfile = f'outfile_{uniq_freq}' + + print('Exporting files for freq {uniq_freq}') savetxt(outfile + '.tsv.gz', phys_out[uniq_freq].timeseries, fmt='%.8e', delimiter='\t') print_json(outfile, phys_out[uniq_freq].freq, phys_out[uniq_freq].start_time, phys_out[uniq_freq].ch_name) print_summary(options.filename, options.num_tps_expected, - phys_out[uniq_freq].num_tps_found, - phys_in.freq, phys_out[uniq_freq].start_time, - outfile) + phys_in.num_tps_found, uniq_freq, + phys_out[uniq_freq].start_time, outfile) if __name__ == '__main__': From 2c61fa15eaa7c1fc3414b794dfa47461ea00ca0c Mon Sep 17 00:00:00 2001 From: smoia Date: Tue, 26 Nov 2019 13:35:52 +0100 Subject: [PATCH 28/42] Addressed @eurunuela 's review to PR #38 --- phys2bids/phys2bids.py | 18 +++---- phys2bids/physio_obj.py | 111 +++++++++++++++++++++++++++++----------- 2 files changed, 89 insertions(+), 40 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 4fc72c267..93e2538c4 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -122,8 +122,8 @@ def _main(argv=None): elif ftype == 'txt': raise Exception('txt not yet supported') else: - raise Exception('This shouldn\'t happen, check out the last few ' - 'lines of code') + # #!# We should add a logger here. + raise Exception('Currently unsupported file type.') print('Reading the file') phys_in = populate_phys_input(infile, options.chtrig) @@ -147,9 +147,9 @@ def _main(argv=None): # The next few lines remove the undesired channels from phys_in. if options.chsel: print('Dropping unselected channels') - for i in [x for x in reversed(range(0, phys_in.ch_amount)) - if x not in options.chsel]: - phys_in.delete_at_index(i) + for i in reversed(range(0, phys_in.ch_amout)): + if i not in options.chsel: + phys_in.delete_at_index(i) # If requested, change channel names. if options.ch_name: @@ -163,9 +163,9 @@ def _main(argv=None): phys_out = {} for uniq_freq in uniq_freq_list: phys_out[uniq_freq] = deepcopy(phys_in) - for i in [i for i, x in enumerate(reversed(phys_in.freq)) - if x != uniq_freq]: - phys_out[uniq_freq].delete_at_index(phys_in.ch_amount-i-1) + for i in reversed(phys_in.freq): + if i != uniq_freq: + phys_out[uniq_freq].delete_at_index(phys_in.ch_amount-i-1) for uniq_freq in uniq_freq_list: phys_out[uniq_freq] = blueprint_output.init_from_blueprint(phys_out[uniq_freq]) @@ -173,7 +173,7 @@ def _main(argv=None): output_amount = len(uniq_freq_list) if output_amount > 1: print(f'Found {output_amount} different frequencies in input!\n' - f'Consequently, preparing {output_amount} of output files') + f'Preparing {output_amount} output files') if options.heur_file and options.sub: print(f'Preparing BIDS output using {options.heur_file}') diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index cf151af5c..c1cb324da 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -20,7 +20,10 @@ def is_valid(var, var_type, list_type=None, return_var=True): var_type: type Type the variable is assumed to be. list_type: type - As type. + Like var_type, but applies to list elements. + return_var: boolean + If true, the function returns the input variable. + Useful for checking over assignment. Output ------ @@ -28,7 +31,7 @@ def is_valid(var, var_type, list_type=None, return_var=True): Variable to be checked (same as input). """ if not isinstance(var, var_type): - raise AttributeError('Something went wrong while populating blueprint') + raise AttributeError(f'The given variable is not a {var_type}') if var_type is list and list_type is not None: for element in var: @@ -44,12 +47,14 @@ def has_size(var, data_size, token): If it's not the case, fill in the var or removes exceding var entry. Input ----- - var: + var: any type Variable to be checked. data_size: int Size of data of interest. - token: - What to be used in case of completion. + token: same type as `var` + If `var` doesn't have as many elements as the data_size, + it will be padded at the end with this `token`. + It has to be the same type as var. Output ------ @@ -60,6 +65,7 @@ def has_size(var, data_size, token): var = var[:data_size] if len(var) < data_size: + is_valid(token, type(var)) var = var + [token] * (data_size - len(var)) return var @@ -90,12 +96,20 @@ class blueprint_input(): List of the units of the channels. Properties - --------------------- + ---------- ch_amount: int Number of channels (ch). + Optional properties + ------------------- + num_timepoints_found: int + Amount of timepoints found in the automatic count, + *if* check_trigger_amount() is run + Methods ------- + rename_channels: + Changes the list "ch_name" in a controlled way. return_index: Returns the proper list entry of all the properties of the object, given an index. @@ -137,6 +151,8 @@ def rename_channels(cls, new_names, ch_trigger=None): Input ----- + cls: :obj: `blueprint_input` + The object on which to operate new_names: list of str New names for channels. ch_trigger: @@ -162,8 +178,16 @@ def return_index(cls, idx): Input ----- + cls: :obj: `blueprint_input` + The object on which to operate idx: int Index of elements to return + + Output + ------ + out: tuple + Tuple containing the proper list entry of all the + properties of the object with index `idx` """ return (cls.timeseries[idx], cls.ch_amount, cls.freq[idx], cls.ch_name[idx], cls.units[idx]) @@ -175,6 +199,8 @@ def delete_at_index(cls, idx): Input ----- + cls: :obj: `blueprint_input` + The object on which to operate idx: int or range Index of elements to delete from all lists """ @@ -183,55 +209,61 @@ def delete_at_index(cls, idx): del(cls.ch_name[idx]) del(cls.units[idx]) - def check_trigger_amount(cls, thr=2.5, num_tps_expected=0, tr=0): + def check_trigger_amount(cls, thr=2.5, num_timepoints_expected=0, tr=0): """ Counts trigger points and corrects time offset in the list representing time. Input ----- + cls: :obj: `blueprint_input` + The object on which to operate thr: float Threshold to be used to detect trigger points. Default is 2.5 - num_tps_expected: int + num_timepoints_expected: int Number of expected triggers (num of TRs in fMRI) tr: float The Repetition Time of the fMRI data. """ print('Counting trigger points') trigger_deriv = np.diff(cls.timeseries[1]) - tps = trigger_deriv > thr - num_tps_found = tps.sum() - time_offset = cls.timeseries[0][tps.argmax()] - - if num_tps_expected: - print('Checking number of tps') - if num_tps_found > num_tps_expected: - tps_extra = num_tps_found - num_tps_expected - print(f'Found {tps_extra} tps more than expected!\n' - 'Assuming extra tps are at the end (try again with a ' - 'more conservative thr)') - - elif num_tps_found < num_tps_expected: - tps_missing = num_tps_expected - num_tps_found - print(f'Found {tps_missing} tps less than expected!') + timepoints = trigger_deriv > thr + num_timepoints_found = timepoints.sum() + time_offset = cls.timeseries[0][timepoints.argmax()] + + if num_timepoints_expected: + print('Checking number of timepoints') + if num_timepoints_found > num_timepoints_expected: + timepoints_extra = (num_timepoints_found - + num_timepoints_expected) + print(f'Found {timepoints_extra} timepoints' + 'more than expected!\n' + 'Assuming extra timepoints are at the end ' + '(try again with a more conservative thr)') + + elif num_timepoints_found < num_timepoints_expected: + timepoints_missing = (num_timepoints_expected - + num_timepoints_found) + print(f'Found {timepoints_missing} timepoints' + 'less than expected!') if tr: - print('Correcting time offset, assuming missing tps ' - 'are at the beginning (try again with ' + print('Correcting time offset, assuming missing timepoints' + ' are at the beginning (try again with ' 'a more liberal thr') - time_offset -= (tps_missing * tr) + time_offset -= (timepoints_missing * tr) else: print('Can\'t correct time offset, (try again specifying ' 'tr or with a more liberal thr') else: - print('Found just the right amount of tps!') + print('Found just the right amount of timepoints!') else: - print('Cannot check the number of tps') + print('Cannot check the number of timepoints') cls.timeseries[0] -= time_offset - cls.num_tps_found = num_tps_found + cls.num_timepoints_found = num_timepoints_found class blueprint_output(): @@ -272,7 +304,7 @@ def __init__(self, timeseries, freq, ch_name, units, start_time): self.timeseries = is_valid(timeseries, np.ndarray) self.ch_amount = self.timeseries.shape[0] self.freq = is_valid(freq, (int, float)) - self.ch_name = has_size(ch_name, self.ch_amount, 'unkown') + self.ch_name = has_size(ch_name, self.ch_amount, 'unknown') self.units = has_size(units, self.ch_amount, '[]') self.start_time = start_time @@ -283,8 +315,16 @@ def return_index(cls, idx): Input ----- + cls: :obj: `blueprint_output` + The object on which to operate idx: int Index of elements to return + + Output + ------ + out: tuple + Tuple containing the proper list entry of all the + properties of the object with index `idx` """ return (cls.timeseries[idx], cls.ch_amount, cls.freq, cls.ch_name[idx], cls.units[idx], cls.start_time) @@ -296,6 +336,8 @@ def delete_at_index(cls, idx): Input ----- + cls: :obj: `blueprint_output` + The object on which to operate idx: int or range Index of elements to delete from all lists """ @@ -310,8 +352,15 @@ def init_from_blueprint(cls, blueprint): Input ----- - blueprint: :obj: blueprint_input + cls: :obj: `blueprint_output` + The object on which to operate + blueprint: :obj: `blueprint_input` The input blueprint object + + Output + ------ + cls: :obj: `blueprint_output` + Populated `blueprint_output` object. """ timeseries = np.asarray(blueprint.timeseries) freq = blueprint.freq[0] From 9b139d6e776b98a97e2a79451c952a2a9333f03a Mon Sep 17 00:00:00 2001 From: smoia Date: Tue, 26 Nov 2019 16:53:27 +0100 Subject: [PATCH 29/42] Added more docstrings, Keep addressing @eurunuela's review to PR #38, added heudiconv copyright and license --- phys2bids/phys2bids.py | 86 ++++++++++++++++++++++++++++++++++++++++- phys2bids/physio_obj.py | 41 ++++++++++++++++++-- phys2bids/utils.py | 8 ++++ 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 93e2538c4..8f6a98527 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -39,6 +39,31 @@ def print_summary(filename, ntp_expected, ntp_found, samp_freq, time_offset, outfile): + """ + Prints a summary onscreen and in file with informations on the files. + + Input + ----- + filename: str + Name of the input of phys2bids. + ntp_expected: int + Number of expected timepoints, as defined by user. + ntp_found: int + Number of timepoints found with the automatic process. + samp_freq: float + Frequency of sampling for the output file. + time_offset: float + Difference between beginning of file and first TR. + outfile: str or path + Fullpath to output file. + + Outcome + ------- + summary: str + Prints the summary on screen + outfile: .log file + File containing summary + """ start_time = -time_offset summary = (f'------------------------------------------------\n' f'Filename: {filename}\n' @@ -53,16 +78,61 @@ def print_summary(filename, ntp_expected, ntp_found, samp_freq, time_offset, out utils.writefile(outfile, '.log', summary) -def print_json(filename, samp_freq, time_offset, ch_name): +def print_json(outfile, samp_freq, time_offset, ch_name): + """ + Prints the json required by BIDS format. + + Input + ----- + 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. + + Outcome + ------- + + outfile: .json file + File containing information for BIDS. + """ start_time = -time_offset summary = dict(SamplingFrequency=samp_freq, StartTime=start_time, Columns=ch_name) - utils.writejson(filename, summary, indent=4, sort_keys=False) + utils.writejson(outfile, summary, indent=4, sort_keys=False) def use_heuristic(heur_file, sub, ses, filename, outdir, record_label=''): utils.check_file_exists(heur_file) + """ + Import the heuristic file specified by the user and uses its output + to rename the file. + + Input + ----- + heur_file: str or path + Fullpath to heuristic file. + sub: str or int + Name of subject. + ses: str or int or None + Name of session. + filename: str + Name of the input of phys2bids. + outdir: str or path + Path to the directory that will become the "site" folder + ("root" folder of BIDS database). + record_label: str + Optional label for the "record" entry of BIDS. + + Output + ------- + heurpath: str or path + Returned fullpath to tsv.gz new file (post BIDS formatting). + """ if sub[:4] != 'sub-': name = 'sub-' + sub @@ -100,6 +170,15 @@ def use_heuristic(heur_file, sub, ses, filename, outdir, record_label=''): def _main(argv=None): + """ + Main workflow of phys2bids. + Runs the parser, does some checks on input, then imports + the right interface file to read the input. If only info is required, + it returns a summary onscreen. + Otherwise, it operates on the input to return a .tsv.gz file, possibily + in BIDS format. + + """ options = _get_parser().parse_args(argv) # Check options to make them internally coherent # #!# This can probably be done while parsing? @@ -167,6 +246,9 @@ def _main(argv=None): if i != uniq_freq: phys_out[uniq_freq].delete_at_index(phys_in.ch_amount-i-1) + # Create a blueprint_output object for each unique frequency found. + # Populate it with the corresponding blueprint input and replace it + # in the dictionary. for uniq_freq in uniq_freq_list: phys_out[uniq_freq] = blueprint_output.init_from_blueprint(phys_out[uniq_freq]) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index c1cb324da..9b0b6df0a 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -15,7 +15,7 @@ def is_valid(var, var_type, list_type=None, return_var=True): checks that the list contains list_type. Input ----- - var: + var: any type Variable to be checked. var_type: type Type the variable is assumed to be. @@ -27,7 +27,7 @@ def is_valid(var, var_type, list_type=None, return_var=True): Output ------ - var: + var: any type Variable to be checked (same as input). """ if not isinstance(var, var_type): @@ -58,7 +58,7 @@ def has_size(var, data_size, token): Output ------ - var: + var: any type Variable to be checked (same as input). """ if len(var) > data_size: @@ -147,7 +147,8 @@ def __init__(self, timeseries, freq, ch_name, units): def rename_channels(cls, new_names, ch_trigger=None): """ - Renames the channels. + Renames the channels. If 'time' or 'trigger' were specified, + it makes sure that they're the first and second entry. Input ----- @@ -157,6 +158,11 @@ def rename_channels(cls, new_names, ch_trigger=None): New names for channels. ch_trigger: Number of the channel containing the trigger. + + Outcome + ------- + cls.ch_name: + Changes content to new_name. """ if 'time' in new_names: del(new_names[new_names.index['time']]) @@ -203,6 +209,12 @@ def delete_at_index(cls, idx): The object on which to operate idx: int or range Index of elements to delete from all lists + + Outcome + ------- + cls: + In all the property that are lists, the element correspondent to + `idx` gets deleted """ del(cls.timeseries[idx]) del(cls.freq[idx]) @@ -225,8 +237,23 @@ def check_trigger_amount(cls, thr=2.5, num_timepoints_expected=0, tr=0): Number of expected triggers (num of TRs in fMRI) tr: float The Repetition Time of the fMRI data. + + Output + ------ + cls.num_timepoints_found: int + Property of the `blueprint_input` class. + Contains the number of timepoints found + with the automatic estimation. + + Outcome + ------- + cls.timeseries: + The property `timeseries` is shifted with the 0 being + the time of first trigger. """ print('Counting trigger points') + # Use first derivative of the trigger channel to find the TRs, + # comparing it to a given threshold. trigger_deriv = np.diff(cls.timeseries[1]) timepoints = trigger_deriv > thr num_timepoints_found = timepoints.sum() @@ -340,6 +367,12 @@ def delete_at_index(cls, idx): The object on which to operate idx: int or range Index of elements to delete from all lists + + Outcome + ------- + cls: + In all the property that are lists, the element correspondent to + `idx` gets deleted """ del(cls.timeseries[idx]) del(cls.ch_name[idx]) diff --git a/phys2bids/utils.py b/phys2bids/utils.py index 8a85fe991..905887df9 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -105,11 +105,18 @@ def copy_file(oldpath, newpath, ext=''): def writefile(filename, ext, text): + """ + Produces a textfile of the specified extension `ext`, + containing the given content `text` + """ with open(filename + ext, 'w') as text_file: print(text, file=text_file) def writejson(filename, data, **kwargs): + """ + Outputs a json file with the given data inside. + """ if not filename.endswith('.json'): filename += '.json' with open(filename, 'w') as out: @@ -122,6 +129,7 @@ def load_heuristic(heuristic): References ---------- Copied from [nipy/heudiconv](https://github.com/nipy/heudiconv) + Copyright [2014-2019] [Heudiconv developers], Apache 2 license. """ if os.path.sep in heuristic or os.path.lexists(heuristic): heuristic_file = os.path.realpath(heuristic) From c924c325bab30ecd7bca29a5c5106035903e3235 Mon Sep 17 00:00:00 2001 From: smoia Date: Tue, 26 Nov 2019 23:36:58 +0100 Subject: [PATCH 30/42] Addressed @rmarkello 's reviews in PR #38 --- phys2bids/cli/run.py | 4 +- phys2bids/interfaces/acq.py | 8 +- phys2bids/phys2bids.py | 155 ++++++++++++++++++------------------ phys2bids/physio_obj.py | 101 ++++++++++++----------- phys2bids/utils.py | 20 +++-- 5 files changed, 144 insertions(+), 144 deletions(-) diff --git a/phys2bids/cli/run.py b/phys2bids/cli/run.py index 101d84a5d..4edf18e54 100644 --- a/phys2bids/cli/run.py +++ b/phys2bids/cli/run.py @@ -73,7 +73,7 @@ def _get_parser(): type=int, help=('The number corresponding to the trigger channel.' ' Channel numbering starts with 0'), - default=1) + default=0) optional.add_argument('-chsel', '--channel-selection', dest='chsel', nargs='*', @@ -100,7 +100,7 @@ def _get_parser(): nargs='*', type=str, help='Columns header (for json file).', - default=['']) + default=None) optional.add_argument('-v', '--version', action='version', version=('%(prog)s ' + __version__)) diff --git a/phys2bids/interfaces/acq.py b/phys2bids/interfaces/acq.py index 97405408c..7ef73710e 100644 --- a/phys2bids/interfaces/acq.py +++ b/phys2bids/interfaces/acq.py @@ -6,7 +6,7 @@ """ from bioread import read_file -from phys2bids.physio_obj import blueprint_input +from phys2bids.physio_obj import BlueprintInput def populate_phys_input(filename, chtrig): @@ -21,13 +21,11 @@ def populate_phys_input(filename, chtrig): units = ['s', data[chtrig].units] names = ['time', 'trigger'] - k = 0 - for ch in data: + for k, ch in enumerate(data): if k != chtrig: print(f'{k:02d}. {ch}') timeseries.append(ch.data) freq.append(ch.samples_per_second) units.append(ch.units) - k += 1 - return blueprint_input(timeseries, freq, names, units) + return BlueprintInput(timeseries, freq, names, units) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 8f6a98527..5667a818e 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -31,7 +31,7 @@ from phys2bids import utils, viz from phys2bids.cli.run import _get_parser -from phys2bids.physio_obj import blueprint_output +from phys2bids.physio_obj import BlueprintOutput # #!# This is hardcoded until we find a better solution @@ -193,16 +193,17 @@ def _main(argv=None): infile = os.path.join(options.indir, options.filename) utils.check_file_exists(infile) - outfile = os.path.join(options.outdir, os.path.basename(options.filename[:-4])) + outfile = os.path.join(options.outdir, + os.path.basename(os.path.splitext(options.filename))) # Read file! if ftype == 'acq': from phys2bids.interfaces.acq import populate_phys_input elif ftype == 'txt': - raise Exception('txt not yet supported') + raise NotImplementedError('txt not yet supported') else: # #!# We should add a logger here. - raise Exception('Currently unsupported file type.') + raise NotImplementedError('Currently unsupported file type.') print('Reading the file') phys_in = populate_phys_input(infile, options.chtrig) @@ -211,84 +212,84 @@ def _main(argv=None): # #!# Here the function viz.plot_channel should be called # for the desired channels. - # If file has to be processed, process it - if not options.info: - - # #!# Get option of no trigger! (which is wrong practice or Respiract) - phys_in.check_trigger_amount(options.thr, options.num_tps_expected, - options.tr) - print('Checking that the output folder exists') - utils.path_exists_or_make_it(options.outdir) - print('Plot trigger') - viz.plot_trigger(phys_in.timeseries[0], phys_in.timeseries[1], - outfile, options) - - # The next few lines remove the undesired channels from phys_in. - if options.chsel: - print('Dropping unselected channels') - for i in reversed(range(0, phys_in.ch_amout)): - if i not in options.chsel: - phys_in.delete_at_index(i) - - # If requested, change channel names. - if options.ch_name: - print('Renaming channels with given names') - phys_in.rename_channels(options.ch_name) - - # The next few lines create a dictionary of different blueprint_input - # objects, one for each unique frequency in phys_in - uniq_freq_list = set(phys_in.freq) - print(f'Found {len(uniq_freq_list)} unique frequencies.') - phys_out = {} - for uniq_freq in uniq_freq_list: - phys_out[uniq_freq] = deepcopy(phys_in) - for i in reversed(phys_in.freq): - if i != uniq_freq: - phys_out[uniq_freq].delete_at_index(phys_in.ch_amount-i-1) - - # Create a blueprint_output object for each unique frequency found. + # If only info were asked, end here. + if options.info: + return + + # Run analysis on trigger channel to get first timepoint and the time offset. + # #!# Get option of no trigger! (which is wrong practice or Respiract) + phys_in.check_trigger_amount(options.thr, options.num_tps_expected, + options.tr) + print('Checking that the output folder exists') + utils.path_exists_or_make_it(options.outdir) + print('Plot trigger') + viz.plot_trigger(phys_in.timeseries[0], phys_in.timeseries[1], + outfile, options) + + # The next few lines remove the undesired channels from phys_in. + if options.chsel: + print('Dropping unselected channels') + for i in reversed(range(0, phys_in.ch_amout)): + if i not in options.chsel: + phys_in.delete_at_index(i) + + # If requested, change channel names. + if options.ch_name is not None: + print('Renaming channels with given names') + phys_in.rename_channels(options.ch_name) + + # The next few lines create a dictionary of different BlueprintInput + # objects, one for each unique frequency in phys_in + uniq_freq_list = set(phys_in.freq) + output_amount = len(uniq_freq_list) + if output_amount > 1: + print(f'Found {output_amount} different frequencies in input!') + + print(f'Preparing {output_amount} output files.') + phys_out = {} + for uniq_freq in uniq_freq_list: + phys_out[uniq_freq] = deepcopy(phys_in) + for i in reversed(phys_in.freq): + if i != uniq_freq: + phys_out[uniq_freq].delete_at_index(phys_in.ch_amount-i-1) + + # Also create a BlueprintOutput object for each unique frequency found. # Populate it with the corresponding blueprint input and replace it # in the dictionary. - for uniq_freq in uniq_freq_list: - phys_out[uniq_freq] = blueprint_output.init_from_blueprint(phys_out[uniq_freq]) + phys_out[uniq_freq] = BlueprintOutput.init_from_blueprint(phys_out[uniq_freq]) - output_amount = len(uniq_freq_list) - if output_amount > 1: - print(f'Found {output_amount} different frequencies in input!\n' - f'Preparing {output_amount} output files') + if options.heur_file and options.sub: + print(f'Preparing BIDS output using {options.heur_file}') + elif options.heur_file and not options.sub: + print(f'While "-heur" was specified, option "-sub" was not.\n' + f'Skipping BIDS formatting.') + for uniq_freq in uniq_freq_list: + # If possible, prepare bids renaming. if options.heur_file and options.sub: - print(f'Preparing BIDS output using {options.heur_file}') - elif options.heur_file and not options.sub: - print(f'While "-heur" was specified, option "-sub" was not.\n' - f'Skipping BIDS formatting.') - - for uniq_freq in uniq_freq_list: - # If possible, prepare bids renaming. - if options.heur_file and options.sub: - if output_amount > 1: - # Add "recording-freq" to filename if more than one freq - outfile = use_heuristic(options.heur_file, options.sub, - options.ses, options.filename, - options.outdir, uniq_freq) - else: - outfile = use_heuristic(options.heur_file, options.sub, - options.ses, options.filename, - options.outdir) - - elif output_amount > 1: - # Append "freq" to filename if more than one freq - outfile = f'outfile_{uniq_freq}' - - print('Exporting files for freq {uniq_freq}') - savetxt(outfile + '.tsv.gz', phys_out[uniq_freq].timeseries, - fmt='%.8e', delimiter='\t') - print_json(outfile, phys_out[uniq_freq].freq, - phys_out[uniq_freq].start_time, - phys_out[uniq_freq].ch_name) - print_summary(options.filename, options.num_tps_expected, - phys_in.num_tps_found, uniq_freq, - phys_out[uniq_freq].start_time, outfile) + if output_amount > 1: + # Add "recording-freq" to filename if more than one freq + outfile = use_heuristic(options.heur_file, options.sub, + options.ses, options.filename, + options.outdir, uniq_freq) + else: + outfile = use_heuristic(options.heur_file, options.sub, + options.ses, options.filename, + options.outdir) + + elif output_amount > 1: + # Append "freq" to filename if more than one freq + outfile = f'outfile_{uniq_freq}' + + print('Exporting files for freq {uniq_freq}') + savetxt(outfile + '.tsv.gz', phys_out[uniq_freq].timeseries, + fmt='%.8e', delimiter='\t') + print_json(outfile, phys_out[uniq_freq].freq, + phys_out[uniq_freq].start_time, + phys_out[uniq_freq].ch_name) + print_summary(options.filename, options.num_tps_expected, + phys_in.num_tps_found, uniq_freq, + phys_out[uniq_freq].start_time, outfile) if __name__ == '__main__': diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 9b0b6df0a..66574296f 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -8,7 +8,7 @@ import numpy as np -def is_valid(var, var_type, list_type=None, return_var=True): +def is_valid(var, var_type, list_type=None): """ Checks that the var is of a certain type. If type is list and list_type is specified, @@ -21,9 +21,6 @@ def is_valid(var, var_type, list_type=None, return_var=True): Type the variable is assumed to be. list_type: type Like var_type, but applies to list elements. - return_var: boolean - If true, the function returns the input variable. - Useful for checking over assignment. Output ------ @@ -35,10 +32,9 @@ def is_valid(var, var_type, list_type=None, return_var=True): if var_type is list and list_type is not None: for element in var: - is_valid(element, list_type, return_var=False) + _ = is_valid(element, list_type, return_var=False) - if return_var: - return var + return var def has_size(var, data_size, token): @@ -47,7 +43,7 @@ def has_size(var, data_size, token): If it's not the case, fill in the var or removes exceding var entry. Input ----- - var: any type + var: list Variable to be checked. data_size: int Size of data of interest. @@ -58,20 +54,20 @@ def has_size(var, data_size, token): Output ------ - var: any type + var: list Variable to be checked (same as input). """ if len(var) > data_size: var = var[:data_size] if len(var) < data_size: - is_valid(token, type(var)) + _ = is_valid(token, type(var[0])) var = var + [token] * (data_size - len(var)) return var -class blueprint_input(): +class BlueprintInput(): """ Main input object for phys2bids. Contains the blueprint to be populated. @@ -145,14 +141,14 @@ def __init__(self, timeseries, freq, ch_name, units): self.ch_name = has_size(ch_name, self.ch_amount, 'unknown') self.units = has_size(units, self.ch_amount, '[]') - def rename_channels(cls, new_names, ch_trigger=None): + def rename_channels(self, new_names, ch_trigger=None): """ Renames the channels. If 'time' or 'trigger' were specified, it makes sure that they're the first and second entry. Input ----- - cls: :obj: `blueprint_input` + self: :obj: `BlueprintInput` The object on which to operate new_names: list of str New names for channels. @@ -161,7 +157,7 @@ def rename_channels(cls, new_names, ch_trigger=None): Outcome ------- - cls.ch_name: + self.ch_name: Changes content to new_name. """ if 'time' in new_names: @@ -174,17 +170,17 @@ def rename_channels(cls, new_names, ch_trigger=None): new_names = ['time', 'trigger'] + new_names - cls.ch_name = has_size(is_valid(new_names, list, list_type=str), - cls.ch_amount, 'unknown') + self.ch_name = has_size(is_valid(new_names, list, list_type=str), + self.ch_amount, 'unknown') - def return_index(cls, idx): + def return_index(self, idx): """ Returns the proper list entry of all the properties of the object, given an index. Input ----- - cls: :obj: `blueprint_input` + self: :obj: `BlueprintInput` The object on which to operate idx: int Index of elements to return @@ -195,40 +191,41 @@ def return_index(cls, idx): Tuple containing the proper list entry of all the properties of the object with index `idx` """ - return (cls.timeseries[idx], cls.ch_amount, cls.freq[idx], - cls.ch_name[idx], cls.units[idx]) + return (self.timeseries[idx], self.ch_amount, self.freq[idx], + self.ch_name[idx], self.units[idx]) - def delete_at_index(cls, idx): + def delete_at_index(self, idx): """ Returns all the proper list entry of the properties of the object, given an index. Input ----- - cls: :obj: `blueprint_input` + self: :obj: `BlueprintInput` The object on which to operate idx: int or range Index of elements to delete from all lists Outcome ------- - cls: + self: In all the property that are lists, the element correspondent to `idx` gets deleted """ - del(cls.timeseries[idx]) - del(cls.freq[idx]) - del(cls.ch_name[idx]) - del(cls.units[idx]) + del(self.timeseries[idx]) + del(self.freq[idx]) + del(self.ch_name[idx]) + del(self.units[idx]) + self.ch_amount -= 1 - def check_trigger_amount(cls, thr=2.5, num_timepoints_expected=0, tr=0): + def check_trigger_amount(self, thr=2.5, num_timepoints_expected=0, tr=0): """ Counts trigger points and corrects time offset in the list representing time. Input ----- - cls: :obj: `blueprint_input` + self: :obj: `BlueprintInput` The object on which to operate thr: float Threshold to be used to detect trigger points. @@ -240,24 +237,24 @@ def check_trigger_amount(cls, thr=2.5, num_timepoints_expected=0, tr=0): Output ------ - cls.num_timepoints_found: int - Property of the `blueprint_input` class. + self.num_timepoints_found: int + Property of the `BlueprintInput` class. Contains the number of timepoints found with the automatic estimation. Outcome ------- - cls.timeseries: + self.timeseries: The property `timeseries` is shifted with the 0 being the time of first trigger. """ print('Counting trigger points') # Use first derivative of the trigger channel to find the TRs, # comparing it to a given threshold. - trigger_deriv = np.diff(cls.timeseries[1]) + trigger_deriv = np.diff(self.timeseries[1]) timepoints = trigger_deriv > thr num_timepoints_found = timepoints.sum() - time_offset = cls.timeseries[0][timepoints.argmax()] + time_offset = self.timeseries[0][timepoints.argmax()] if num_timepoints_expected: print('Checking number of timepoints') @@ -289,11 +286,11 @@ def check_trigger_amount(cls, thr=2.5, num_timepoints_expected=0, tr=0): else: print('Cannot check the number of timepoints') - cls.timeseries[0] -= time_offset - cls.num_timepoints_found = num_timepoints_found + self.timeseries[0] -= time_offset + self.num_timepoints_found = num_timepoints_found -class blueprint_output(): +class BlueprintOutput(): """ Main output object for phys2bids. Contains the blueprint to be exported. @@ -335,14 +332,14 @@ def __init__(self, timeseries, freq, ch_name, units, start_time): self.units = has_size(units, self.ch_amount, '[]') self.start_time = start_time - def return_index(cls, idx): + def return_index(self, idx): """ Returns all the proper list entry of the properties of the object, given an index. Input ----- - cls: :obj: `blueprint_output` + self: :obj: `BlueprintOutput` The object on which to operate idx: int Index of elements to return @@ -353,47 +350,47 @@ def return_index(cls, idx): Tuple containing the proper list entry of all the properties of the object with index `idx` """ - return (cls.timeseries[idx], cls.ch_amount, cls.freq, - cls.ch_name[idx], cls.units[idx], cls.start_time) + return (self.timeseries[idx], self.ch_amount, self.freq, + self.ch_name[idx], self.units[idx], self.start_time) - def delete_at_index(cls, idx): + def delete_at_index(self, idx): """ Returns all the proper list entry of the properties of the object, given an index. Input ----- - cls: :obj: `blueprint_output` + self: :obj: `BlueprintOutput` The object on which to operate idx: int or range Index of elements to delete from all lists Outcome ------- - cls: + self: In all the property that are lists, the element correspondent to `idx` gets deleted """ - del(cls.timeseries[idx]) - del(cls.ch_name[idx]) - del(cls.units[idx]) + del(self.timeseries[idx]) + del(self.ch_name[idx]) + del(self.units[idx]) @classmethod def init_from_blueprint(cls, blueprint): """ - Method to populate the output blueprint using blueprint_input. + Method to populate the output blueprint using BlueprintInput. Input ----- - cls: :obj: `blueprint_output` + cls: :obj: `BlueprintOutput` The object on which to operate - blueprint: :obj: `blueprint_input` + blueprint: :obj: `BlueprintInput` The input blueprint object Output ------ - cls: :obj: `blueprint_output` - Populated `blueprint_output` object. + cls: :obj: `BlueprintOutput` + Populated `BlueprintOutput` object. """ timeseries = np.asarray(blueprint.timeseries) freq = blueprint.freq[0] diff --git a/phys2bids/utils.py b/phys2bids/utils.py index 905887df9..62ce65d67 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -4,6 +4,8 @@ import os import sys +from pathlib import Path + SUPPORTED_FTYPES = ('acq') # , 'txt', 'mat', ... @@ -21,10 +23,12 @@ def check_input_ext(filename, ext): """ Checks that the given file has the given extension """ - if filename[-len(ext):] != ext: - filename = filename + ext - - return filename + if '.gz' in ext: + if filename[-len(ext):] != ext: + filename = filename + ext + return filename + else: + return Path(filename).with_suffix(ext) def check_input_type(filename, indir): @@ -34,14 +38,14 @@ def check_input_type(filename, indir): """ fftype_found = False for ftype in SUPPORTED_FTYPES: - filename = check_input_ext(filename, ftype) - if os.path.isfile(os.path.join(indir, filename)): + fname = check_input_ext(filename, ftype) + if os.path.isfile(os.path.join(indir, fname)): fftype_found = True break if fftype_found: print(f'File extension is .{ftype}') - return filename, ftype + return fname, ftype else: raise Exception(f'The file {filename} wasn\'t found in {indir}' f' or {ftype} is not supported yet.\n' @@ -67,7 +71,7 @@ def check_file_exists(filename): def print_info(filename, phys_object): """ - Print the info of the input files, using blueprint_input object + Print the info of the input files, using BlueprintInput object """ print(f'File {filename} contains:\n') From e641231724f251dcb272a85cfc58a238fc3f8012 Mon Sep 17 00:00:00 2001 From: smoia Date: Tue, 26 Nov 2019 23:39:03 +0100 Subject: [PATCH 31/42] Changed ch_name default from Nonetype to empty list --- phys2bids/cli/run.py | 2 +- phys2bids/phys2bids.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/phys2bids/cli/run.py b/phys2bids/cli/run.py index 4edf18e54..b2d3f7c1e 100644 --- a/phys2bids/cli/run.py +++ b/phys2bids/cli/run.py @@ -100,7 +100,7 @@ def _get_parser(): nargs='*', type=str, help='Columns header (for json file).', - default=None) + default=[]) optional.add_argument('-v', '--version', action='version', version=('%(prog)s ' + __version__)) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 5667a818e..13fc107ee 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -234,7 +234,7 @@ def _main(argv=None): phys_in.delete_at_index(i) # If requested, change channel names. - if options.ch_name is not None: + if options.ch_name: print('Renaming channels with given names') phys_in.rename_channels(options.ch_name) From d355bbdd3cc45cda5f840425fff212b98fc19809 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 27 Nov 2019 15:09:12 +0100 Subject: [PATCH 32/42] Assignment of empty `recording` Co-Authored-By: Vicente Ferrer <38909338+vinferrer@users.noreply.github.com> --- phys2bids/phys2bids.py | 1 + 1 file changed, 1 insertion(+) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 13fc107ee..01aeca742 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -158,6 +158,7 @@ def use_heuristic(heur_file, sub, ses, filename, outdir, record_label=''): heur = utils.load_heuristic(heur_file) name = heur.heur(filename[:-4], name) +recording = '' if record_label: recording = f'_recording-{record_label}' From b970ae3ee17246dbe0180b1a97e7adba6a30593e Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 27 Nov 2019 16:57:55 +0100 Subject: [PATCH 33/42] Addressing @vinferrer 's review in PR #38, moving method from `utils.py` `physio_obj.py`. --- phys2bids/phys2bids.py | 2 +- phys2bids/physio_obj.py | 23 +++++++++++++++++++++++ phys2bids/utils.py | 11 ----------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 01aeca742..4393dec1f 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -209,7 +209,7 @@ def _main(argv=None): print('Reading the file') phys_in = populate_phys_input(infile, options.chtrig) print('Reading infos') - utils.print_info(options.filename, phys_in) + phys_in.print_info(options.filename) # #!# Here the function viz.plot_channel should be called # for the desired channels. diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 66574296f..ecd72e60c 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -289,6 +289,29 @@ def check_trigger_amount(self, thr=2.5, num_timepoints_expected=0, tr=0): self.timeseries[0] -= time_offset self.num_timepoints_found = num_timepoints_found + def print_info(self, filename): + """ + Print info on the file, channel by channel. + + Input + ----- + self: :obj: `BlueprintInput` + The object on which to operate + filename: str or path + Name of the input file to phys2bids + + Outcome + ------- + ch: + Returns to stdout (e.g. on screen) channels, + their names and their sampling rate. + """ + print(f'File {filename} contains:\n') + + for ch in range(2, self.ch_amount): + print(f'{(ch-2):02d}. {self.ch_name[ch]};' + f' sampled at {self.freq[ch]} Hz') + class BlueprintOutput(): """ diff --git a/phys2bids/utils.py b/phys2bids/utils.py index 62ce65d67..479332e11 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -69,17 +69,6 @@ def check_file_exists(filename): raise FileNotFoundError(f'The file {filename} does not exist!') -def print_info(filename, phys_object): - """ - Print the info of the input files, using BlueprintInput object - """ - print(f'File {filename} contains:\n') - - for ch in range(2, phys_object.ch_amount): - print(f'{(ch-2):02d}. {phys_object.ch_name[ch]};' - f' sampled at {phys_object.freq[ch]} Hz') - - def move_file(oldpath, newpath, ext=''): """ Moves file from oldpath to newpath. From a18b2b715f29c41c274cd5708a2935da2031075c Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 27 Nov 2019 17:03:37 +0100 Subject: [PATCH 34/42] Keep addressing @vinferrer 's PR #38 review --- phys2bids/physio_obj.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index ecd72e60c..6aaa1750d 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -408,7 +408,8 @@ def init_from_blueprint(cls, blueprint): cls: :obj: `BlueprintOutput` The object on which to operate blueprint: :obj: `BlueprintInput` - The input blueprint object + The input blueprint object. + !!! All its frequencies should be the same !!! Output ------ From a257472d5b0da698c47af4fe31f30d56993b0238 Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 27 Nov 2019 18:28:16 +0100 Subject: [PATCH 35/42] Bug fix due to external commit --- phys2bids/phys2bids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 4393dec1f..734a522ed 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -158,7 +158,7 @@ def use_heuristic(heur_file, sub, ses, filename, outdir, record_label=''): heur = utils.load_heuristic(heur_file) name = heur.heur(filename[:-4], name) -recording = '' + recording = '' if record_label: recording = f'_recording-{record_label}' From 056e0e15a87055d57343ea726350082cc850fa76 Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 27 Nov 2019 19:11:34 +0100 Subject: [PATCH 36/42] Bug fixes --- phys2bids/interfaces/acq.py | 1 + phys2bids/phys2bids.py | 2 +- phys2bids/physio_obj.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/phys2bids/interfaces/acq.py b/phys2bids/interfaces/acq.py index 7ef73710e..d3554b6e3 100644 --- a/phys2bids/interfaces/acq.py +++ b/phys2bids/interfaces/acq.py @@ -27,5 +27,6 @@ def populate_phys_input(filename, chtrig): timeseries.append(ch.data) freq.append(ch.samples_per_second) units.append(ch.units) + names.append(ch.name) return BlueprintInput(timeseries, freq, names, units) diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py index 734a522ed..be7dbf39b 100644 --- a/phys2bids/phys2bids.py +++ b/phys2bids/phys2bids.py @@ -195,7 +195,7 @@ def _main(argv=None): infile = os.path.join(options.indir, options.filename) utils.check_file_exists(infile) outfile = os.path.join(options.outdir, - os.path.basename(os.path.splitext(options.filename))) + os.path.splitext(os.path.basename(options.filename))[0]) # Read file! if ftype == 'acq': diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py index 6aaa1750d..c9b285de9 100644 --- a/phys2bids/physio_obj.py +++ b/phys2bids/physio_obj.py @@ -32,7 +32,7 @@ def is_valid(var, var_type, list_type=None): if var_type is list and list_type is not None: for element in var: - _ = is_valid(element, list_type, return_var=False) + _ = is_valid(element, list_type,) return var From d896d5b667b8baffd0db68d5092041e1d8ed04ff Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 27 Nov 2019 19:12:22 +0100 Subject: [PATCH 37/42] Added check on extension entry in check_input_ext prior to Path().with_suffix() --- phys2bids/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/phys2bids/utils.py b/phys2bids/utils.py index 479332e11..6dc1782cf 100644 --- a/phys2bids/utils.py +++ b/phys2bids/utils.py @@ -6,7 +6,7 @@ from pathlib import Path -SUPPORTED_FTYPES = ('acq') # , 'txt', 'mat', ... +SUPPORTED_FTYPES = ('acq', ) # 'txt', 'mat', ... def check_input_dir(indir): @@ -28,6 +28,9 @@ def check_input_ext(filename, ext): filename = filename + ext return filename else: + if ext[0] != '.': + ext = '.' + ext + return Path(filename).with_suffix(ext) From 61b7f322dfbc69a94d65dc391d91aafae312ba00 Mon Sep 17 00:00:00 2001 From: smoia Date: Wed, 27 Nov 2019 19:12:37 +0100 Subject: [PATCH 38/42] Added heuristic for acq test file --- phys2bids/heuristics/heur_test_acq.py | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 phys2bids/heuristics/heur_test_acq.py diff --git a/phys2bids/heuristics/heur_test_acq.py b/phys2bids/heuristics/heur_test_acq.py new file mode 100644 index 000000000..a1e182b9b --- /dev/null +++ b/phys2bids/heuristics/heur_test_acq.py @@ -0,0 +1,54 @@ +import sys +import fnmatch + + +def heur(physinfo, name, task='', acq='', direct='', rec='', run=''): + # ############################## # + # ## Modify here! ## # + # ## ## # + # ## Possible variables are: ## # + # ## -task (required) ## # + # ## -run ## # + # ## -rec ## # + # ## -acq ## # + # ## -direct ## # + # ## ## # + # ## ## # + # ## See example below ## # + # ############################## # + + if fnmatch.fnmatchcase(physinfo, '*samefreq*.acq'): + task = 'test' + run = '00' + rec = 'biopac' + elif physinfo == 'Example': + task = 'rest' + run = '01' + acq = 'resp' + # ############################## # + # ## Don't modify below this! ## # + # ############################## # + else: + # #!# Transform sys.exit in debug warnings or raiseexceptions! + # #!# Make all of the above a dictionary + sys.exit() + + if not task: + sys.exit() + + name = name + '_task-' + task + + # filename spec: sub-