Repository: NeuralEnsemble/python-neo

Issue:
Neuralynx loader output for a given file differs depending on what other files are present in folder
When loading a Neuralynx data file, the timestamps assigned by neo depend on what other files are present in the same folder. 

For instance, loading a .ntt file with no other files present gives a first timestamp of 0.0 s, but loading that same file with some .ncs files also present gives a first timestamp of ~0.12 s.

Minimal working example [here](https://github.com/vandermeerlab/data-formats/blob/master/neo-nept-loading.ipynb).

I would consider this behavior undesirable because of the considerable potential for inconsistencies. The same code can now give different results depending on what files are present at the time of loading, and it is unclear how any timestamped data from different sources (e.g. spike sorters) could be aligned with the neo timestamps.

I would prefer an option, maybe even the default, to have the loader output the timestamps actually reported by the Neuralynx recording system.

Discussion:

Patch:
diff --git a/neo/io/neuralynxio.py b/neo/io/neuralynxio.py
index 463ede1e..8469c6a4 100644
--- a/neo/io/neuralynxio.py
+++ b/neo/io/neuralynxio.py
@@ -26,7 +26,26 @@ class NeuralynxIO(NeuralynxRawIO, BaseFromRaw):
     _prefered_signal_group_mode = 'group-by-same-units'
     mode = 'dir'
 
-    def __init__(self, dirname, use_cache=False, cache_path='same_as_resource'):
-        NeuralynxRawIO.__init__(self, dirname=dirname,
-                                use_cache=use_cache, cache_path=cache_path)
+    def __init__(self, dirname, use_cache=False, cache_path='same_as_resource',
+                 keep_original_times=False):
+        """
+        Initialise IO instance
+
+        Parameters
+        ----------
+        dirname : str
+            Directory containing data files
+        use_cache : bool, optional
+            Cache results of initial file scans for faster loading in subsequent runs.
+            Default: False
+        cache_path : str, optional
+            Folder path to use for cache files.
+            Default: 'same_as_resource'
+        keep_original_times : bool
+            Preserve original time stamps as in data files. By default datasets are
+            shifted to begin at t_start = 0*pq.second.
+            Default: False
+        """
+        NeuralynxRawIO.__init__(self, dirname=dirname, use_cache=use_cache,
+                                cache_path=cache_path, keep_original_times=keep_original_times)
         BaseFromRaw.__init__(self, dirname)
diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py
index 2d4bfd28..6f6f1221 100644
--- a/neo/rawio/neuralynxrawio.py
+++ b/neo/rawio/neuralynxrawio.py
@@ -50,8 +50,9 @@ class NeuralynxRawIO(BaseRawIO):
     extensions = ['nse', 'ncs', 'nev', 'ntt']
     rawmode = 'one-dir'
 
-    def __init__(self, dirname='', **kargs):
+    def __init__(self, dirname='', keep_original_times=False, **kargs):
         self.dirname = dirname
+        self.keep_original_times = keep_original_times
         BaseRawIO.__init__(self, **kargs)
 
     def _source_name(self):
@@ -239,7 +240,11 @@ class NeuralynxRawIO(BaseRawIO):
             self.global_t_start = self._sigs_t_start[0]
             self.global_t_stop = self._sigs_t_stop[-1]
 
-        # fille into header dict
+        if self.keep_original_times:
+            self.global_t_stop = self.global_t_stop - self.global_t_start
+            self.global_t_start = 0
+
+        # fill into header dict
         self.header = {}
         self.header['nb_block'] = 1
         self.header['nb_segment'] = [self._nb_segment]


Test patch:
diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py
index 6cba7426..927ae6b3 100644
--- a/neo/test/iotest/test_neuralynxio.py
+++ b/neo/test/iotest/test_neuralynxio.py
@@ -267,6 +267,29 @@ class TestData(CommonNeuralynxIOTest, unittest.TestCase):
                                            anasig.magnitude[:overlap, 0] * gain_factor_0,
                                            rtol=0.01)
 
+    def test_keep_original_spike_times(self):
+        for session in self.files_to_test:
+            dirname = self.get_filename_path(session)
+            nio = NeuralynxIO(dirname=dirname, keep_original_times=True)
+            block = nio.read_block()
+
+            for st in block.segments[0].spiketrains:
+                filename = st.file_origin.replace('original_data', 'plain_data')
+                if '.nse' in st.file_origin:
+                    filename = filename.replace('.nse', '.txt')
+                    times_column = 0
+                    plain_data = np.loadtxt(filename)[:, times_column]
+                elif '.ntt' in st.file_origin:
+                    filename = filename.replace('.ntt', '.txt')
+                    times_column = 2
+                    plain_data = np.loadtxt(filename)[:, times_column]
+                    # ntt files contain 4 rows per spike time
+                    plain_data = plain_data[::4]
+
+                times = st.rescale(pq.microsecond).magnitude
+                overlap = min(len(plain_data), len(times))
+                np.testing.assert_allclose(plain_data[:overlap], times[:overlap], rtol=1e-10)
+
 
 class TestIncompleteBlocks(CommonNeuralynxIOTest, unittest.TestCase):
     def test_incomplete_block_handling_v632(self):