Repository: esi-neuroscience/syncopy

Issue:
Single trial `corr` bug
Something goes wrong with the trialdefinition for:
```python
corr = spy.connectivityanalysis(adata, method='corr', keeptrials=True)
```
when there is an odd number of samples..

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd559993..fec01324 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
 ### Fixed
 - fix bug #394 'Copying a spy.StructDict returns a dict'.
 - serializable `.cfg` #392
+- single trial cross-corr bug #446
 
 ## [2022.12]
 
diff --git a/syncopy/connectivity/ST_compRoutines.py b/syncopy/connectivity/ST_compRoutines.py
index 4c4a667c..825a6f6a 100644
--- a/syncopy/connectivity/ST_compRoutines.py
+++ b/syncopy/connectivity/ST_compRoutines.py
@@ -461,24 +461,30 @@ class CrossCovariance(ComputationalRoutine):
     def process_metadata(self, data, out):
 
         # Get trialdef array + channels from source: note, since lags are encoded
-        # in time-axis, trial offsets etc. are bogus anyway: simply take max-sample
+        # in time-axis, trial offsets etc. are bogus anyways: simply take max-sample
         # counts / 2 to fit lags
         if data.selection is not None:
             chanSec = data.selection.channel
-            trl = np.ceil(data.selection.trialdefinition / 2)
+            old_trldef = data.selection.trialdefinition
         else:
             chanSec = slice(None)
-            trl = np.ceil(data.trialdefinition / 2)
+            old_trldef = data.trialdefinition
 
-        # If trial-averaging was requested, use the first trial as reference
-        # (all trials had to have identical lengths), and average onset timings
+        # these are the new trial sizes:
+        # max lag is half the available sample number
+        trl_sizes = np.ceil(np.diff(old_trldef, axis=1)[:, 0] / 2)
+        # create sampleinfo
+        si = np.r_[0, np.cumsum(trl_sizes)]
+        sampleinfo = np.column_stack([si[:-1], si[1:]])
+        # set offset to 0 (the 0-lag)
+        trl_def = np.column_stack([sampleinfo, np.zeros(len(trl_sizes), dtype=np.float32)])
 
+        # If trial-averaging was requested, use the first trial as reference
+        # trials had to have the same shape
         if not self.keeptrials:
-            trl = trl[[0], :]
+            trl_def = trl_def[[0], :]
 
-        # set 1st entry of time axis to the 0-lag
-        trl[:, 2] = 0
-        out.trialdefinition = trl
+        out.trialdefinition = trl_def
 
         # Attach remaining meta-data
         out.samplerate = data.samplerate


Test patch:
diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py
index e818b30a..f225c412 100644
--- a/syncopy/tests/test_connectivity.py
+++ b/syncopy/tests/test_connectivity.py
@@ -578,9 +578,9 @@ class TestCSD:
 class TestCorrelation:
 
     nChannels = 5
-    nTrials = 10
+    nTrials = 50
     fs = 1000
-    nSamples = 2000   # 2s long signals
+    nSamples = 2001   # 2s long signals
 
     # -- a single harmonic with phase shifts between channels
 
@@ -613,6 +613,7 @@ class TestCorrelation:
 
     def test_corr_solution(self, **kwargs):
 
+        # `keeptrials=False` is the default here!
         corr = cafunc(data=self.data, method='corr', **kwargs)
 
         # test 0-lag autocorr is 1 for all channels
@@ -656,6 +657,15 @@ class TestCorrelation:
 
         # only plot for simple solution test
         if len(kwargs) == 0:
+
+            # test that keeptrials=False yields (almost) same results
+            # as post-hoc trial averaging
+            corr_st = cafunc(data=self.data, method='corr', keeptrials=True)
+            corr_st_trl_avg = spy.mean(corr_st, dim='trials')
+            # keeptrials=False normalizes with global signal variances
+            # hence there can be actually small differences
+            assert np.allclose(corr_st_trl_avg.data[()], corr.data[()], atol=1e-2)
+
             plot_corr(corr, 0, 0, label='corr 0-0')
             plot_corr(corr, 1, 1, label='corr 1-1')
             plot_corr(corr, 0, 2, label='corr 0-2')