Repository: physiopy/phys2bids

Issue:
Add 'auto' option for populate_phys_input chtrig parameter
<!--- Provide a general summary of the issue in the Title above -->
Users may not know which channel is the trigger channel, so it would help them to be able to auto-detect the trigger channel and, in the absence of useable information, raise an error with the names of the available channels.
## Context / Motivation
<!--- Why is this change important to you? How would you use it? -->
<!--- How can it benefit other users? -->
For any user who doesn't know/remember what their trigger channel is, they have to load the data to a `BlueprintInput` with some random number set as the trigger channel, check the channel names, and then re-load the data with the correct trigger channel index. This should streamline the process in those cases. Plus, it would simplify the standard loading procedure in that there would only be one required argument to the function.

## Possible Implementation
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
<!--- If you already have worked on the idea, please share a link to the branch in your forked project -->

- [ ] Create an alias list with "trig", "trigger", "TRIGGER", [...]
- [ ] raise a warning if the trigger channel's name does not contain any element in the alias list
- [ ] if the user didn't give a trigger channel, we look into the names and check if there is a match to any element of the alias list. If there is not, we raise an error.

Discussion:

Patch:
diff --git a/phys2bids/cli/run.py b/phys2bids/cli/run.py
index 054bc4b..0527d61 100644
--- a/phys2bids/cli/run.py
+++ b/phys2bids/cli/run.py
@@ -77,8 +77,9 @@ def _get_parser():
                           type=int,
                           help='The column number of the trigger channel. '
                                'Channel numbering starts with 1. '
-                               'Default is 1.',
-                          default=1)
+                               'Default is 0. If chtrig is left as zero phys2bids will '
+                               'perform an automatic trigger channel search by channel names.',
+                          default=0)
     optional.add_argument('-chsel', '--channel-selection',
                           dest='chsel',
                           nargs='*',
diff --git a/phys2bids/phys2bids.py b/phys2bids/phys2bids.py
index c1b4fd4..b31b6f7 100644
--- a/phys2bids/phys2bids.py
+++ b/phys2bids/phys2bids.py
@@ -44,6 +44,7 @@ from . import __version__
 from .due import due, Doi
 
 LGR = logging.getLogger(__name__)
+LGR.setLevel(logging.INFO)
 
 
 def print_summary(filename, ntp_expected, ntp_found, samp_freq, time_offset, outfile):
@@ -127,7 +128,7 @@ def print_json(outfile, samp_freq, time_offset, ch_name):
     description='The BIDS specification',
     cite_module=True)
 def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None,
-              sub=None, ses=None, chtrig=1, chsel=None, num_timepoints_expected=None,
+              sub=None, ses=None, chtrig=0, chsel=None, num_timepoints_expected=None,
               tr=None, thr=None, pad=9, ch_name=[], yml='', debug=False, quiet=False):
     """
     Run main workflow of phys2bids.
@@ -193,7 +194,6 @@ def phys2bids(filename, info=False, indir='.', outdir='.', heur_file=None,
     indir = os.path.abspath(indir)
     if chtrig < 1:
         raise Exception('Wrong trigger channel. Channel indexing starts with 1!')
-
     filename, ftype = utils.check_input_type(filename,
                                              indir)
 
diff --git a/phys2bids/physio_obj.py b/phys2bids/physio_obj.py
index 3466aa6..0eea267 100644
--- a/phys2bids/physio_obj.py
+++ b/phys2bids/physio_obj.py
@@ -4,12 +4,16 @@
 """I/O objects for phys2bids."""
 
 import logging
+import re
 from copy import deepcopy
 from itertools import groupby
 
 import numpy as np
 
+TRIGGER_NAMES = ["trig", "trigger"]
+
 LGR = logging.getLogger(__name__)
+LGR.setLevel(logging.INFO)
 
 
 def is_valid(var, var_type, list_type=None):
@@ -239,6 +243,12 @@ class BlueprintInput():
         self.num_timepoints_found = deepcopy(num_timepoints_found)
         self.thr = deepcopy(thr)
         self.time_offset = deepcopy(time_offset)
+        if trigger_idx == 0:
+            self.auto_trigger_selection()
+        else:
+            if ch_name[trigger_idx] not in TRIGGER_NAMES:
+                LGR.info('Trigger channel name is not in our trigger channel name alias list. '
+                         'Please make sure you choose the proper channel.')
 
     @property
     def ch_amount(self):
@@ -543,6 +553,52 @@ class BlueprintInput():
 
         LGR.info(info)
 
+    def auto_trigger_selection(self):
+        """
+        Find a trigger index matching the channels with a regular expresion.
+
+        It compares the channel name with the the regular expressions stored
+        in TRIGGER_NAMES.
+
+        Parameters
+        ----------
+        self
+
+        Raises
+        ------
+        Exception
+        More than one possible trigger channel was automatically found.
+
+        Exception
+        No trigger channel automatically found
+
+        Notes
+        -----
+        Outcome:
+            trigger_idx : int
+                Automatically retrieved trigger index
+        """
+        LGR.info('Running automatic trigger detection.')
+        joint_match = '§'.join(TRIGGER_NAMES)
+        indexes = []
+        for n, case in enumerate(self.ch_name):
+            name = re.split(r'(\W+|\d|_|\s)', case)
+            name = list(filter(None, name))
+
+            if re.search('|'.join(name), joint_match, re.IGNORECASE):
+                indexes = indexes + [n]
+
+        if indexes:
+            if len(indexes) > 1:
+                raise Exception('More than one possible trigger channel was automatically found. '
+                                'Please run phys2bids specifying the -chtrig argument.')
+            else:
+                self.trigger_idx = indexes[0]
+                LGR.info(f'{self.ch_name[self.trigger_idx]} selected as trigger channel')
+        else:
+            raise Exception('No trigger channel automatically found. Please run phys2bids '
+                            'specifying the -chtrig argument.')
+
 
 class BlueprintOutput():
     """


Test patch:
diff --git a/phys2bids/tests/test_phys2bids.py b/phys2bids/tests/test_phys2bids.py
index 5161e8e..c37ee3f 100644
--- a/phys2bids/tests/test_phys2bids.py
+++ b/phys2bids/tests/test_phys2bids.py
@@ -52,7 +52,7 @@ def test_raise_exception(samefreq_full_acq_file):
 
     with raises(Exception) as errorinfo:
         phys2bids.phys2bids(filename=test_filename, num_timepoints_expected=[70], tr=[1.3, 2],
-                            indir=test_path, outdir=test_path)
+                            indir=test_path, outdir=test_path, chtrig=3)
     assert "doesn't match" in str(errorinfo.value)
 
     with raises(Exception) as errorinfo:
diff --git a/phys2bids/tests/test_physio_obj.py b/phys2bids/tests/test_physio_obj.py
index b28be27..fb6d61f 100644
--- a/phys2bids/tests/test_physio_obj.py
+++ b/phys2bids/tests/test_physio_obj.py
@@ -2,7 +2,6 @@
 
 import numpy as np
 from pytest import raises
-
 from phys2bids import physio_obj as po
 
 
@@ -260,3 +259,39 @@ def test_BlueprintOutput():
 
     # Test __eq__
     assert blueprint_out == blueprint_out
+
+
+def test_auto_trigger_selection(caplog):
+    """Test auto_trigger_selection."""
+    test_time = np.array([0, 1, 2, 3, 4])
+    test_trigger = np.array([0, 1, 2, 3, 4])
+    test_half = np.array([0, 1, 2])
+    test_twice = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+    test_timeseries = [test_time, test_trigger, test_half, test_twice, test_twice, test_twice]
+    test_freq = [1, 1, 0.5, 2, 2, 2]
+    test_chn_name = ['time', 'trigger', 'half', 'CO2', 'CO 2', 'strigose']
+    test_units = ['s', 'V', 'V', 'V', 'V', 'V']
+
+    # test when trigger is not the name of the channel
+    test_chtrig = 2
+    phys_in = po.BlueprintInput(test_timeseries, test_freq, test_chn_name,
+                                test_units, test_chtrig)
+    assert 'Trigger channel name is not' in caplog.text
+
+    # test when trigger is 0 and that the trigger channel is recognized by name:
+    test_chtrig = 0
+    phys_in = po.BlueprintInput(test_timeseries, test_freq, test_chn_name,
+                                test_units, test_chtrig)
+    assert phys_in.trigger_idx == 1
+    # test when no trigger is found
+    test_chn_name = ['time', 'TRIGGAH', 'half', 'CO2', 'CO 2', 'strigose']
+    with raises(Exception) as errorinfo:
+        phys_in = po.BlueprintInput(test_timeseries, test_freq, test_chn_name,
+                                    test_units, test_chtrig)
+        assert 'No trigger channel automatically found' in str(errorinfo.value)
+    # test when no trigger is found
+    test_chn_name = ['time', 'trigger', 'TRIGGER', 'CO2', 'CO 2', 'strigose']
+    with raises(Exception) as errorinfo:
+        phys_in = po.BlueprintInput(test_timeseries, test_freq, test_chn_name,
+                                    test_units, test_chtrig)
+        assert 'More than one possible trigger channel' in str(errorinfo.value)