Skip to content

Commit 29e99c8

Browse files
authored
MAINT: Bump minimums (mne-tools#11557)
1 parent 132d1da commit 29e99c8

File tree

16 files changed

+63
-235
lines changed

16 files changed

+63
-235
lines changed

.github/workflows/compat_old.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
run:
1919
shell: bash
2020
env:
21-
CONDA_DEPENDENCIES: 'numpy=1.18 scipy=1.6.3 matplotlib=3.1 pandas=1.0 scikit-learn=0.22'
21+
CONDA_DEPENDENCIES: 'numpy=1.20.2 scipy=1.6.3 matplotlib=3.4 pandas=1.2.4 scikit-learn=0.24.2'
2222
DISPLAY: ':99.0'
2323
MNE_LOGGING_LEVEL: 'warning'
2424
OPENBLAS_NUM_THREADS: '1'

README.rst

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,28 +94,36 @@ Dependencies
9494
The minimum required dependencies to run MNE-Python are:
9595

9696
- Python >= 3.8
97-
- NumPy >= 1.18.1
97+
- NumPy >= 1.20.2
9898
- SciPy >= 1.6.3
99-
- Matplotlib >= 3.1.0
99+
- Matplotlib >= 3.4.0
100100
- pooch >= 1.5
101101
- tqdm
102102
- Jinja2
103103
- decorator
104104

105105
For full functionality, some functions require:
106106

107-
- Scikit-learn >= 0.22.0
107+
- Scikit-learn >= 0.24.2
108108
- joblib >= 0.15 (for parallelization control)
109-
- Numba >= 0.48.0
110-
- NiBabel >= 2.5.0
109+
- mne-qt-browser >= 0.1 (for fast raw data visualization)
110+
- Qt5 >= 5.12 via one of the following bindings (for fast raw data visualization and interactive 3D visualization):
111+
112+
- PyQt6 >= 6.0
113+
- PySide6 >= 6.0
114+
- PyQt5 >= 5.12
115+
- PySide2 >= 5.12
116+
117+
- Numba >= 0.53.1
118+
- NiBabel >= 3.2.1
111119
- OpenMEEG >= 2.5.5
112-
- Pandas >= 1.0.0
120+
- Pandas >= 1.2.4
113121
- Picard >= 0.3
114-
- CuPy >= 7.1.1 (for NVIDIA CUDA acceleration)
115-
- DIPY >= 1.1.0
116-
- Imageio >= 2.6.1
117-
- PyVista >= 0.32
118-
- pyvistaqt >= 0.4
122+
- CuPy >= 9.0.0 (for NVIDIA CUDA acceleration)
123+
- DIPY >= 1.4.0
124+
- Imageio >= 2.8.0
125+
- PyVista >= 0.32 (for 3D visualization)
126+
- pyvistaqt >= 0.4 (for 3D visualization)
119127
- mffpy >= 0.5.7
120128
- h5py
121129
- h5io

doc/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,8 @@ def append_attr_meth_examples(app, what, name, obj, options, lines):
631631
'navigation_with_keys': False,
632632
'show_toc_level': 1,
633633
'navbar_end': ['theme-switcher', 'version-switcher', 'navbar-icon-links'],
634-
'footer_items': ['copyright'],
634+
'footer_start': ['copyright'],
635+
'footer_end': [],
635636
'secondary_sidebar_items': ['page-toc'],
636637
'analytics': dict(google_analytics_id='G-5TBCPCRB6X'),
637638
'switcher': {

mne/decoding/tests/test_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from mne import create_info, EpochsArray
1212
from mne.fixes import is_regressor, is_classifier
13-
from mne.utils import requires_sklearn, requires_version
13+
from mne.utils import requires_sklearn
1414
from mne.decoding.base import (_get_inverse_funcs, LinearModel, get_coef,
1515
cross_val_multiscore, BaseEstimator)
1616
from mne.decoding.search_light import SlidingEstimator
@@ -268,7 +268,7 @@ def test_get_coef_multiclass(n_features, n_targets):
268268
lm.fit(X, Y, sample_weight=np.ones(len(Y)))
269269

270270

271-
@requires_version('sklearn', '0.22') # roc_auc_ovr_weighted
271+
@requires_sklearn
272272
@pytest.mark.parametrize('n_classes, n_channels, n_times', [
273273
(4, 10, 2),
274274
(4, 3, 2),

mne/fixes.py

Lines changed: 13 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -94,84 +94,6 @@ def _csc_matrix_cast(x):
9494
return csc_matrix(x)
9595

9696

97-
###############################################################################
98-
# Backporting nibabel's read_geometry
99-
100-
def _get_read_geometry():
101-
"""Get the geometry reading function."""
102-
try:
103-
import nibabel as nib
104-
has_nibabel = True
105-
except ImportError:
106-
has_nibabel = False
107-
if has_nibabel:
108-
from nibabel.freesurfer import read_geometry
109-
else:
110-
read_geometry = _read_geometry
111-
return read_geometry
112-
113-
114-
def _read_geometry(filepath, read_metadata=False, read_stamp=False):
115-
"""Backport from nibabel."""
116-
from .surface import _fread3, _fread3_many
117-
volume_info = dict()
118-
119-
TRIANGLE_MAGIC = 16777214
120-
QUAD_MAGIC = 16777215
121-
NEW_QUAD_MAGIC = 16777213
122-
with open(filepath, "rb") as fobj:
123-
magic = _fread3(fobj)
124-
if magic in (QUAD_MAGIC, NEW_QUAD_MAGIC): # Quad file
125-
nvert = _fread3(fobj)
126-
nquad = _fread3(fobj)
127-
(fmt, div) = (">i2", 100.) if magic == QUAD_MAGIC else (">f4", 1.)
128-
coords = np.fromfile(fobj, fmt, nvert * 3).astype(np.float64) / div
129-
coords = coords.reshape(-1, 3)
130-
quads = _fread3_many(fobj, nquad * 4)
131-
quads = quads.reshape(nquad, 4)
132-
#
133-
# Face splitting follows
134-
#
135-
faces = np.zeros((2 * nquad, 3), dtype=np.int64)
136-
nface = 0
137-
for quad in quads:
138-
if (quad[0] % 2) == 0:
139-
faces[nface] = quad[0], quad[1], quad[3]
140-
nface += 1
141-
faces[nface] = quad[2], quad[3], quad[1]
142-
nface += 1
143-
else:
144-
faces[nface] = quad[0], quad[1], quad[2]
145-
nface += 1
146-
faces[nface] = quad[0], quad[2], quad[3]
147-
nface += 1
148-
149-
elif magic == TRIANGLE_MAGIC: # Triangle file
150-
create_stamp = fobj.readline().rstrip(b'\n').decode('utf-8')
151-
fobj.readline()
152-
vnum = np.fromfile(fobj, ">i4", 1)[0]
153-
fnum = np.fromfile(fobj, ">i4", 1)[0]
154-
coords = np.fromfile(fobj, ">f4", vnum * 3).reshape(vnum, 3)
155-
faces = np.fromfile(fobj, ">i4", fnum * 3).reshape(fnum, 3)
156-
157-
if read_metadata:
158-
volume_info = _read_volume_info(fobj)
159-
else:
160-
raise ValueError("File does not appear to be a Freesurfer surface")
161-
162-
coords = coords.astype(np.float64)
163-
164-
ret = (coords, faces)
165-
if read_metadata:
166-
if len(volume_info) == 0:
167-
warnings.warn('No volume information contained in the file')
168-
ret += (volume_info,)
169-
if read_stamp:
170-
ret += (create_stamp,)
171-
172-
return ret
173-
174-
17597
###############################################################################
17698
# NumPy Generator (NumPy 1.17)
17799

@@ -234,36 +156,6 @@ def _read_volume_info(fobj):
234156
return volume_info
235157

236158

237-
def _serialize_volume_info(volume_info):
238-
"""An implementation of nibabel.freesurfer.io._serialize_volume_info, since
239-
old versions of nibabel (<=2.1.0) don't have it."""
240-
keys = ['head', 'valid', 'filename', 'volume', 'voxelsize', 'xras', 'yras',
241-
'zras', 'cras']
242-
diff = set(volume_info.keys()).difference(keys)
243-
if len(diff) > 0:
244-
raise ValueError('Invalid volume info: %s.' % diff.pop())
245-
246-
strings = list()
247-
for key in keys:
248-
if key == 'head':
249-
if not (np.array_equal(volume_info[key], [20]) or np.array_equal(
250-
volume_info[key], [2, 0, 20])):
251-
warnings.warn("Unknown extension code.")
252-
strings.append(np.array(volume_info[key], dtype='>i4').tobytes())
253-
elif key in ('valid', 'filename'):
254-
val = volume_info[key]
255-
strings.append('{} = {}\n'.format(key, val).encode('utf-8'))
256-
elif key == 'volume':
257-
val = volume_info[key]
258-
strings.append('{} = {} {} {}\n'.format(
259-
key, val[0], val[1], val[2]).encode('utf-8'))
260-
else:
261-
val = volume_info[key]
262-
strings.append('{} = {:0.10g} {:0.10g} {:0.10g}\n'.format(
263-
key.ljust(6), val[0], val[1], val[2]).encode('utf-8'))
264-
return b''.join(strings)
265-
266-
267159
##############################################################################
268160
# adapted from scikit-learn
269161

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

879771

880-
# This shim can be removed once NumPy 1.19.0+ is required (1.18.4 has sign bug)
881-
def svd(a, hermitian=False):
882-
if hermitian: # faster
883-
s, u = np.linalg.eigh(a)
884-
sgn = np.sign(s)
885-
s = np.abs(s)
886-
sidx = np.argsort(s)[..., ::-1]
887-
sgn = np.take_along_axis(sgn, sidx, axis=-1)
888-
s = np.take_along_axis(s, sidx, axis=-1)
889-
u = np.take_along_axis(u, sidx[..., None, :], axis=-1)
890-
# singular values are unsigned, move the sign into v
891-
vt = (u * sgn[..., np.newaxis, :]).swapaxes(-2, -1).conj()
892-
np.abs(s, out=s)
893-
return u, s, vt
894-
else:
895-
return np.linalg.svd(a)
896-
897-
898772
###############################################################################
899773
# From nilearn
900774

901-
902775
def _crop_colorbar(cbar, cbar_vmin, cbar_vmax):
903776
"""
904777
crop a colorbar to show from cbar_vmin to cbar_vmax
@@ -915,31 +788,18 @@ def _crop_colorbar(cbar, cbar_vmin, cbar_vmax):
915788
new_tick_locs = np.linspace(cbar_vmin, cbar_vmax,
916789
len(cbar_tick_locs))
917790

918-
# matplotlib >= 3.2.0 no longer normalizes axes between 0 and 1
919-
# See https://matplotlib.org/3.2.1/api/prev_api_changes/api_changes_3.2.0.html
920-
# _outline was removed in
921-
# https://github.com/matplotlib/matplotlib/commit/03a542e875eba091a027046d5ec652daa8be6863
922-
# so we use the code from there
923-
if _compare_version(matplotlib.__version__, '>=', '3.2.0'):
924-
cbar.ax.set_ylim(cbar_vmin, cbar_vmax)
925-
X = cbar._mesh()[0]
926-
X = np.array([X[0], X[-1]])
927-
Y = np.array([[cbar_vmin, cbar_vmin], [cbar_vmax, cbar_vmax]])
928-
N = X.shape[0]
929-
ii = [0, 1, N - 2, N - 1, 2 * N - 1, 2 * N - 2, N + 1, N, 0]
930-
x = X.T.reshape(-1)[ii]
931-
y = Y.T.reshape(-1)[ii]
932-
xy = (np.column_stack([y, x])
933-
if cbar.orientation == 'horizontal' else
934-
np.column_stack([x, y]))
935-
cbar.outline.set_xy(xy)
936-
else:
937-
cbar.ax.set_ylim(cbar.norm(cbar_vmin), cbar.norm(cbar_vmax))
938-
outline = cbar.outline.get_xy()
939-
outline[:2, 1] += cbar.norm(cbar_vmin)
940-
outline[2:6, 1] -= (1. - cbar.norm(cbar_vmax))
941-
outline[6:, 1] += cbar.norm(cbar_vmin)
942-
cbar.outline.set_xy(outline)
791+
cbar.ax.set_ylim(cbar_vmin, cbar_vmax)
792+
X = cbar._mesh()[0]
793+
X = np.array([X[0], X[-1]])
794+
Y = np.array([[cbar_vmin, cbar_vmin], [cbar_vmax, cbar_vmax]])
795+
N = X.shape[0]
796+
ii = [0, 1, N - 2, N - 1, 2 * N - 1, 2 * N - 2, N + 1, N, 0]
797+
x = X.T.reshape(-1)[ii]
798+
y = Y.T.reshape(-1)[ii]
799+
xy = (np.column_stack([y, x])
800+
if cbar.orientation == 'horizontal' else
801+
np.column_stack([x, y]))
802+
cbar.outline.set_xy(xy)
943803

944804
cbar.set_ticks(new_tick_locs)
945805
cbar.update_ticks()
@@ -951,7 +811,7 @@ def _crop_colorbar(cbar, cbar_vmin, cbar_vmax):
951811
# Here we choose different defaults to speed things up by default
952812
try:
953813
import numba
954-
if _compare_version(numba.__version__, '<', '0.48'):
814+
if _compare_version(numba.__version__, '<', '0.53.1'):
955815
raise ImportError
956816
prange = numba.prange
957817
def jit(nopython=True, nogil=True, fastmath=True, cache=True,

mne/surface.py

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@
1313
from collections import OrderedDict
1414
from glob import glob
1515
from os import path as op
16-
from struct import pack
1716
import time
1817
import warnings
1918

2019
import numpy as np
2120

2221
from .channels.channels import _get_meg_system
23-
from .fixes import (_serialize_volume_info, _get_read_geometry, jit,
24-
prange, bincount)
22+
from .fixes import jit, prange, bincount
2523
from .io.constants import FIFF
2624
from .io.pick import pick_types
2725
from .parallel import parallel_func
@@ -810,6 +808,7 @@ def read_surface(fname, read_metadata=False, return_dict=False,
810808
write_surface
811809
read_tri
812810
"""
811+
from ._freesurfer import _import_nibabel
813812
fname = _check_fname(fname, 'read', True)
814813
_check_option('file_format', file_format, ['auto', 'freesurfer', 'obj'])
815814

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

822821
if file_format == 'freesurfer':
823-
ret = _get_read_geometry()(fname, read_metadata=read_metadata)
822+
_import_nibabel('read surface geometry')
823+
from nibabel.freesurfer import read_geometry
824+
ret = read_geometry(fname, read_metadata=read_metadata)
824825
elif file_format == 'obj':
825826
ret = _read_wavefront_obj(fname)
826827
if read_metadata:
@@ -1185,6 +1186,7 @@ def write_surface(fname, coords, faces, create_stamp='', volume_info=None,
11851186
read_surface
11861187
read_tri
11871188
"""
1189+
from ._freesurfer import _import_nibabel
11881190
fname = _check_fname(fname, overwrite=overwrite)
11891191
_check_option('file_format', file_format, ['auto', 'freesurfer', 'obj'])
11901192

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

11971199
if file_format == 'freesurfer':
1198-
try:
1199-
import nibabel as nib
1200-
has_nibabel = True
1201-
except ImportError:
1202-
has_nibabel = False
1203-
if has_nibabel:
1204-
nib.freesurfer.io.write_geometry(fname, coords, faces,
1205-
create_stamp=create_stamp,
1206-
volume_info=volume_info)
1207-
return
1208-
if len(create_stamp.splitlines()) > 1:
1209-
raise ValueError("create_stamp can only contain one line")
1210-
1211-
with open(fname, 'wb') as fid:
1212-
fid.write(pack('>3B', 255, 255, 254))
1213-
strs = ['%s\n' % create_stamp, '\n']
1214-
strs = [s.encode('utf-8') for s in strs]
1215-
fid.writelines(strs)
1216-
vnum = len(coords)
1217-
fnum = len(faces)
1218-
fid.write(pack('>2i', vnum, fnum))
1219-
fid.write(np.array(coords, dtype='>f4').tobytes())
1220-
fid.write(np.array(faces, dtype='>i4').tobytes())
1221-
1222-
# Add volume info, if given
1223-
if volume_info is not None and len(volume_info) > 0:
1224-
fid.write(_serialize_volume_info(volume_info))
1225-
1226-
elif file_format == 'obj':
1200+
_import_nibabel('write surface geometry')
1201+
from nibabel.freesurfer import write_geometry
1202+
write_geometry(
1203+
fname, coords, faces, create_stamp=create_stamp,
1204+
volume_info=volume_info)
1205+
else:
1206+
assert file_format == 'obj'
12271207
with open(fname, 'w') as fid:
12281208
for line in create_stamp.splitlines():
12291209
fid.write(f'# {line}\n')

mne/tests/test_annotations.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,6 @@ def test_annotations_simple_iteration():
10621062
assert elem == expected_value
10631063

10641064

1065-
@requires_version('numpy', '1.12')
10661065
def test_annotations_slices():
10671066
"""Test indexing Annotations."""
10681067
NUM_ANNOT = 5

mne/tests/test_surface.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def test_compute_nearest():
118118
@testing.requires_testing_data
119119
def test_io_surface(tmp_path):
120120
"""Test reading and writing of Freesurfer surface mesh files."""
121+
pytest.importorskip('nibabel')
121122
fname_quad = data_path / "subjects" / "bert" / "surf" / "lh.inflated.nofix"
122123
fname_tri = data_path / "subjects" / "sample" / "bem" / "inner_skull.surf"
123124
for fname in (fname_quad, fname_tri):
@@ -155,6 +156,7 @@ def test_io_surface(tmp_path):
155156
@testing.requires_testing_data
156157
def test_read_curv():
157158
"""Test reading curvature data."""
159+
pytest.importorskip('nibabel')
158160
fname_curv = data_path / "subjects" / "fsaverage" / "surf" / "lh.curv"
159161
fname_surf = data_path / "subjects" / "fsaverage" / "surf" / "lh.inflated"
160162
bin_curv = read_curvature(fname_curv)

0 commit comments

Comments
 (0)