Repository: mne-tools/mne-python

Issue:
mne read_proj Unbound local error 'global_nchan' referenced before assignment
Dear All,

I’m trying to clean any heartbeat related artifacts from an EEG+ECG recording. To do this I’m computing a signal spatial projection for the ECG data recorded in a Bio channel. Then I created a function to save this projection as a fif file then using the saved file, plot the projection as a topomap. Creating the projection and saving it as a fif seem to be working, but an unbound local error is generated in reading the projection file.

Here's the code generating the issue.

import mne; mne.sys_info()
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os
from mne import read_proj

sfreq = 500
l_freq, h_freq = 0.1, 40
tmin, tmax = 0, 60
fmin, fmax = 0.5, 250
n_fft = 2048

channel_names = ['C3', 'C4', 'Cz', 'F3', 'F4', 'F7', 'F8', 'Fp1', 'Fp2', 'Fz', 'O1', 'O2', 'Oz', 'P3', 'P4', 'P7', 'P8', 'Pz', 'T7', 'T8', 'Bio3', 'VSyn', 'ASyn', 'LABEL']
EEG_channels = ['C3', 'C4', 'Cz', 'F3', 'F4', 'F7', 'F8', 'Fp1', 'Fp2', 'Fz', 'O1', 'O2', 'Oz', 'P3', 'P4', 'P7', 'P8', 'Pz', 'T7', 'T8']
channel_types = ['stim', 'meg', 'eeg', 'eeg', 'eog']

os.chdir('C:/Users/6/TEST')

raw = mne.io.read_raw_edf(r'C:/Users/6/TEST/ANFI0001_2020-07-30_M45_Joe_SMITH_EOEC_PRE.edf', eog=None, misc=None, stim_channel='auto', exclude=('VSyn', 'ASyn', 'LABEL'), preload=True, verbose=None)

raw.set_channel_types({'Bio3': 'ecg'})

ecg_projs = mne.preprocessing.compute_proj_ecg(raw, raw_event=None, tmin=- 0.2, tmax=0.4, n_grad=2, n_mag=2, n_eeg=2, l_freq=1.0, h_freq=40.0, average=True, filter_length='10s', n_jobs=1, ch_name=None, reject={'mag': 3e-12}, flat=None, bads=[], avg_ref=False, no_proj=False, event_id=999, ecg_l_freq=5, ecg_h_freq=35, tstart=0.0, qrs_threshold='auto', filter_method='fir', iir_params=None, copy=True, return_drop_log=False, meg='separate', verbose=None)

global ecg_fname
def create_ecg_fif(ecg_fname):
os.path(r'C:/Users/6/TEST/heartbeat-proj.fif')
mne.write_proj(ecg_fname, ecg_projs)
return ecg_fname
ecg_fname = create_ecg_fif

ecg_projs = read_proj(r'C:/Users/6/TEST/heartbeat-proj.fif')
raw.add_proj(ecg_projs)
raw.plot_projs_topomap()

In plotting the projection I'm getting the following error:

Traceback (most recent call last):

File "C:\Users\6\TEST\untitled0.py", line 80, in
ecg_projs = read_proj(r'C:/Users/6/TEST/heartbeat-proj.fif')

File "C:\Anaconda3\envs\mne\lib\site-packages\mne\proj.py", line 47, in read_proj
projs = _read_proj(fid, tree)

File "", line 21, in _read_proj

File "C:\Anaconda3\envs\mne\lib\site-packages\mne\io\proj.py", line 342, in _read_proj
nchan = global_nchan

UnboundLocalError: local variable 'global_nchan' referenced before assignment

Any help would be greatly appreciated. Thanks in advance. 


Discussion:
can you share with me the edf file so I can replicate the issue?

>

Sure, the edf, proj and py files are in this zip folder. 

[read proj issue.zip](https://github.com/mne-tools/mne-python/files/5215272/read.proj.issue.zip)

Thanks for helping with this.

Resolving patch:
diff --git a/mne/io/proj.py b/mne/io/proj.py
--- a/mne/io/proj.py
+++ b/mne/io/proj.py
@@ -19,7 +19,7 @@
 from .write import (write_int, write_float, write_string, write_name_list,
                     write_float_matrix, end_block, start_block)
 from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT
-from ..utils import logger, verbose, warn, fill_doc
+from ..utils import logger, verbose, warn, fill_doc, _validate_type
 
 
 class Projection(dict):
@@ -31,7 +31,7 @@ class Projection(dict):
     def __repr__(self):  # noqa: D105
         s = "%s" % self['desc']
         s += ", active : %s" % self['active']
-        s += ", n_channels : %s" % self['data']['ncol']
+        s += f", n_channels : {len(self['data']['col_names'])}"
         return "<Projection | %s>" % s
 
     # speed up info copy by taking advantage of mutability
@@ -357,18 +357,23 @@ def _read_proj(fid, node, verbose=None):
     if len(nodes) == 0:
         return projs
 
-    tag = find_tag(fid, nodes[0], FIFF.FIFF_NCHAN)
-    if tag is not None:
-        global_nchan = int(tag.data)
+    # This might exist but we won't use it:
+    # global_nchan = None
+    # tag = find_tag(fid, nodes[0], FIFF.FIFF_NCHAN)
+    # if tag is not None:
+    #     global_nchan = int(tag.data)
 
     items = dir_tree_find(nodes[0], FIFF.FIFFB_PROJ_ITEM)
     for item in items:
         #   Find all desired tags in one item
-        tag = find_tag(fid, item, FIFF.FIFF_NCHAN)
-        if tag is not None:
-            nchan = int(tag.data)
-        else:
-            nchan = global_nchan
+
+        # This probably also exists but used to be written incorrectly
+        # sometimes
+        # tag = find_tag(fid, item, FIFF.FIFF_NCHAN)
+        # if tag is not None:
+        #     nchan = int(tag.data)
+        # else:
+        #     nchan = global_nchan
 
         tag = find_tag(fid, item, FIFF.FIFF_DESCRIPTION)
         if tag is not None:
@@ -380,13 +385,6 @@ def _read_proj(fid, node, verbose=None):
             else:
                 raise ValueError('Projection item description missing')
 
-        # XXX : is this useful ?
-        # tag = find_tag(fid, item, FIFF.FIFF_PROJ_ITEM_CH_NAME_LIST)
-        # if tag is not None:
-        #     namelist = tag.data
-        # else:
-        #     raise ValueError('Projection item channel list missing')
-
         tag = find_tag(fid, item, FIFF.FIFF_PROJ_ITEM_KIND)
         if tag is not None:
             kind = int(tag.data)
@@ -431,6 +429,9 @@ def _read_proj(fid, node, verbose=None):
             raise ValueError('Number of channel names does not match the '
                              'size of data matrix')
 
+        # just always use this, we used to have bugs with writing the
+        # number correctly...
+        nchan = len(names)
         #   Use exactly the same fields in data as in a named matrix
         one = Projection(kind=kind, active=active, desc=desc,
                          data=dict(nrow=nvec, ncol=nchan, row_names=None,
@@ -441,14 +442,11 @@ def _read_proj(fid, node, verbose=None):
 
     if len(projs) > 0:
         logger.info('    Read a total of %d projection items:' % len(projs))
-        for k in range(len(projs)):
-            if projs[k]['active']:
-                misc = 'active'
-            else:
-                misc = ' idle'
-            logger.info('        %s (%d x %d) %s'
-                        % (projs[k]['desc'], projs[k]['data']['nrow'],
-                           projs[k]['data']['ncol'], misc))
+        for proj in projs:
+            misc = 'active' if proj['active'] else ' idle'
+            logger.info(f'        {proj["desc"]} '
+                        f'({proj["data"]["nrow"]} x '
+                        f'{len(proj["data"]["col_names"])}) {misc}')
 
     return projs
 
@@ -468,11 +466,17 @@ def _write_proj(fid, projs):
     """
     if len(projs) == 0:
         return
+
+    # validation
+    _validate_type(projs, (list, tuple), 'projs')
+    for pi, proj in enumerate(projs):
+        _validate_type(proj, Projection, f'projs[{pi}]')
+
     start_block(fid, FIFF.FIFFB_PROJ)
 
     for proj in projs:
         start_block(fid, FIFF.FIFFB_PROJ_ITEM)
-        write_int(fid, FIFF.FIFF_NCHAN, proj['data']['ncol'])
+        write_int(fid, FIFF.FIFF_NCHAN, len(proj['data']['col_names']))
         write_name_list(fid, FIFF.FIFF_PROJ_ITEM_CH_NAME_LIST,
                         proj['data']['col_names'])
         write_string(fid, FIFF.FIFF_NAME, proj['desc'])
@@ -618,6 +622,7 @@ def _make_projector(projs, ch_names, bads=(), include_active=True,
                 p['data']['data'] = this_vecs[sel].T
                 p['data']['col_names'] = [p['data']['col_names'][ii]
                                           for ii in vecsel]
+                p['data']['ncol'] = len(p['data']['col_names'])
             nvec += p['data']['nrow']
 
     #   Check whether all of the vectors are exactly zero


Test patch:
diff --git a/mne/tests/test_proj.py b/mne/tests/test_proj.py
--- a/mne/tests/test_proj.py
+++ b/mne/tests/test_proj.py
@@ -20,7 +20,7 @@
 from mne.proj import (read_proj, write_proj, make_eeg_average_ref_proj,
                       _has_eeg_average_ref_proj)
 from mne.rank import _compute_rank_int
-from mne.utils import _TempDir, run_tests_if_main
+from mne.utils import run_tests_if_main
 
 base_dir = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data')
 raw_fname = op.join(base_dir, 'test_raw.fif')
@@ -144,9 +144,9 @@ def test_sensitivity_maps():
     sensitivity_map(fwd)
 
 
-def test_compute_proj_epochs():
+def test_compute_proj_epochs(tmpdir):
     """Test SSP computation on epochs."""
-    tempdir = _TempDir()
+    tempdir = str(tmpdir)
     event_id, tmin, tmax = 1, -0.2, 0.3
 
     raw = read_raw_fif(raw_fname, preload=True)
@@ -218,11 +218,18 @@ def test_compute_proj_epochs():
     with pytest.warns(RuntimeWarning, match='-proj.fif'):
         read_proj(proj_badname)
 
+    # bad inputs
+    fname = op.join(tempdir, 'out-proj.fif')
+    with pytest.raises(TypeError, match='projs'):
+        write_proj(fname, 'foo')
+    with pytest.raises(TypeError, match=r'projs\[0\] must be .*'):
+        write_proj(fname, ['foo'])
+
 
 @pytest.mark.slowtest
-def test_compute_proj_raw():
+def test_compute_proj_raw(tmpdir):
     """Test SSP computation on raw."""
-    tempdir = _TempDir()
+    tempdir = str(tmpdir)
     # Test that the raw projectors work
     raw_time = 2.5  # Do shorter amount for speed
     raw = read_raw_fif(raw_fname).crop(0, raw_time)