Repository: scipy/scipy

Issue:
BUG: `loadmat` fails to load matlab structures with anonymous functions when `simplify_cells=True`
Scipy.io.loadmat fails to load matlab structures from .mat files when they contain anonymous functions if the simplify_cells switch is set to True. I believe the reason is because loadmat registers anonymous functions as 0-dimensional mat_struct objects, but the loadmat pipeline requires mat_struct objects to have at least 1 dimension when using the simplify_cells switch. This description demonstrates the issue with a minimal code example and then indicates exactly which part of the code in scipy contains the bug. 

The following were executed in Matlab:
```
exStruct = struct();             % in MATLAB!!
exStruct.anonymous = @(x) x;     % in MATLAB!!
save('exStruct.mat','exStruct')  % in MATLAB!!
```

Then, if you run the following block in python, an IndexError will be generated: "IndexError: too many indices for array: array is 0-dimensional, but 1 were indexed"
```
import scipy.io as scio
fileName = 'exStruct.mat'
matStruct = scio.loadmat(fileName, simplify_cells=True) 
```

In scipy.io.matlab._mio5.py: line 106, the function: `_has_struct(elem)` is as follows:
```
def _has_struct(elem):
    """Determine if elem is an array and if first array item is a struct."""
    return (isinstance(elem, np.ndarray) and (elem.size > 0) and
            isinstance(elem[0], mat_struct))
```

This function (`_has_struct(elem)`) causes the error when trying to load the anonymous function, because it is loaded as a 0-dimensional mat_struct. 

Trying again with `simplify_cells=False` to load exStruct and show what the anonymous field looks like when loaded:
```
import numpy as np
matStruct = scio.loadmat(fileName, struct_as_record=False, squeeze_me=True)
exStruct = matStruct['exStruct']
print(exStruct.anonymous) # returns: <scipy.io.matlab._mio5_params.mat_struct object at 0x000001B7FFFC3820>
print(isinstance(exStruct.anonymous, np.ndarray)) # True
print(exStruct.anonymous.size > 0) # True
print(exStruct.anonymous[0]) # IndexError: too many indices for array: array is 0-dimensional, but 1 were indexed
```

For some reason, an anonymous function is returned as a mat_struct object with 0 dimensions. However, scipy.io.loadmat is only designed to handle mat_struct objects with at least 1 dimension. One solution to prevent an error is to include `(elem.ndim>0)` in the `_has_struct(elem)` function, however, I tried this and can't run the anonymous function after loading into python.



Discussion:

Resolving patch:
diff --git a/scipy/io/matlab/_mio5.py b/scipy/io/matlab/_mio5.py
--- a/scipy/io/matlab/_mio5.py
+++ b/scipy/io/matlab/_mio5.py
@@ -103,7 +103,7 @@
 
 def _has_struct(elem):
     """Determine if elem is an array and if first array item is a struct."""
-    return (isinstance(elem, np.ndarray) and (elem.size > 0) and
+    return (isinstance(elem, np.ndarray) and (elem.size > 0) and (elem.ndim > 0) and
             isinstance(elem[0], mat_struct))
 
 


Test patch:
diff --git a/scipy/io/matlab/tests/test_mio.py b/scipy/io/matlab/tests/test_mio.py
--- a/scipy/io/matlab/tests/test_mio.py
+++ b/scipy/io/matlab/tests/test_mio.py
@@ -1290,6 +1290,12 @@ def test_opaque():
     assert isinstance(data['parabola'].item()[3].item()[3], MatlabOpaque)
 
 
+def test_opaque_simplify():
+    """Test that we can read a MatlabOpaque object when simplify_cells=True."""
+    data = loadmat(pjoin(test_data_path, 'parabola.mat'), simplify_cells=True)
+    assert isinstance(data['parabola'], MatlabFunction)
+
+
 def test_deprecation():
     """Test that access to previous attributes still works."""
     # This should be accessible immediately from scipy.io import