We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Print heartbeat log messages
Some steps take quite a while to complete without printing log messages. This causes issues with CircleCI, which may believe the job is stuck; but it's also an issue for users, who cannot be sure that we're still operational either.

The least we should do is periodically print some heartbeat messages.

Better yet would be some useful status info.

Affected steps are e.g. the time-by-time decoding with our ds000246 configuration.

cc @larsoner 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py`
Content:
```
1 """Decode time-by-time using a "sliding" estimator.
2 
3 A sliding estimator fits a separate logistic regression model for every time
4 point. The end result is an averaging effect across sensors.
5 
6 This approach is different from the one taken in the decoding script for
7 entire epochs. Here, the classifier is traines on the entire epoch, and hence
8 can learn about the entire time course of the signal.
9 """
10 
11 ###############################################################################
12 # Let us first import the libraries
13 
14 import os.path as op
15 import itertools
16 from types import SimpleNamespace
17 
18 import numpy as np
19 import pandas as pd
20 from scipy.io import savemat
21 
22 import mne
23 from mne.decoding import (
24     GeneralizingEstimator, SlidingEstimator, cross_val_multiscore
25 )
26 
27 from mne_bids import BIDSPath
28 
29 from sklearn.preprocessing import StandardScaler
30 from sklearn.pipeline import make_pipeline
31 from sklearn.model_selection import StratifiedKFold
32 
33 from ..._config_utils import (
34     get_sessions, get_subjects, get_task, get_datatype, get_eeg_reference,
35     get_deriv_root, _restrict_analyze_channels, get_decoding_contrasts,
36 )
37 from ..._decoding import LogReg
38 from ..._logging import gen_log_kwargs, logger
39 from ..._run import failsafe_run, save_logs
40 from ..._parallel import get_parallel_backend, get_n_jobs
41 
42 
43 def get_input_fnames_time_decoding(**kwargs):
44     cfg = kwargs.pop('cfg')
45     subject = kwargs.pop('subject')
46     session = kwargs.pop('session')
47     # TODO: Somehow remove these?
48     del kwargs['condition1']
49     del kwargs['condition2']
50     assert len(kwargs) == 0, kwargs.keys()
51     del kwargs
52     # TODO: Shouldn't this at least use the PTP-rejected epochs if available?
53     fname_epochs = BIDSPath(subject=subject,
54                             session=session,
55                             task=cfg.task,
56                             acquisition=cfg.acq,
57                             run=None,
58                             recording=cfg.rec,
59                             space=cfg.space,
60                             suffix='epo',
61                             extension='.fif',
62                             datatype=cfg.datatype,
63                             root=cfg.deriv_root,
64                             check=False)
65     in_files = dict()
66     in_files['epochs'] = fname_epochs
67     return in_files
68 
69 
70 @failsafe_run(script_path=__file__,
71               get_input_fnames=get_input_fnames_time_decoding)
72 def run_time_decoding(*, cfg, subject, condition1, condition2, session,
73                       in_files):
74     msg = f'Contrasting conditions: {condition1} – {condition2}'
75     logger.info(**gen_log_kwargs(message=msg, subject=subject,
76                                  session=session))
77     out_files = dict()
78     bids_path = in_files['epochs'].copy()
79 
80     epochs = mne.read_epochs(in_files.pop('epochs'))
81     _restrict_analyze_channels(epochs, cfg)
82 
83     # We define the epochs and the labels
84     if isinstance(cfg.conditions, dict):
85         epochs_conds = [cfg.conditions[condition1],
86                         cfg.conditions[condition2]]
87         cond_names = [condition1, condition2]
88     else:
89         epochs_conds = cond_names = [condition1, condition2]
90         epochs_conds = [condition1, condition2]
91 
92     # We have to use this approach because the conditions could be based on
93     # metadata selection, so simply using epochs[conds[0], conds[1]] would
94     # not work.
95     epochs = mne.concatenate_epochs([epochs[epochs_conds[0]],
96                                      epochs[epochs_conds[1]]])
97     n_cond1 = len(epochs[epochs_conds[0]])
98     n_cond2 = len(epochs[epochs_conds[1]])
99 
100     decim = cfg.decoding_time_generalization_decim
101     if cfg.decoding_time_generalization and decim > 1:
102         epochs.decimate(decim, verbose='error')
103 
104     X = epochs.get_data()
105     y = np.r_[np.ones(n_cond1), np.zeros(n_cond2)]
106     with get_parallel_backend(cfg):
107         clf = make_pipeline(
108             StandardScaler(),
109             LogReg(
110                 solver='liblinear',  # much faster than the default
111                 random_state=cfg.random_state,
112                 n_jobs=1,
113             )
114         )
115         cv = StratifiedKFold(
116             shuffle=True,
117             random_state=cfg.random_state,
118             n_splits=cfg.decoding_n_splits,
119         )
120 
121         if cfg.decoding_time_generalization:
122             estimator = GeneralizingEstimator(
123                 clf,
124                 scoring=cfg.decoding_metric,
125                 n_jobs=cfg.n_jobs,
126             )
127             cv_scoring_n_jobs = 1
128         else:
129             estimator = SlidingEstimator(
130                 clf,
131                 scoring=cfg.decoding_metric,
132                 n_jobs=1,
133             )
134             cv_scoring_n_jobs = cfg.n_jobs
135 
136         scores = cross_val_multiscore(
137             estimator, X=X, y=y, cv=cv, n_jobs=cv_scoring_n_jobs
138         )
139 
140         # let's save the scores now
141         a_vs_b = f'{cond_names[0]}+{cond_names[1]}'.replace(op.sep, '')
142         processing = f'{a_vs_b}+TimeByTime+{cfg.decoding_metric}'
143         processing = processing.replace('_', '-').replace('-', '')
144         mat_key = f'mat_{processing}'
145         out_files[mat_key] = bids_path.copy().update(
146             suffix='decoding', processing=processing, extension='.mat')
147         savemat(
148             out_files[mat_key],
149             {
150                 'scores': scores,
151                 'times': epochs.times,
152                 'decim': decim,
153             }
154         )
155 
156         if cfg.decoding_time_generalization:
157             # Only store the mean scores for the diagonal in the TSV file –
158             # we still have all time generalization results in the MAT file
159             # we just saved.
160             mean_crossval_score = np.diag(scores.mean(axis=0))
161         else:
162             mean_crossval_score = scores.mean(axis=0)
163 
164         out_files[f'tsv_{processing}'] = out_files[mat_key].copy().update(
165             extension='.tsv')
166         tabular_data = pd.DataFrame(
167             dict(cond_1=[cond_names[0]] * len(epochs.times),
168                  cond_2=[cond_names[1]] * len(epochs.times),
169                  time=epochs.times,
170                  mean_crossval_score=mean_crossval_score,
171                  metric=[cfg.decoding_metric] * len(epochs.times))
172         )
173         tabular_data.to_csv(
174             out_files[f'tsv_{processing}'], sep='\t', index=False)
175     assert len(in_files) == 0, in_files.keys()
176     return out_files
177 
178 
179 def get_config(
180     *,
181     config,
182 ) -> SimpleNamespace:
183     cfg = SimpleNamespace(
184         task=get_task(config),
185         datatype=get_datatype(config),
186         acq=config.acq,
187         rec=config.rec,
188         space=config.space,
189         deriv_root=get_deriv_root(config),
190         conditions=config.conditions,
191         contrasts=get_decoding_contrasts(config),
192         decode=config.decode,
193         decoding_metric=config.decoding_metric,
194         decoding_n_splits=config.decoding_n_splits,
195         decoding_time_generalization=config.decoding_time_generalization,
196         decoding_time_generalization_decim=config.decoding_time_generalization_decim,  # noqa: E501
197         random_state=config.random_state,
198         analyze_channels=config.analyze_channels,
199         ch_types=config.ch_types,
200         eeg_reference=get_eeg_reference(config),
201         n_jobs=get_n_jobs(config),
202         parallel_backend=config.parallel_backend,
203         interactive=config.interactive,
204         N_JOBS=config.N_JOBS,
205     )
206     return cfg
207 
208 
209 def main():
210     """Run time-by-time decoding."""
211     import config
212     if not config.contrasts:
213         msg = 'No contrasts specified; not performing decoding.'
214         logger.info(**gen_log_kwargs(message=msg))
215         return
216 
217     if not config.decode:
218         msg = 'No decoding requested by user.'
219         logger.info(**gen_log_kwargs(message=msg))
220         return
221 
222     # Here we go parallel inside the :class:`mne.decoding.SlidingEstimator`
223     # so we don't dispatch manually to multiple jobs.
224     logs = []
225     for subject, session, (cond_1, cond_2) in itertools.product(
226         get_subjects(config),
227         get_sessions(config),
228         get_decoding_contrasts(config)
229     ):
230         log = run_time_decoding(
231             cfg=get_config(config=config),
232             subject=subject,
233             condition1=cond_1,
234             condition2=cond_2,
235             session=session,
236         )
237         logs.append(log)
238     save_logs(config=config, logs=logs)
239 
240 
241 if __name__ == '__main__':
242     main()
243
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py b/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py
--- a/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py
+++ b/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py
@@ -37,7 +37,8 @@
 from ..._decoding import LogReg
 from ..._logging import gen_log_kwargs, logger
 from ..._run import failsafe_run, save_logs
-from ..._parallel import get_parallel_backend, get_n_jobs
+from ..._parallel import (
+    get_parallel_backend, get_n_jobs, get_parallel_backend_name)
 
 
 def get_input_fnames_time_decoding(**kwargs):
@@ -71,7 +72,11 @@
               get_input_fnames=get_input_fnames_time_decoding)
 def run_time_decoding(*, cfg, subject, condition1, condition2, session,
                       in_files):
-    msg = f'Contrasting conditions: {condition1} – {condition2}'
+    if cfg.decoding_time_generalization:
+        kind = 'time generalization'
+    else:
+        kind = 'sliding estimator'
+    msg = f'Contrasting conditions ({kind}): {condition1} – {condition2}'
     logger.info(**gen_log_kwargs(message=msg, subject=subject,
                                  session=session))
     out_files = dict()
@@ -103,6 +108,8 @@
 
     X = epochs.get_data()
     y = np.r_[np.ones(n_cond1), np.zeros(n_cond2)]
+    # ProgressBar does not work on dask, so only enable it if not using dask
+    verbose = get_parallel_backend_name(cfg) != "dask"
     with get_parallel_backend(cfg):
         clf = make_pipeline(
             StandardScaler(),
@@ -134,7 +141,8 @@
             cv_scoring_n_jobs = cfg.n_jobs
 
         scores = cross_val_multiscore(
-            estimator, X=X, y=y, cv=cv, n_jobs=cv_scoring_n_jobs
+            estimator, X=X, y=y, cv=cv, n_jobs=cv_scoring_n_jobs,
+            verbose=verbose,  # ensure ProgressBar is shown (can be slow)
         )
 
         # let's save the scores now

{"golden_diff": "diff --git a/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py b/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py\n--- a/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py\n+++ b/mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py\n@@ -37,7 +37,8 @@\n from ..._decoding import LogReg\n from ..._logging import gen_log_kwargs, logger\n from ..._run import failsafe_run, save_logs\n-from ..._parallel import get_parallel_backend, get_n_jobs\n+from ..._parallel import (\n+    get_parallel_backend, get_n_jobs, get_parallel_backend_name)\n \n \n def get_input_fnames_time_decoding(**kwargs):\n@@ -71,7 +72,11 @@\n               get_input_fnames=get_input_fnames_time_decoding)\n def run_time_decoding(*, cfg, subject, condition1, condition2, session,\n                       in_files):\n-    msg = f'Contrasting conditions: {condition1} \u2013 {condition2}'\n+    if cfg.decoding_time_generalization:\n+        kind = 'time generalization'\n+    else:\n+        kind = 'sliding estimator'\n+    msg = f'Contrasting conditions ({kind}): {condition1} \u2013 {condition2}'\n     logger.info(**gen_log_kwargs(message=msg, subject=subject,\n                                  session=session))\n     out_files = dict()\n@@ -103,6 +108,8 @@\n \n     X = epochs.get_data()\n     y = np.r_[np.ones(n_cond1), np.zeros(n_cond2)]\n+    # ProgressBar does not work on dask, so only enable it if not using dask\n+    verbose = get_parallel_backend_name(cfg) != \"dask\"\n     with get_parallel_backend(cfg):\n         clf = make_pipeline(\n             StandardScaler(),\n@@ -134,7 +141,8 @@\n             cv_scoring_n_jobs = cfg.n_jobs\n \n         scores = cross_val_multiscore(\n-            estimator, X=X, y=y, cv=cv, n_jobs=cv_scoring_n_jobs\n+            estimator, X=X, y=y, cv=cv, n_jobs=cv_scoring_n_jobs,\n+            verbose=verbose,  # ensure ProgressBar is shown (can be slow)\n         )\n \n         # let's save the scores now\n", "issue": "Print heartbeat log messages\nSome steps take quite a while to complete without printing log messages. This causes issues with CircleCI, which may believe the job is stuck; but it's also an issue for users, who cannot be sure that we're still operational either.\r\n\r\nThe least we should do is periodically print some heartbeat messages.\r\n\r\nBetter yet would be some useful status info.\r\n\r\nAffected steps are e.g. the time-by-time decoding with our ds000246 configuration.\r\n\r\ncc @larsoner \n", "before_files": [{"content": "\"\"\"Decode time-by-time using a \"sliding\" estimator.\n\nA sliding estimator fits a separate logistic regression model for every time\npoint. The end result is an averaging effect across sensors.\n\nThis approach is different from the one taken in the decoding script for\nentire epochs. Here, the classifier is traines on the entire epoch, and hence\ncan learn about the entire time course of the signal.\n\"\"\"\n\n###############################################################################\n# Let us first import the libraries\n\nimport os.path as op\nimport itertools\nfrom types import SimpleNamespace\n\nimport numpy as np\nimport pandas as pd\nfrom scipy.io import savemat\n\nimport mne\nfrom mne.decoding import (\n    GeneralizingEstimator, SlidingEstimator, cross_val_multiscore\n)\n\nfrom mne_bids import BIDSPath\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.model_selection import StratifiedKFold\n\nfrom ..._config_utils import (\n    get_sessions, get_subjects, get_task, get_datatype, get_eeg_reference,\n    get_deriv_root, _restrict_analyze_channels, get_decoding_contrasts,\n)\nfrom ..._decoding import LogReg\nfrom ..._logging import gen_log_kwargs, logger\nfrom ..._run import failsafe_run, save_logs\nfrom ..._parallel import get_parallel_backend, get_n_jobs\n\n\ndef get_input_fnames_time_decoding(**kwargs):\n    cfg = kwargs.pop('cfg')\n    subject = kwargs.pop('subject')\n    session = kwargs.pop('session')\n    # TODO: Somehow remove these?\n    del kwargs['condition1']\n    del kwargs['condition2']\n    assert len(kwargs) == 0, kwargs.keys()\n    del kwargs\n    # TODO: Shouldn't this at least use the PTP-rejected epochs if available?\n    fname_epochs = BIDSPath(subject=subject,\n                            session=session,\n                            task=cfg.task,\n                            acquisition=cfg.acq,\n                            run=None,\n                            recording=cfg.rec,\n                            space=cfg.space,\n                            suffix='epo',\n                            extension='.fif',\n                            datatype=cfg.datatype,\n                            root=cfg.deriv_root,\n                            check=False)\n    in_files = dict()\n    in_files['epochs'] = fname_epochs\n    return in_files\n\n\n@failsafe_run(script_path=__file__,\n              get_input_fnames=get_input_fnames_time_decoding)\ndef run_time_decoding(*, cfg, subject, condition1, condition2, session,\n                      in_files):\n    msg = f'Contrasting conditions: {condition1} \u2013 {condition2}'\n    logger.info(**gen_log_kwargs(message=msg, subject=subject,\n                                 session=session))\n    out_files = dict()\n    bids_path = in_files['epochs'].copy()\n\n    epochs = mne.read_epochs(in_files.pop('epochs'))\n    _restrict_analyze_channels(epochs, cfg)\n\n    # We define the epochs and the labels\n    if isinstance(cfg.conditions, dict):\n        epochs_conds = [cfg.conditions[condition1],\n                        cfg.conditions[condition2]]\n        cond_names = [condition1, condition2]\n    else:\n        epochs_conds = cond_names = [condition1, condition2]\n        epochs_conds = [condition1, condition2]\n\n    # We have to use this approach because the conditions could be based on\n    # metadata selection, so simply using epochs[conds[0], conds[1]] would\n    # not work.\n    epochs = mne.concatenate_epochs([epochs[epochs_conds[0]],\n                                     epochs[epochs_conds[1]]])\n    n_cond1 = len(epochs[epochs_conds[0]])\n    n_cond2 = len(epochs[epochs_conds[1]])\n\n    decim = cfg.decoding_time_generalization_decim\n    if cfg.decoding_time_generalization and decim > 1:\n        epochs.decimate(decim, verbose='error')\n\n    X = epochs.get_data()\n    y = np.r_[np.ones(n_cond1), np.zeros(n_cond2)]\n    with get_parallel_backend(cfg):\n        clf = make_pipeline(\n            StandardScaler(),\n            LogReg(\n                solver='liblinear',  # much faster than the default\n                random_state=cfg.random_state,\n                n_jobs=1,\n            )\n        )\n        cv = StratifiedKFold(\n            shuffle=True,\n            random_state=cfg.random_state,\n            n_splits=cfg.decoding_n_splits,\n        )\n\n        if cfg.decoding_time_generalization:\n            estimator = GeneralizingEstimator(\n                clf,\n                scoring=cfg.decoding_metric,\n                n_jobs=cfg.n_jobs,\n            )\n            cv_scoring_n_jobs = 1\n        else:\n            estimator = SlidingEstimator(\n                clf,\n                scoring=cfg.decoding_metric,\n                n_jobs=1,\n            )\n            cv_scoring_n_jobs = cfg.n_jobs\n\n        scores = cross_val_multiscore(\n            estimator, X=X, y=y, cv=cv, n_jobs=cv_scoring_n_jobs\n        )\n\n        # let's save the scores now\n        a_vs_b = f'{cond_names[0]}+{cond_names[1]}'.replace(op.sep, '')\n        processing = f'{a_vs_b}+TimeByTime+{cfg.decoding_metric}'\n        processing = processing.replace('_', '-').replace('-', '')\n        mat_key = f'mat_{processing}'\n        out_files[mat_key] = bids_path.copy().update(\n            suffix='decoding', processing=processing, extension='.mat')\n        savemat(\n            out_files[mat_key],\n            {\n                'scores': scores,\n                'times': epochs.times,\n                'decim': decim,\n            }\n        )\n\n        if cfg.decoding_time_generalization:\n            # Only store the mean scores for the diagonal in the TSV file \u2013\n            # we still have all time generalization results in the MAT file\n            # we just saved.\n            mean_crossval_score = np.diag(scores.mean(axis=0))\n        else:\n            mean_crossval_score = scores.mean(axis=0)\n\n        out_files[f'tsv_{processing}'] = out_files[mat_key].copy().update(\n            extension='.tsv')\n        tabular_data = pd.DataFrame(\n            dict(cond_1=[cond_names[0]] * len(epochs.times),\n                 cond_2=[cond_names[1]] * len(epochs.times),\n                 time=epochs.times,\n                 mean_crossval_score=mean_crossval_score,\n                 metric=[cfg.decoding_metric] * len(epochs.times))\n        )\n        tabular_data.to_csv(\n            out_files[f'tsv_{processing}'], sep='\\t', index=False)\n    assert len(in_files) == 0, in_files.keys()\n    return out_files\n\n\ndef get_config(\n    *,\n    config,\n) -> SimpleNamespace:\n    cfg = SimpleNamespace(\n        task=get_task(config),\n        datatype=get_datatype(config),\n        acq=config.acq,\n        rec=config.rec,\n        space=config.space,\n        deriv_root=get_deriv_root(config),\n        conditions=config.conditions,\n        contrasts=get_decoding_contrasts(config),\n        decode=config.decode,\n        decoding_metric=config.decoding_metric,\n        decoding_n_splits=config.decoding_n_splits,\n        decoding_time_generalization=config.decoding_time_generalization,\n        decoding_time_generalization_decim=config.decoding_time_generalization_decim,  # noqa: E501\n        random_state=config.random_state,\n        analyze_channels=config.analyze_channels,\n        ch_types=config.ch_types,\n        eeg_reference=get_eeg_reference(config),\n        n_jobs=get_n_jobs(config),\n        parallel_backend=config.parallel_backend,\n        interactive=config.interactive,\n        N_JOBS=config.N_JOBS,\n    )\n    return cfg\n\n\ndef main():\n    \"\"\"Run time-by-time decoding.\"\"\"\n    import config\n    if not config.contrasts:\n        msg = 'No contrasts specified; not performing decoding.'\n        logger.info(**gen_log_kwargs(message=msg))\n        return\n\n    if not config.decode:\n        msg = 'No decoding requested by user.'\n        logger.info(**gen_log_kwargs(message=msg))\n        return\n\n    # Here we go parallel inside the :class:`mne.decoding.SlidingEstimator`\n    # so we don't dispatch manually to multiple jobs.\n    logs = []\n    for subject, session, (cond_1, cond_2) in itertools.product(\n        get_subjects(config),\n        get_sessions(config),\n        get_decoding_contrasts(config)\n    ):\n        log = run_time_decoding(\n            cfg=get_config(config=config),\n            subject=subject,\n            condition1=cond_1,\n            condition2=cond_2,\n            session=session,\n        )\n        logs.append(log)\n    save_logs(config=config, logs=logs)\n\n\nif __name__ == '__main__':\n    main()\n", "path": "mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py"}], "after_files": [{"content": "\"\"\"Decode time-by-time using a \"sliding\" estimator.\n\nA sliding estimator fits a separate logistic regression model for every time\npoint. The end result is an averaging effect across sensors.\n\nThis approach is different from the one taken in the decoding script for\nentire epochs. Here, the classifier is traines on the entire epoch, and hence\ncan learn about the entire time course of the signal.\n\"\"\"\n\n###############################################################################\n# Let us first import the libraries\n\nimport os.path as op\nimport itertools\nfrom types import SimpleNamespace\n\nimport numpy as np\nimport pandas as pd\nfrom scipy.io import savemat\n\nimport mne\nfrom mne.decoding import (\n    GeneralizingEstimator, SlidingEstimator, cross_val_multiscore\n)\n\nfrom mne_bids import BIDSPath\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.model_selection import StratifiedKFold\n\nfrom ..._config_utils import (\n    get_sessions, get_subjects, get_task, get_datatype, get_eeg_reference,\n    get_deriv_root, _restrict_analyze_channels, get_decoding_contrasts,\n)\nfrom ..._decoding import LogReg\nfrom ..._logging import gen_log_kwargs, logger\nfrom ..._run import failsafe_run, save_logs\nfrom ..._parallel import (\n    get_parallel_backend, get_n_jobs, get_parallel_backend_name)\n\n\ndef get_input_fnames_time_decoding(**kwargs):\n    cfg = kwargs.pop('cfg')\n    subject = kwargs.pop('subject')\n    session = kwargs.pop('session')\n    # TODO: Somehow remove these?\n    del kwargs['condition1']\n    del kwargs['condition2']\n    assert len(kwargs) == 0, kwargs.keys()\n    del kwargs\n    # TODO: Shouldn't this at least use the PTP-rejected epochs if available?\n    fname_epochs = BIDSPath(subject=subject,\n                            session=session,\n                            task=cfg.task,\n                            acquisition=cfg.acq,\n                            run=None,\n                            recording=cfg.rec,\n                            space=cfg.space,\n                            suffix='epo',\n                            extension='.fif',\n                            datatype=cfg.datatype,\n                            root=cfg.deriv_root,\n                            check=False)\n    in_files = dict()\n    in_files['epochs'] = fname_epochs\n    return in_files\n\n\n@failsafe_run(script_path=__file__,\n              get_input_fnames=get_input_fnames_time_decoding)\ndef run_time_decoding(*, cfg, subject, condition1, condition2, session,\n                      in_files):\n    if cfg.decoding_time_generalization:\n        kind = 'time generalization'\n    else:\n        kind = 'sliding estimator'\n    msg = f'Contrasting conditions ({kind}): {condition1} \u2013 {condition2}'\n    logger.info(**gen_log_kwargs(message=msg, subject=subject,\n                                 session=session))\n    out_files = dict()\n    bids_path = in_files['epochs'].copy()\n\n    epochs = mne.read_epochs(in_files.pop('epochs'))\n    _restrict_analyze_channels(epochs, cfg)\n\n    # We define the epochs and the labels\n    if isinstance(cfg.conditions, dict):\n        epochs_conds = [cfg.conditions[condition1],\n                        cfg.conditions[condition2]]\n        cond_names = [condition1, condition2]\n    else:\n        epochs_conds = cond_names = [condition1, condition2]\n        epochs_conds = [condition1, condition2]\n\n    # We have to use this approach because the conditions could be based on\n    # metadata selection, so simply using epochs[conds[0], conds[1]] would\n    # not work.\n    epochs = mne.concatenate_epochs([epochs[epochs_conds[0]],\n                                     epochs[epochs_conds[1]]])\n    n_cond1 = len(epochs[epochs_conds[0]])\n    n_cond2 = len(epochs[epochs_conds[1]])\n\n    decim = cfg.decoding_time_generalization_decim\n    if cfg.decoding_time_generalization and decim > 1:\n        epochs.decimate(decim, verbose='error')\n\n    X = epochs.get_data()\n    y = np.r_[np.ones(n_cond1), np.zeros(n_cond2)]\n    # ProgressBar does not work on dask, so only enable it if not using dask\n    verbose = get_parallel_backend_name(cfg) != \"dask\"\n    with get_parallel_backend(cfg):\n        clf = make_pipeline(\n            StandardScaler(),\n            LogReg(\n                solver='liblinear',  # much faster than the default\n                random_state=cfg.random_state,\n                n_jobs=1,\n            )\n        )\n        cv = StratifiedKFold(\n            shuffle=True,\n            random_state=cfg.random_state,\n            n_splits=cfg.decoding_n_splits,\n        )\n\n        if cfg.decoding_time_generalization:\n            estimator = GeneralizingEstimator(\n                clf,\n                scoring=cfg.decoding_metric,\n                n_jobs=cfg.n_jobs,\n            )\n            cv_scoring_n_jobs = 1\n        else:\n            estimator = SlidingEstimator(\n                clf,\n                scoring=cfg.decoding_metric,\n                n_jobs=1,\n            )\n            cv_scoring_n_jobs = cfg.n_jobs\n\n        scores = cross_val_multiscore(\n            estimator, X=X, y=y, cv=cv, n_jobs=cv_scoring_n_jobs,\n            verbose=verbose,  # ensure ProgressBar is shown (can be slow)\n        )\n\n        # let's save the scores now\n        a_vs_b = f'{cond_names[0]}+{cond_names[1]}'.replace(op.sep, '')\n        processing = f'{a_vs_b}+TimeByTime+{cfg.decoding_metric}'\n        processing = processing.replace('_', '-').replace('-', '')\n        mat_key = f'mat_{processing}'\n        out_files[mat_key] = bids_path.copy().update(\n            suffix='decoding', processing=processing, extension='.mat')\n        savemat(\n            out_files[mat_key],\n            {\n                'scores': scores,\n                'times': epochs.times,\n                'decim': decim,\n            }\n        )\n\n        if cfg.decoding_time_generalization:\n            # Only store the mean scores for the diagonal in the TSV file \u2013\n            # we still have all time generalization results in the MAT file\n            # we just saved.\n            mean_crossval_score = np.diag(scores.mean(axis=0))\n        else:\n            mean_crossval_score = scores.mean(axis=0)\n\n        out_files[f'tsv_{processing}'] = out_files[mat_key].copy().update(\n            extension='.tsv')\n        tabular_data = pd.DataFrame(\n            dict(cond_1=[cond_names[0]] * len(epochs.times),\n                 cond_2=[cond_names[1]] * len(epochs.times),\n                 time=epochs.times,\n                 mean_crossval_score=mean_crossval_score,\n                 metric=[cfg.decoding_metric] * len(epochs.times))\n        )\n        tabular_data.to_csv(\n            out_files[f'tsv_{processing}'], sep='\\t', index=False)\n    assert len(in_files) == 0, in_files.keys()\n    return out_files\n\n\ndef get_config(\n    *,\n    config,\n) -> SimpleNamespace:\n    cfg = SimpleNamespace(\n        task=get_task(config),\n        datatype=get_datatype(config),\n        acq=config.acq,\n        rec=config.rec,\n        space=config.space,\n        deriv_root=get_deriv_root(config),\n        conditions=config.conditions,\n        contrasts=get_decoding_contrasts(config),\n        decode=config.decode,\n        decoding_metric=config.decoding_metric,\n        decoding_n_splits=config.decoding_n_splits,\n        decoding_time_generalization=config.decoding_time_generalization,\n        decoding_time_generalization_decim=config.decoding_time_generalization_decim,  # noqa: E501\n        random_state=config.random_state,\n        analyze_channels=config.analyze_channels,\n        ch_types=config.ch_types,\n        eeg_reference=get_eeg_reference(config),\n        n_jobs=get_n_jobs(config),\n        parallel_backend=config.parallel_backend,\n        interactive=config.interactive,\n        N_JOBS=config.N_JOBS,\n    )\n    return cfg\n\n\ndef main():\n    \"\"\"Run time-by-time decoding.\"\"\"\n    import config\n    if not config.contrasts:\n        msg = 'No contrasts specified; not performing decoding.'\n        logger.info(**gen_log_kwargs(message=msg))\n        return\n\n    if not config.decode:\n        msg = 'No decoding requested by user.'\n        logger.info(**gen_log_kwargs(message=msg))\n        return\n\n    # Here we go parallel inside the :class:`mne.decoding.SlidingEstimator`\n    # so we don't dispatch manually to multiple jobs.\n    logs = []\n    for subject, session, (cond_1, cond_2) in itertools.product(\n        get_subjects(config),\n        get_sessions(config),\n        get_decoding_contrasts(config)\n    ):\n        log = run_time_decoding(\n            cfg=get_config(config=config),\n            subject=subject,\n            condition1=cond_1,\n            condition2=cond_2,\n            session=session,\n        )\n        logs.append(log)\n    save_logs(config=config, logs=logs)\n\n\nif __name__ == '__main__':\n    main()\n", "path": "mne_bids_pipeline/scripts/sensor/_03_decoding_time_by_time.py"}]}