initial commit
This commit is contained in:
917
mne/utils/config.py
Normal file
917
mne/utils/config.py
Normal file
@@ -0,0 +1,917 @@
|
||||
"""The config functions."""
|
||||
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import atexit
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import os.path as op
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from functools import lru_cache, partial
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from urllib.error import URLError
|
||||
from urllib.request import urlopen
|
||||
|
||||
from packaging.version import parse
|
||||
|
||||
from ._logging import logger, warn
|
||||
from .check import _check_fname, _check_option, _check_qt_version, _validate_type
|
||||
from .docs import fill_doc
|
||||
from .misc import _pl
|
||||
|
||||
_temp_home_dir = None
|
||||
|
||||
|
||||
class UnknownPlatformError(Exception):
|
||||
"""Exception raised for unknown platforms."""
|
||||
|
||||
|
||||
def set_cache_dir(cache_dir):
|
||||
"""Set the directory to be used for temporary file storage.
|
||||
|
||||
This directory is used by joblib to store memmapped arrays,
|
||||
which reduces memory requirements and speeds up parallel
|
||||
computation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cache_dir : str or None
|
||||
Directory to use for temporary file storage. None disables
|
||||
temporary file storage.
|
||||
"""
|
||||
if cache_dir is not None and not op.exists(cache_dir):
|
||||
raise OSError(f"Directory {cache_dir} does not exist")
|
||||
|
||||
set_config("MNE_CACHE_DIR", cache_dir, set_env=False)
|
||||
|
||||
|
||||
def set_memmap_min_size(memmap_min_size):
|
||||
"""Set the minimum size for memmaping of arrays for parallel processing.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
memmap_min_size : str or None
|
||||
Threshold on the minimum size of arrays that triggers automated memory
|
||||
mapping for parallel processing, e.g., '1M' for 1 megabyte.
|
||||
Use None to disable memmaping of large arrays.
|
||||
"""
|
||||
_validate_type(memmap_min_size, (str, None), "memmap_min_size")
|
||||
if memmap_min_size is not None:
|
||||
if memmap_min_size[-1] not in ["K", "M", "G"]:
|
||||
raise ValueError(
|
||||
"The size has to be given in kilo-, mega-, or "
|
||||
f"gigabytes, e.g., 100K, 500M, 1G, got {repr(memmap_min_size)}"
|
||||
)
|
||||
|
||||
set_config("MNE_MEMMAP_MIN_SIZE", memmap_min_size, set_env=False)
|
||||
|
||||
|
||||
# List the known configuration values
|
||||
_known_config_types = {
|
||||
"MNE_3D_OPTION_ANTIALIAS": (
|
||||
"bool, whether to use full-screen antialiasing in 3D plots"
|
||||
),
|
||||
"MNE_3D_OPTION_DEPTH_PEELING": "bool, whether to use depth peeling in 3D plots",
|
||||
"MNE_3D_OPTION_MULTI_SAMPLES": (
|
||||
"int, number of samples to use for full-screen antialiasing"
|
||||
),
|
||||
"MNE_3D_OPTION_SMOOTH_SHADING": ("bool, whether to use smooth shading in 3D plots"),
|
||||
"MNE_3D_OPTION_THEME": ("str, the color theme (light or dark) to use for 3D plots"),
|
||||
"MNE_BROWSE_RAW_SIZE": (
|
||||
"tuple, width and height of the raw browser window (in inches)"
|
||||
),
|
||||
"MNE_BROWSER_BACKEND": (
|
||||
"str, the backend to use for the MNE Browse Raw window (qt or matplotlib)"
|
||||
),
|
||||
"MNE_BROWSER_OVERVIEW_MODE": (
|
||||
"str, the overview mode to use in the MNE Browse Raw window )"
|
||||
"(see mne.viz.plot_raw for valid options)"
|
||||
),
|
||||
"MNE_BROWSER_PRECOMPUTE": (
|
||||
"bool, whether to precompute raw data in the MNE Browse Raw window"
|
||||
),
|
||||
"MNE_BROWSER_THEME": "str, the color theme (light or dark) to use for the browser",
|
||||
"MNE_BROWSER_USE_OPENGL": (
|
||||
"bool, whether to use OpenGL for rendering in the MNE Browse Raw window"
|
||||
),
|
||||
"MNE_CACHE_DIR": "str, path to the cache directory for parallel execution",
|
||||
"MNE_COREG_ADVANCED_RENDERING": (
|
||||
"bool, whether to use advanced OpenGL rendering in mne coreg"
|
||||
),
|
||||
"MNE_COREG_COPY_ANNOT": (
|
||||
"bool, whether to copy the annotation files during warping"
|
||||
),
|
||||
"MNE_COREG_FULLSCREEN": "bool, whether to use full-screen mode in mne coreg",
|
||||
"MNE_COREG_GUESS_MRI_SUBJECT": (
|
||||
"bool, whether to guess the MRI subject in mne coreg"
|
||||
),
|
||||
"MNE_COREG_HEAD_HIGH_RES": (
|
||||
"bool, whether to use high-res head surface in mne coreg"
|
||||
),
|
||||
"MNE_COREG_HEAD_OPACITY": ("bool, the head surface opacity to use in mne coreg"),
|
||||
"MNE_COREG_HEAD_INSIDE": (
|
||||
"bool, whether to add an opaque inner scalp head surface to help "
|
||||
"occlude points behind the head in mne coreg"
|
||||
),
|
||||
"MNE_COREG_INTERACTION": (
|
||||
"str, interaction style in mne coreg (trackball or terrain)"
|
||||
),
|
||||
"MNE_COREG_MARK_INSIDE": (
|
||||
"bool, whether to mark points inside the head surface in mne coreg"
|
||||
),
|
||||
"MNE_COREG_PREPARE_BEM": (
|
||||
"bool, whether to prepare the BEM solution after warping in mne coreg"
|
||||
),
|
||||
"MNE_COREG_ORIENT_TO_SURFACE": (
|
||||
"bool, whether to orient the digitization markers to the head surface "
|
||||
"in mne coreg"
|
||||
),
|
||||
"MNE_COREG_SCALE_LABELS": (
|
||||
"bool, whether to scale the MRI labels during warping in mne coreg"
|
||||
),
|
||||
"MNE_COREG_SCALE_BY_DISTANCE": (
|
||||
"bool, whether to scale the digitization markers by their distance from "
|
||||
"the scalp in mne coreg"
|
||||
),
|
||||
"MNE_COREG_SCENE_SCALE": (
|
||||
"float, the scale factor of the 3D scene in mne coreg (default 0.16)"
|
||||
),
|
||||
"MNE_COREG_WINDOW_HEIGHT": "int, window height for mne coreg",
|
||||
"MNE_COREG_WINDOW_WIDTH": "int, window width for mne coreg",
|
||||
"MNE_COREG_SUBJECTS_DIR": "str, path to the subjects directory for mne coreg",
|
||||
"MNE_CUDA_DEVICE": "int, CUDA device to use for GPU processing",
|
||||
"MNE_DATA": "str, default data directory",
|
||||
"MNE_DATASETS_BRAINSTORM_PATH": "str, path for brainstorm data",
|
||||
"MNE_DATASETS_EEGBCI_PATH": "str, path for EEGBCI data",
|
||||
"MNE_DATASETS_EPILEPSY_ECOG_PATH": "str, path for epilepsy_ecog data",
|
||||
"MNE_DATASETS_HF_SEF_PATH": "str, path for HF_SEF data",
|
||||
"MNE_DATASETS_MEGSIM_PATH": "str, path for MEGSIM data",
|
||||
"MNE_DATASETS_MISC_PATH": "str, path for misc data",
|
||||
"MNE_DATASETS_MTRF_PATH": "str, path for MTRF data",
|
||||
"MNE_DATASETS_SAMPLE_PATH": "str, path for sample data",
|
||||
"MNE_DATASETS_SOMATO_PATH": "str, path for somato data",
|
||||
"MNE_DATASETS_MULTIMODAL_PATH": "str, path for multimodal data",
|
||||
"MNE_DATASETS_FNIRS_MOTOR_PATH": "str, path for fnirs_motor data",
|
||||
"MNE_DATASETS_OPM_PATH": "str, path for OPM data",
|
||||
"MNE_DATASETS_SPM_FACE_DATASETS_TESTS": "str, path for spm_face data",
|
||||
"MNE_DATASETS_SPM_FACE_PATH": "str, path for spm_face data",
|
||||
"MNE_DATASETS_TESTING_PATH": "str, path for testing data",
|
||||
"MNE_DATASETS_VISUAL_92_CATEGORIES_PATH": "str, path for visual_92_categories data",
|
||||
"MNE_DATASETS_KILOWORD_PATH": "str, path for kiloword data",
|
||||
"MNE_DATASETS_FIELDTRIP_CMC_PATH": "str, path for fieldtrip_cmc data",
|
||||
"MNE_DATASETS_PHANTOM_KIT_PATH": "str, path for phantom_kit data",
|
||||
"MNE_DATASETS_PHANTOM_4DBTI_PATH": "str, path for phantom_4dbti data",
|
||||
"MNE_DATASETS_PHANTOM_KERNEL_PATH": "str, path for phantom_kernel data",
|
||||
"MNE_DATASETS_LIMO_PATH": "str, path for limo data",
|
||||
"MNE_DATASETS_REFMEG_NOISE_PATH": "str, path for refmeg_noise data",
|
||||
"MNE_DATASETS_SSVEP_PATH": "str, path for ssvep data",
|
||||
"MNE_DATASETS_ERP_CORE_PATH": "str, path for erp_core data",
|
||||
"MNE_FORCE_SERIAL": "bool, force serial rather than parallel execution",
|
||||
"MNE_LOGGING_LEVEL": (
|
||||
"str or int, controls the level of verbosity of any function "
|
||||
"decorated with @verbose. See "
|
||||
"https://mne.tools/stable/auto_tutorials/intro/50_configure_mne.html#logging"
|
||||
),
|
||||
"MNE_MEMMAP_MIN_SIZE": (
|
||||
"str, threshold on the minimum size of arrays passed to the workers that "
|
||||
"triggers automated memory mapping, e.g., 1M or 0.5G"
|
||||
),
|
||||
"MNE_REPR_HTML": (
|
||||
"bool, represent some of our objects with rich HTML in a notebook "
|
||||
"environment"
|
||||
),
|
||||
"MNE_SKIP_NETWORK_TESTS": (
|
||||
"bool, used in a test decorator (@requires_good_network) to skip "
|
||||
"tests that include large downloads"
|
||||
),
|
||||
"MNE_SKIP_TESTING_DATASET_TESTS": (
|
||||
"bool, used in test decorators (@requires_spm_data, "
|
||||
"@requires_bstraw_data) to skip tests that require specific datasets"
|
||||
),
|
||||
"MNE_STIM_CHANNEL": "string, the default channel name for mne.find_events",
|
||||
"MNE_TQDM": (
|
||||
'str, either "tqdm", "tqdm.auto", or "off". Controls presence/absence '
|
||||
"of progress bars"
|
||||
),
|
||||
"MNE_USE_CUDA": "bool, use GPU for filtering/resampling",
|
||||
"MNE_USE_NUMBA": (
|
||||
"bool, use Numba just-in-time compiler for some of our intensive "
|
||||
"computations"
|
||||
),
|
||||
"SUBJECTS_DIR": "path-like, directory of freesurfer MRI files for each subject",
|
||||
}
|
||||
|
||||
# These allow for partial matches, e.g. 'MNE_STIM_CHANNEL_1' is okay key
|
||||
_known_config_wildcards = (
|
||||
"MNE_STIM_CHANNEL", # can have multiple stim channels
|
||||
"MNE_DATASETS_FNIRS", # mne-nirs
|
||||
"MNE_NIRS", # mne-nirs
|
||||
"MNE_KIT2FIFF", # mne-kit-gui
|
||||
"MNE_ICALABEL", # mne-icalabel
|
||||
"MNE_LSL", # mne-lsl
|
||||
)
|
||||
|
||||
|
||||
def _load_config(config_path, raise_error=False):
|
||||
"""Safely load a config file."""
|
||||
with open(config_path) as fid:
|
||||
try:
|
||||
config = json.load(fid)
|
||||
except ValueError:
|
||||
# No JSON object could be decoded --> corrupt file?
|
||||
msg = (
|
||||
f"The MNE-Python config file ({config_path}) is not a valid JSON "
|
||||
"file and might be corrupted"
|
||||
)
|
||||
if raise_error:
|
||||
raise RuntimeError(msg)
|
||||
warn(msg)
|
||||
config = dict()
|
||||
return config
|
||||
|
||||
|
||||
def get_config_path(home_dir=None):
|
||||
r"""Get path to standard mne-python config file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
home_dir : str | None
|
||||
The folder that contains the .mne config folder.
|
||||
If None, it is found automatically.
|
||||
|
||||
Returns
|
||||
-------
|
||||
config_path : str
|
||||
The path to the mne-python configuration file. On windows, this
|
||||
will be '%USERPROFILE%\.mne\mne-python.json'. On every other
|
||||
system, this will be ~/.mne/mne-python.json.
|
||||
"""
|
||||
val = op.join(_get_extra_data_path(home_dir=home_dir), "mne-python.json")
|
||||
return val
|
||||
|
||||
|
||||
def get_config(key=None, default=None, raise_error=False, home_dir=None, use_env=True):
|
||||
"""Read MNE-Python preferences from environment or config file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : None | str
|
||||
The preference key to look for. The os environment is searched first,
|
||||
then the mne-python config file is parsed.
|
||||
If None, all the config parameters present in environment variables or
|
||||
the path are returned. If key is an empty string, a list of all valid
|
||||
keys (but not values) is returned.
|
||||
default : str | None
|
||||
Value to return if the key is not found.
|
||||
raise_error : bool
|
||||
If True, raise an error if the key is not found (instead of returning
|
||||
default).
|
||||
home_dir : str | None
|
||||
The folder that contains the .mne config folder.
|
||||
If None, it is found automatically.
|
||||
use_env : bool
|
||||
If True, consider env vars, if available.
|
||||
If False, only use MNE-Python configuration file values.
|
||||
|
||||
.. versionadded:: 0.18
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : dict | str | None
|
||||
The preference key value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
set_config
|
||||
"""
|
||||
_validate_type(key, (str, type(None)), "key", "string or None")
|
||||
|
||||
if key == "":
|
||||
# These are str->str (immutable) so we should just copy the dict
|
||||
# itself, no need for deepcopy
|
||||
return _known_config_types.copy()
|
||||
|
||||
# first, check to see if key is in env
|
||||
if use_env and key is not None and key in os.environ:
|
||||
return os.environ[key]
|
||||
|
||||
# second, look for it in mne-python config file
|
||||
config_path = get_config_path(home_dir=home_dir)
|
||||
if not op.isfile(config_path):
|
||||
config = {}
|
||||
else:
|
||||
config = _load_config(config_path)
|
||||
|
||||
if key is None:
|
||||
# update config with environment variables
|
||||
if use_env:
|
||||
env_keys = set(config).union(_known_config_types).intersection(os.environ)
|
||||
config.update({key: os.environ[key] for key in env_keys})
|
||||
return config
|
||||
elif raise_error is True and key not in config:
|
||||
loc_env = "the environment or in the " if use_env else ""
|
||||
meth_env = (
|
||||
(f'either os.environ["{key}"] = VALUE for a temporary solution, or ')
|
||||
if use_env
|
||||
else ""
|
||||
)
|
||||
extra_env = (
|
||||
" You can also set the environment variable before running python."
|
||||
if use_env
|
||||
else ""
|
||||
)
|
||||
meth_file = (
|
||||
f'mne.utils.set_config("{key}", VALUE, set_env=True) for a permanent one'
|
||||
)
|
||||
raise KeyError(
|
||||
f'Key "{key}" not found in {loc_env}'
|
||||
f"the mne-python config file ({config_path}). "
|
||||
f"Try {meth_env}{meth_file}.{extra_env}"
|
||||
)
|
||||
else:
|
||||
return config.get(key, default)
|
||||
|
||||
|
||||
def set_config(key, value, home_dir=None, set_env=True):
|
||||
"""Set a MNE-Python preference key in the config file and environment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : str
|
||||
The preference key to set.
|
||||
value : str | None
|
||||
The value to assign to the preference key. If None, the key is
|
||||
deleted.
|
||||
home_dir : str | None
|
||||
The folder that contains the .mne config folder.
|
||||
If None, it is found automatically.
|
||||
set_env : bool
|
||||
If True (default), update :data:`os.environ` in addition to
|
||||
updating the MNE-Python config file.
|
||||
|
||||
See Also
|
||||
--------
|
||||
get_config
|
||||
"""
|
||||
_validate_type(key, "str", "key")
|
||||
# While JSON allow non-string types, we allow users to override config
|
||||
# settings using env, which are strings, so we enforce that here
|
||||
_validate_type(value, (str, "path-like", type(None)), "value")
|
||||
if value is not None:
|
||||
value = str(value)
|
||||
|
||||
if key not in _known_config_types and not any(
|
||||
key.startswith(k) for k in _known_config_wildcards
|
||||
):
|
||||
warn(f'Setting non-standard config type: "{key}"')
|
||||
|
||||
# Read all previous values
|
||||
config_path = get_config_path(home_dir=home_dir)
|
||||
if op.isfile(config_path):
|
||||
config = _load_config(config_path, raise_error=True)
|
||||
else:
|
||||
config = dict()
|
||||
logger.info(
|
||||
f"Attempting to create new mne-python configuration file:\n{config_path}"
|
||||
)
|
||||
if value is None:
|
||||
config.pop(key, None)
|
||||
if set_env and key in os.environ:
|
||||
del os.environ[key]
|
||||
else:
|
||||
config[key] = value
|
||||
if set_env:
|
||||
os.environ[key] = value
|
||||
if key == "MNE_BROWSER_BACKEND":
|
||||
from ..viz._figure import set_browser_backend
|
||||
|
||||
set_browser_backend(value)
|
||||
|
||||
# Write all values. This may fail if the default directory is not
|
||||
# writeable.
|
||||
directory = op.dirname(config_path)
|
||||
if not op.isdir(directory):
|
||||
os.mkdir(directory)
|
||||
with open(config_path, "w") as fid:
|
||||
json.dump(config, fid, sort_keys=True, indent=0)
|
||||
|
||||
|
||||
def _get_extra_data_path(home_dir=None):
|
||||
"""Get path to extra data (config, tables, etc.)."""
|
||||
global _temp_home_dir
|
||||
if home_dir is None:
|
||||
home_dir = os.environ.get("_MNE_FAKE_HOME_DIR")
|
||||
if home_dir is None:
|
||||
# this has been checked on OSX64, Linux64, and Win32
|
||||
if "nt" == os.name.lower():
|
||||
APPDATA_DIR = os.getenv("APPDATA")
|
||||
USERPROFILE_DIR = os.getenv("USERPROFILE")
|
||||
if APPDATA_DIR is not None and op.isdir(
|
||||
op.join(APPDATA_DIR, ".mne")
|
||||
): # backward-compat
|
||||
home_dir = APPDATA_DIR
|
||||
elif USERPROFILE_DIR is not None:
|
||||
home_dir = USERPROFILE_DIR
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
"The USERPROFILE environment variable is not set, cannot "
|
||||
"determine the location of the MNE-Python configuration "
|
||||
"folder"
|
||||
)
|
||||
del APPDATA_DIR, USERPROFILE_DIR
|
||||
else:
|
||||
# This is a more robust way of getting the user's home folder on
|
||||
# Linux platforms (not sure about OSX, Unix or BSD) than checking
|
||||
# the HOME environment variable. If the user is running some sort
|
||||
# of script that isn't launched via the command line (e.g. a script
|
||||
# launched via Upstart) then the HOME environment variable will
|
||||
# not be set.
|
||||
if os.getenv("MNE_DONTWRITE_HOME", "") == "true":
|
||||
if _temp_home_dir is None:
|
||||
_temp_home_dir = tempfile.mkdtemp()
|
||||
atexit.register(
|
||||
partial(shutil.rmtree, _temp_home_dir, ignore_errors=True)
|
||||
)
|
||||
home_dir = _temp_home_dir
|
||||
else:
|
||||
home_dir = os.path.expanduser("~")
|
||||
|
||||
if home_dir is None:
|
||||
raise ValueError(
|
||||
"mne-python config file path could "
|
||||
"not be determined, please report this "
|
||||
"error to mne-python developers"
|
||||
)
|
||||
|
||||
return op.join(home_dir, ".mne")
|
||||
|
||||
|
||||
def get_subjects_dir(subjects_dir=None, raise_error=False):
|
||||
"""Safely use subjects_dir input to return SUBJECTS_DIR.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subjects_dir : path-like | None
|
||||
If a value is provided, return subjects_dir. Otherwise, look for
|
||||
SUBJECTS_DIR config and return the result.
|
||||
raise_error : bool
|
||||
If True, raise a KeyError if no value for SUBJECTS_DIR can be found
|
||||
(instead of returning None).
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : Path | None
|
||||
The SUBJECTS_DIR value.
|
||||
"""
|
||||
from_config = False
|
||||
if subjects_dir is None:
|
||||
subjects_dir = get_config("SUBJECTS_DIR", raise_error=raise_error)
|
||||
from_config = True
|
||||
if subjects_dir is not None:
|
||||
subjects_dir = Path(subjects_dir)
|
||||
if subjects_dir is not None:
|
||||
# Emit a nice error or warning if their config is bad
|
||||
try:
|
||||
subjects_dir = _check_fname(
|
||||
fname=subjects_dir,
|
||||
overwrite="read",
|
||||
must_exist=True,
|
||||
need_dir=True,
|
||||
name="subjects_dir",
|
||||
)
|
||||
except FileNotFoundError:
|
||||
if from_config:
|
||||
msg = (
|
||||
"SUBJECTS_DIR in your MNE-Python configuration or environment "
|
||||
"does not exist, consider using mne.set_config to fix it: "
|
||||
f"{subjects_dir}"
|
||||
)
|
||||
if raise_error:
|
||||
raise FileNotFoundError(msg) from None
|
||||
else:
|
||||
warn(msg)
|
||||
elif raise_error:
|
||||
raise
|
||||
|
||||
return subjects_dir
|
||||
|
||||
|
||||
@fill_doc
|
||||
def _get_stim_channel(stim_channel, info, raise_error=True):
|
||||
"""Determine the appropriate stim_channel.
|
||||
|
||||
First, 'MNE_STIM_CHANNEL', 'MNE_STIM_CHANNEL_1', 'MNE_STIM_CHANNEL_2', etc.
|
||||
are read. If these are not found, it will fall back to 'STI 014' if
|
||||
present, then fall back to the first channel of type 'stim', if present.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stim_channel : str | list of str | None
|
||||
The stim channel selected by the user.
|
||||
%(info_not_none)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stim_channel : list of str
|
||||
The name of the stim channel(s) to use
|
||||
"""
|
||||
from .._fiff.pick import pick_types
|
||||
|
||||
if stim_channel is not None:
|
||||
if not isinstance(stim_channel, list):
|
||||
_validate_type(stim_channel, "str", "Stim channel")
|
||||
stim_channel = [stim_channel]
|
||||
for channel in stim_channel:
|
||||
_validate_type(channel, "str", "Each provided stim channel")
|
||||
return stim_channel
|
||||
|
||||
stim_channel = list()
|
||||
ch_count = 0
|
||||
ch = get_config("MNE_STIM_CHANNEL")
|
||||
while ch is not None and ch in info["ch_names"]:
|
||||
stim_channel.append(ch)
|
||||
ch_count += 1
|
||||
ch = get_config(f"MNE_STIM_CHANNEL_{ch_count}")
|
||||
if ch_count > 0:
|
||||
return stim_channel
|
||||
|
||||
if "STI101" in info["ch_names"]: # combination channel for newer systems
|
||||
return ["STI101"]
|
||||
if "STI 014" in info["ch_names"]: # for older systems
|
||||
return ["STI 014"]
|
||||
|
||||
stim_channel = pick_types(info, meg=False, ref_meg=False, stim=True)
|
||||
if len(stim_channel) == 0 and raise_error:
|
||||
raise ValueError(
|
||||
"No stim channels found. Consider specifying them "
|
||||
"manually using the 'stim_channel' parameter."
|
||||
)
|
||||
stim_channel = [info["ch_names"][ch_] for ch_ in stim_channel]
|
||||
return stim_channel
|
||||
|
||||
|
||||
def _get_root_dir():
|
||||
"""Get as close to the repo root as possible."""
|
||||
root_dir = Path(__file__).parents[1]
|
||||
up_dir = root_dir.parent
|
||||
if (up_dir / "setup.py").is_file() and all(
|
||||
(up_dir / x).is_dir() for x in ("mne", "examples", "doc")
|
||||
):
|
||||
root_dir = up_dir
|
||||
return root_dir
|
||||
|
||||
|
||||
def _get_numpy_libs():
|
||||
bad_lib = "unknown linalg bindings"
|
||||
try:
|
||||
from threadpoolctl import threadpool_info
|
||||
except Exception as exc:
|
||||
return bad_lib + f" (threadpoolctl module not found: {exc})"
|
||||
pools = threadpool_info()
|
||||
rename = dict(
|
||||
openblas="OpenBLAS",
|
||||
mkl="MKL",
|
||||
)
|
||||
for pool in pools:
|
||||
if pool["internal_api"] in ("openblas", "mkl"):
|
||||
return (
|
||||
f'{rename[pool["internal_api"]]} '
|
||||
f'{pool["version"]} with '
|
||||
f'{pool["num_threads"]} thread{_pl(pool["num_threads"])}'
|
||||
)
|
||||
return bad_lib
|
||||
|
||||
|
||||
_gpu_cmd = """\
|
||||
from pyvista import GPUInfo; \
|
||||
gi = GPUInfo(); \
|
||||
print(gi.version); \
|
||||
print(gi.renderer)"""
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_gpu_info():
|
||||
# Once https://github.com/pyvista/pyvista/pull/2250 is merged and PyVista
|
||||
# does a release, we can triage based on version > 0.33.2
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-c", _gpu_cmd], check=False, capture_output=True
|
||||
)
|
||||
out = proc.stdout.decode().strip().replace("\r", "").split("\n")
|
||||
if proc.returncode or len(out) != 2:
|
||||
return None, None
|
||||
return out
|
||||
|
||||
|
||||
def _get_total_memory():
|
||||
"""Return the total memory of the system in bytes."""
|
||||
if platform.system() == "Windows":
|
||||
o = subprocess.check_output(
|
||||
[
|
||||
"powershell.exe",
|
||||
"(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory",
|
||||
]
|
||||
).decode()
|
||||
total_memory = int(o)
|
||||
elif platform.system() == "Linux":
|
||||
o = subprocess.check_output(["free", "-b"]).decode()
|
||||
total_memory = int(o.splitlines()[1].split()[1])
|
||||
elif platform.system() == "Darwin":
|
||||
o = subprocess.check_output(["sysctl", "hw.memsize"]).decode()
|
||||
total_memory = int(o.split(":")[1].strip())
|
||||
else:
|
||||
raise UnknownPlatformError("Could not determine total memory")
|
||||
|
||||
return total_memory
|
||||
|
||||
|
||||
def _get_cpu_brand():
|
||||
"""Return the CPU brand string."""
|
||||
if platform.system() == "Windows":
|
||||
o = subprocess.check_output(
|
||||
["powershell.exe", "(Get-CimInstance Win32_Processor).Name"]
|
||||
).decode()
|
||||
cpu_brand = o.strip().splitlines()[-1]
|
||||
elif platform.system() == "Linux":
|
||||
o = subprocess.check_output(["grep", "model name", "/proc/cpuinfo"]).decode()
|
||||
cpu_brand = o.splitlines()[0].split(": ")[1]
|
||||
elif platform.system() == "Darwin":
|
||||
o = subprocess.check_output(["sysctl", "machdep.cpu"]).decode()
|
||||
cpu_brand = o.split("brand_string: ")[1].strip()
|
||||
else:
|
||||
cpu_brand = "?"
|
||||
|
||||
return cpu_brand
|
||||
|
||||
|
||||
def sys_info(
|
||||
fid=None,
|
||||
show_paths=False,
|
||||
*,
|
||||
dependencies="user",
|
||||
unicode="auto",
|
||||
check_version=True,
|
||||
):
|
||||
"""Print system information.
|
||||
|
||||
This function prints system information useful when triaging bugs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fid : file-like | None
|
||||
The file to write to. Will be passed to :func:`print()`. Can be None to
|
||||
use :data:`sys.stdout`.
|
||||
show_paths : bool
|
||||
If True, print paths for each module.
|
||||
dependencies : 'user' | 'developer'
|
||||
Show dependencies relevant for users (default) or for developers
|
||||
(i.e., output includes additional dependencies).
|
||||
unicode : bool | "auto"
|
||||
Include Unicode symbols in output. If "auto", corresponds to True on Linux and
|
||||
macOS, and False on Windows.
|
||||
|
||||
.. versionadded:: 0.24
|
||||
check_version : bool | float
|
||||
If True (default), attempt to check that the version of MNE-Python is up to date
|
||||
with the latest release on GitHub. Can be a float to give a different timeout
|
||||
(in sec) from the default (2 sec).
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
_validate_type(dependencies, str)
|
||||
_check_option("dependencies", dependencies, ("user", "developer"))
|
||||
_validate_type(check_version, (bool, "numeric"), "check_version")
|
||||
_validate_type(unicode, (bool, str), "unicode")
|
||||
_check_option("unicode", unicode, ("auto", True, False))
|
||||
if unicode == "auto":
|
||||
if platform.system() in ("Darwin", "Linux"):
|
||||
unicode = True
|
||||
else: # Windows
|
||||
unicode = False
|
||||
ljust = 24 if dependencies == "developer" else 21
|
||||
platform_str = platform.platform()
|
||||
|
||||
out = partial(print, end="", file=fid)
|
||||
out("Platform".ljust(ljust) + platform_str + "\n")
|
||||
out("Python".ljust(ljust) + str(sys.version).replace("\n", " ") + "\n")
|
||||
out("Executable".ljust(ljust) + sys.executable + "\n")
|
||||
try:
|
||||
cpu_brand = _get_cpu_brand()
|
||||
except Exception:
|
||||
cpu_brand = "?"
|
||||
out("CPU".ljust(ljust) + f"{cpu_brand} ")
|
||||
out(f"({multiprocessing.cpu_count()} cores)\n")
|
||||
out("Memory".ljust(ljust))
|
||||
try:
|
||||
total_memory = _get_total_memory()
|
||||
except UnknownPlatformError:
|
||||
total_memory = "?"
|
||||
else:
|
||||
total_memory = f"{total_memory / 1024**3:.1f}" # convert to GiB
|
||||
out(f"{total_memory} GiB\n")
|
||||
out("\n")
|
||||
ljust -= 3 # account for +/- symbols
|
||||
libs = _get_numpy_libs()
|
||||
unavailable = []
|
||||
use_mod_names = (
|
||||
"# Core",
|
||||
"mne",
|
||||
"numpy",
|
||||
"scipy",
|
||||
"matplotlib",
|
||||
"",
|
||||
"# Numerical (optional)",
|
||||
"sklearn",
|
||||
"numba",
|
||||
"nibabel",
|
||||
"nilearn",
|
||||
"dipy",
|
||||
"openmeeg",
|
||||
"cupy",
|
||||
"pandas",
|
||||
"h5io",
|
||||
"h5py",
|
||||
"",
|
||||
"# Visualization (optional)",
|
||||
"pyvista",
|
||||
"pyvistaqt",
|
||||
"vtk",
|
||||
"qtpy",
|
||||
"ipympl",
|
||||
"pyqtgraph",
|
||||
"mne-qt-browser",
|
||||
"ipywidgets",
|
||||
# "trame", # no version, see https://github.com/Kitware/trame/issues/183
|
||||
"trame_client",
|
||||
"trame_server",
|
||||
"trame_vtk",
|
||||
"trame_vuetify",
|
||||
"",
|
||||
"# Ecosystem (optional)",
|
||||
"mne-bids",
|
||||
"mne-nirs",
|
||||
"mne-features",
|
||||
"mne-connectivity",
|
||||
"mne-icalabel",
|
||||
"mne-bids-pipeline",
|
||||
"neo",
|
||||
"eeglabio",
|
||||
"edfio",
|
||||
"mffpy",
|
||||
"pybv",
|
||||
"",
|
||||
)
|
||||
if dependencies == "developer":
|
||||
use_mod_names += (
|
||||
"# Testing",
|
||||
"pytest",
|
||||
"statsmodels",
|
||||
"numpydoc",
|
||||
"flake8",
|
||||
"jupyter_client",
|
||||
"nbclient",
|
||||
"nbformat",
|
||||
"pydocstyle",
|
||||
"nitime",
|
||||
"imageio",
|
||||
"imageio-ffmpeg",
|
||||
"snirf",
|
||||
"",
|
||||
"# Documentation",
|
||||
"sphinx",
|
||||
"sphinx-gallery",
|
||||
"pydata-sphinx-theme",
|
||||
"",
|
||||
"# Infrastructure",
|
||||
"decorator",
|
||||
"jinja2",
|
||||
# "lazy-loader",
|
||||
"packaging",
|
||||
"pooch",
|
||||
"tqdm",
|
||||
"",
|
||||
)
|
||||
try:
|
||||
unicode = unicode and (sys.stdout.encoding.lower().startswith("utf"))
|
||||
except Exception: # in case someone overrides sys.stdout in an unsafe way
|
||||
unicode = False
|
||||
mne_version_good = True
|
||||
for mi, mod_name in enumerate(use_mod_names):
|
||||
# upcoming break
|
||||
if mod_name == "": # break
|
||||
if unavailable:
|
||||
out("└☐ " if unicode else " - ")
|
||||
out("unavailable".ljust(ljust))
|
||||
out(f"{', '.join(unavailable)}\n")
|
||||
unavailable = []
|
||||
if mi != len(use_mod_names) - 1:
|
||||
out("\n")
|
||||
continue
|
||||
elif mod_name.startswith("# "): # header
|
||||
mod_name = mod_name.replace("# ", "")
|
||||
out(f"{mod_name}\n")
|
||||
continue
|
||||
pre = "├"
|
||||
last = use_mod_names[mi + 1] == "" and not unavailable
|
||||
if last:
|
||||
pre = "└"
|
||||
try:
|
||||
mod = import_module(mod_name.replace("-", "_"))
|
||||
except Exception:
|
||||
unavailable.append(mod_name)
|
||||
else:
|
||||
mark = "☑" if unicode else "+"
|
||||
mne_extra = ""
|
||||
if mod_name == "mne" and check_version:
|
||||
timeout = 2.0 if check_version is True else float(check_version)
|
||||
mne_version_good, mne_extra = _check_mne_version(timeout)
|
||||
if mne_version_good is None:
|
||||
mne_version_good = True
|
||||
elif not mne_version_good:
|
||||
mark = "☒" if unicode else "X"
|
||||
out(f"{pre}{mark} " if unicode else f" {mark} ")
|
||||
out(f"{mod_name}".ljust(ljust))
|
||||
if mod_name == "vtk":
|
||||
vtk_version = mod.vtkVersion()
|
||||
# 9.0 dev has VersionFull but 9.0 doesn't
|
||||
for attr in ("GetVTKVersionFull", "GetVTKVersion"):
|
||||
if hasattr(vtk_version, attr):
|
||||
version = getattr(vtk_version, attr)()
|
||||
if version != "":
|
||||
out(version)
|
||||
break
|
||||
else:
|
||||
out("unknown")
|
||||
else:
|
||||
out(mod.__version__.lstrip("v"))
|
||||
if mod_name == "numpy":
|
||||
out(f" ({libs})")
|
||||
elif mod_name == "qtpy":
|
||||
version, api = _check_qt_version(return_api=True)
|
||||
out(f" ({api}={version})")
|
||||
elif mod_name == "matplotlib":
|
||||
out(f" (backend={mod.get_backend()})")
|
||||
elif mod_name == "pyvista":
|
||||
version, renderer = _get_gpu_info()
|
||||
if version is None:
|
||||
out(" (OpenGL unavailable)")
|
||||
else:
|
||||
out(f" (OpenGL {version} via {renderer})")
|
||||
elif mod_name == "mne":
|
||||
out(f" ({mne_extra})")
|
||||
# Now comes stuff after the version
|
||||
if show_paths:
|
||||
if last:
|
||||
pre = " "
|
||||
elif unicode:
|
||||
pre = "│ "
|
||||
else:
|
||||
pre = " | "
|
||||
out(f'\n{pre}{" " * ljust}{op.dirname(mod.__file__)}')
|
||||
out("\n")
|
||||
|
||||
if not mne_version_good:
|
||||
out(
|
||||
"\nTo update to the latest supported release version to get bugfixes and "
|
||||
"improvements, visit "
|
||||
"https://mne.tools/stable/install/updating.html\n"
|
||||
)
|
||||
|
||||
|
||||
def _get_latest_version(timeout):
|
||||
# Bandit complains about urlopen, but we know the URL here
|
||||
url = "https://api.github.com/repos/mne-tools/mne-python/releases/latest"
|
||||
try:
|
||||
with urlopen(url, timeout=timeout) as f: # nosec
|
||||
response = json.load(f)
|
||||
except (URLError, TimeoutError) as err:
|
||||
# Triage error type
|
||||
if "SSL" in str(err):
|
||||
return "SSL error"
|
||||
elif "timed out" in str(err):
|
||||
return f"timeout after {timeout} sec"
|
||||
else:
|
||||
return f"unknown error: {err}"
|
||||
else:
|
||||
return response["tag_name"].lstrip("v") or "version unknown"
|
||||
|
||||
|
||||
def _check_mne_version(timeout):
|
||||
rel_ver = _get_latest_version(timeout)
|
||||
if not rel_ver[0].isnumeric():
|
||||
return None, (f"unable to check for latest version on GitHub, {rel_ver}")
|
||||
rel_ver = parse(rel_ver)
|
||||
this_ver = parse(import_module("mne").__version__)
|
||||
if this_ver > rel_ver:
|
||||
return True, f"devel, latest release is {rel_ver}"
|
||||
if this_ver == rel_ver:
|
||||
return True, "latest release"
|
||||
else:
|
||||
return False, f"outdated, release {rel_ver} is available!"
|
||||
Reference in New Issue
Block a user