Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/compat_old.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
run:
shell: bash
env:
CONDA_DEPENDENCIES: 'numpy=1.18 scipy=1.6.3 matplotlib=3.1 pandas=1.0 scikit-learn=0.22'
CONDA_DEPENDENCIES: 'numpy=1.20.2 scipy=1.6.3 matplotlib=3.4 pandas=1.2.4 scikit-learn=0.24.2'
DISPLAY: ':99.0'
MNE_LOGGING_LEVEL: 'warning'
OPENBLAS_NUM_THREADS: '1'
Expand Down
30 changes: 19 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,28 +94,36 @@ Dependencies
The minimum required dependencies to run MNE-Python are:

- Python >= 3.8
- NumPy >= 1.18.1
- NumPy >= 1.20.2
- SciPy >= 1.6.3
- Matplotlib >= 3.1.0
- Matplotlib >= 3.4.0
- pooch >= 1.5
- tqdm
- Jinja2
- decorator

For full functionality, some functions require:

- Scikit-learn >= 0.22.0
- Scikit-learn >= 0.24.2
- joblib >= 0.15 (for parallelization control)
- Numba >= 0.48.0
- NiBabel >= 2.5.0
- mne-qt-browser >= 0.1 (for fast raw data visualization)
- Qt5 >= 5.12 via one of the following bindings (for fast raw data visualization and interactive 3D visualization):

- PyQt6 >= 6.0
- PySide6 >= 6.0
- PyQt5 >= 5.12
- PySide2 >= 5.12

- Numba >= 0.53.1
- NiBabel >= 3.2.1
- OpenMEEG >= 2.5.5
- Pandas >= 1.0.0
- Pandas >= 1.2.4
- Picard >= 0.3
- CuPy >= 7.1.1 (for NVIDIA CUDA acceleration)
- DIPY >= 1.1.0
- Imageio >= 2.6.1
- PyVista >= 0.32
- pyvistaqt >= 0.4
- CuPy >= 9.0.0 (for NVIDIA CUDA acceleration)
- DIPY >= 1.4.0
- Imageio >= 2.8.0
- PyVista >= 0.32 (for 3D visualization)
- pyvistaqt >= 0.4 (for 3D visualization)
- mffpy >= 0.5.7
- h5py
- h5io
Expand Down
3 changes: 2 additions & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,8 @@ def append_attr_meth_examples(app, what, name, obj, options, lines):
'navigation_with_keys': False,
'show_toc_level': 1,
'navbar_end': ['theme-switcher', 'version-switcher', 'navbar-icon-links'],
'footer_items': ['copyright'],
'footer_start': ['copyright'],
'footer_end': [],
'secondary_sidebar_items': ['page-toc'],
'analytics': dict(google_analytics_id='G-5TBCPCRB6X'),
'switcher': {
Expand Down
4 changes: 2 additions & 2 deletions mne/decoding/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from mne import create_info, EpochsArray
from mne.fixes import is_regressor, is_classifier
from mne.utils import requires_sklearn, requires_version
from mne.utils import requires_sklearn
from mne.decoding.base import (_get_inverse_funcs, LinearModel, get_coef,
cross_val_multiscore, BaseEstimator)
from mne.decoding.search_light import SlidingEstimator
Expand Down Expand Up @@ -268,7 +268,7 @@ def test_get_coef_multiclass(n_features, n_targets):
lm.fit(X, Y, sample_weight=np.ones(len(Y)))


@requires_version('sklearn', '0.22') # roc_auc_ovr_weighted
@requires_sklearn
@pytest.mark.parametrize('n_classes, n_channels, n_times', [
(4, 10, 2),
(4, 3, 2),
Expand Down
166 changes: 13 additions & 153 deletions mne/fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,84 +94,6 @@ def _csc_matrix_cast(x):
return csc_matrix(x)


###############################################################################
# Backporting nibabel's read_geometry

def _get_read_geometry():
"""Get the geometry reading function."""
try:
import nibabel as nib
has_nibabel = True
except ImportError:
has_nibabel = False
if has_nibabel:
from nibabel.freesurfer import read_geometry
else:
read_geometry = _read_geometry
return read_geometry


def _read_geometry(filepath, read_metadata=False, read_stamp=False):
"""Backport from nibabel."""
from .surface import _fread3, _fread3_many
volume_info = dict()

TRIANGLE_MAGIC = 16777214
QUAD_MAGIC = 16777215
NEW_QUAD_MAGIC = 16777213
with open(filepath, "rb") as fobj:
magic = _fread3(fobj)
if magic in (QUAD_MAGIC, NEW_QUAD_MAGIC): # Quad file
nvert = _fread3(fobj)
nquad = _fread3(fobj)
(fmt, div) = (">i2", 100.) if magic == QUAD_MAGIC else (">f4", 1.)
coords = np.fromfile(fobj, fmt, nvert * 3).astype(np.float64) / div
coords = coords.reshape(-1, 3)
quads = _fread3_many(fobj, nquad * 4)
quads = quads.reshape(nquad, 4)
#
# Face splitting follows
#
faces = np.zeros((2 * nquad, 3), dtype=np.int64)
nface = 0
for quad in quads:
if (quad[0] % 2) == 0:
faces[nface] = quad[0], quad[1], quad[3]
nface += 1
faces[nface] = quad[2], quad[3], quad[1]
nface += 1
else:
faces[nface] = quad[0], quad[1], quad[2]
nface += 1
faces[nface] = quad[0], quad[2], quad[3]
nface += 1

elif magic == TRIANGLE_MAGIC: # Triangle file
create_stamp = fobj.readline().rstrip(b'\n').decode('utf-8')
fobj.readline()
vnum = np.fromfile(fobj, ">i4", 1)[0]
fnum = np.fromfile(fobj, ">i4", 1)[0]
coords = np.fromfile(fobj, ">f4", vnum * 3).reshape(vnum, 3)
faces = np.fromfile(fobj, ">i4", fnum * 3).reshape(fnum, 3)

if read_metadata:
volume_info = _read_volume_info(fobj)
else:
raise ValueError("File does not appear to be a Freesurfer surface")

coords = coords.astype(np.float64)

ret = (coords, faces)
if read_metadata:
if len(volume_info) == 0:
warnings.warn('No volume information contained in the file')
ret += (volume_info,)
if read_stamp:
ret += (create_stamp,)

return ret


###############################################################################
# NumPy Generator (NumPy 1.17)

Expand Down Expand Up @@ -234,36 +156,6 @@ def _read_volume_info(fobj):
return volume_info


def _serialize_volume_info(volume_info):
"""An implementation of nibabel.freesurfer.io._serialize_volume_info, since
old versions of nibabel (<=2.1.0) don't have it."""
keys = ['head', 'valid', 'filename', 'volume', 'voxelsize', 'xras', 'yras',
'zras', 'cras']
diff = set(volume_info.keys()).difference(keys)
if len(diff) > 0:
raise ValueError('Invalid volume info: %s.' % diff.pop())

strings = list()
for key in keys:
if key == 'head':
if not (np.array_equal(volume_info[key], [20]) or np.array_equal(
volume_info[key], [2, 0, 20])):
warnings.warn("Unknown extension code.")
strings.append(np.array(volume_info[key], dtype='>i4').tobytes())
elif key in ('valid', 'filename'):
val = volume_info[key]
strings.append('{} = {}\n'.format(key, val).encode('utf-8'))
elif key == 'volume':
val = volume_info[key]
strings.append('{} = {} {} {}\n'.format(
key, val[0], val[1], val[2]).encode('utf-8'))
else:
val = volume_info[key]
strings.append('{} = {:0.10g} {:0.10g} {:0.10g}\n'.format(
key.ljust(6), val[0], val[1], val[2]).encode('utf-8'))
return b''.join(strings)


##############################################################################
# adapted from scikit-learn

Expand Down Expand Up @@ -877,28 +769,9 @@ def stable_cumsum(arr, axis=None, rtol=1e-05, atol=1e-08):
return out


# This shim can be removed once NumPy 1.19.0+ is required (1.18.4 has sign bug)
def svd(a, hermitian=False):
if hermitian: # faster
s, u = np.linalg.eigh(a)
sgn = np.sign(s)
s = np.abs(s)
sidx = np.argsort(s)[..., ::-1]
sgn = np.take_along_axis(sgn, sidx, axis=-1)
s = np.take_along_axis(s, sidx, axis=-1)
u = np.take_along_axis(u, sidx[..., None, :], axis=-1)
# singular values are unsigned, move the sign into v
vt = (u * sgn[..., np.newaxis, :]).swapaxes(-2, -1).conj()
np.abs(s, out=s)
return u, s, vt
else:
return np.linalg.svd(a)


###############################################################################
# From nilearn


def _crop_colorbar(cbar, cbar_vmin, cbar_vmax):
"""
crop a colorbar to show from cbar_vmin to cbar_vmax
Expand All @@ -915,31 +788,18 @@ def _crop_colorbar(cbar, cbar_vmin, cbar_vmax):
new_tick_locs = np.linspace(cbar_vmin, cbar_vmax,
len(cbar_tick_locs))

# matplotlib >= 3.2.0 no longer normalizes axes between 0 and 1
# See https://matplotlib.org/3.2.1/api/prev_api_changes/api_changes_3.2.0.html
# _outline was removed in
# https://github.com/matplotlib/matplotlib/commit/03a542e875eba091a027046d5ec652daa8be6863
# so we use the code from there
if _compare_version(matplotlib.__version__, '>=', '3.2.0'):
cbar.ax.set_ylim(cbar_vmin, cbar_vmax)
X = cbar._mesh()[0]
X = np.array([X[0], X[-1]])
Y = np.array([[cbar_vmin, cbar_vmin], [cbar_vmax, cbar_vmax]])
N = X.shape[0]
ii = [0, 1, N - 2, N - 1, 2 * N - 1, 2 * N - 2, N + 1, N, 0]
x = X.T.reshape(-1)[ii]
y = Y.T.reshape(-1)[ii]
xy = (np.column_stack([y, x])
if cbar.orientation == 'horizontal' else
np.column_stack([x, y]))
cbar.outline.set_xy(xy)
else:
cbar.ax.set_ylim(cbar.norm(cbar_vmin), cbar.norm(cbar_vmax))
outline = cbar.outline.get_xy()
outline[:2, 1] += cbar.norm(cbar_vmin)
outline[2:6, 1] -= (1. - cbar.norm(cbar_vmax))
outline[6:, 1] += cbar.norm(cbar_vmin)
cbar.outline.set_xy(outline)
cbar.ax.set_ylim(cbar_vmin, cbar_vmax)
X = cbar._mesh()[0]
X = np.array([X[0], X[-1]])
Y = np.array([[cbar_vmin, cbar_vmin], [cbar_vmax, cbar_vmax]])
N = X.shape[0]
ii = [0, 1, N - 2, N - 1, 2 * N - 1, 2 * N - 2, N + 1, N, 0]
x = X.T.reshape(-1)[ii]
y = Y.T.reshape(-1)[ii]
xy = (np.column_stack([y, x])
if cbar.orientation == 'horizontal' else
np.column_stack([x, y]))
cbar.outline.set_xy(xy)

cbar.set_ticks(new_tick_locs)
cbar.update_ticks()
Expand All @@ -951,7 +811,7 @@ def _crop_colorbar(cbar, cbar_vmin, cbar_vmax):
# Here we choose different defaults to speed things up by default
try:
import numba
if _compare_version(numba.__version__, '<', '0.48'):
if _compare_version(numba.__version__, '<', '0.53.1'):
raise ImportError
prange = numba.prange
def jit(nopython=True, nogil=True, fastmath=True, cache=True,
Expand Down
46 changes: 13 additions & 33 deletions mne/surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@
from collections import OrderedDict
from glob import glob
from os import path as op
from struct import pack
import time
import warnings

import numpy as np

from .channels.channels import _get_meg_system
from .fixes import (_serialize_volume_info, _get_read_geometry, jit,
prange, bincount)
from .fixes import jit, prange, bincount
from .io.constants import FIFF
from .io.pick import pick_types
from .parallel import parallel_func
Expand Down Expand Up @@ -810,6 +808,7 @@ def read_surface(fname, read_metadata=False, return_dict=False,
write_surface
read_tri
"""
from ._freesurfer import _import_nibabel
fname = _check_fname(fname, 'read', True)
_check_option('file_format', file_format, ['auto', 'freesurfer', 'obj'])

Expand All @@ -820,7 +819,9 @@ def read_surface(fname, read_metadata=False, return_dict=False,
file_format = 'freesurfer'

if file_format == 'freesurfer':
ret = _get_read_geometry()(fname, read_metadata=read_metadata)
_import_nibabel('read surface geometry')
from nibabel.freesurfer import read_geometry
ret = read_geometry(fname, read_metadata=read_metadata)
elif file_format == 'obj':
ret = _read_wavefront_obj(fname)
if read_metadata:
Expand Down Expand Up @@ -1185,6 +1186,7 @@ def write_surface(fname, coords, faces, create_stamp='', volume_info=None,
read_surface
read_tri
"""
from ._freesurfer import _import_nibabel
fname = _check_fname(fname, overwrite=overwrite)
_check_option('file_format', file_format, ['auto', 'freesurfer', 'obj'])

Expand All @@ -1195,35 +1197,13 @@ def write_surface(fname, coords, faces, create_stamp='', volume_info=None,
file_format = 'freesurfer'

if file_format == 'freesurfer':
try:
import nibabel as nib
has_nibabel = True
except ImportError:
has_nibabel = False
if has_nibabel:
nib.freesurfer.io.write_geometry(fname, coords, faces,
create_stamp=create_stamp,
volume_info=volume_info)
return
if len(create_stamp.splitlines()) > 1:
raise ValueError("create_stamp can only contain one line")

with open(fname, 'wb') as fid:
fid.write(pack('>3B', 255, 255, 254))
strs = ['%s\n' % create_stamp, '\n']
strs = [s.encode('utf-8') for s in strs]
fid.writelines(strs)
vnum = len(coords)
fnum = len(faces)
fid.write(pack('>2i', vnum, fnum))
fid.write(np.array(coords, dtype='>f4').tobytes())
fid.write(np.array(faces, dtype='>i4').tobytes())

# Add volume info, if given
if volume_info is not None and len(volume_info) > 0:
fid.write(_serialize_volume_info(volume_info))

elif file_format == 'obj':
_import_nibabel('write surface geometry')
from nibabel.freesurfer import write_geometry
write_geometry(
fname, coords, faces, create_stamp=create_stamp,
volume_info=volume_info)
else:
assert file_format == 'obj'
with open(fname, 'w') as fid:
for line in create_stamp.splitlines():
fid.write(f'# {line}\n')
Expand Down
1 change: 0 additions & 1 deletion mne/tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,6 @@ def test_annotations_simple_iteration():
assert elem == expected_value


@requires_version('numpy', '1.12')
def test_annotations_slices():
"""Test indexing Annotations."""
NUM_ANNOT = 5
Expand Down
2 changes: 2 additions & 0 deletions mne/tests/test_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def test_compute_nearest():
@testing.requires_testing_data
def test_io_surface(tmp_path):
"""Test reading and writing of Freesurfer surface mesh files."""
pytest.importorskip('nibabel')
fname_quad = data_path / "subjects" / "bert" / "surf" / "lh.inflated.nofix"
fname_tri = data_path / "subjects" / "sample" / "bem" / "inner_skull.surf"
for fname in (fname_quad, fname_tri):
Expand Down Expand Up @@ -155,6 +156,7 @@ def test_io_surface(tmp_path):
@testing.requires_testing_data
def test_read_curv():
"""Test reading curvature data."""
pytest.importorskip('nibabel')
fname_curv = data_path / "subjects" / "fsaverage" / "surf" / "lh.curv"
fname_surf = data_path / "subjects" / "fsaverage" / "surf" / "lh.inflated"
bin_curv = read_curvature(fname_curv)
Expand Down
Loading