From e2a78551461d34145e5420831fdc985f9da0f2e4 Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Sun, 15 Jun 2014 19:50:30 +0200 Subject: [PATCH 01/12] MVAR connectivity and example misc ENH: matrix-type connectivity visualization ENH: node colors moved plot_connectivity_matrix below plot_connectivity_circle ENH: improved MVAR interface rearranged function arguments Improved MVAR example fixed beta bandwidth added scot external improved documentation added new functions to documentation python 2 compatibility added externals.scot to setup.py updated scot package Adjusted MVAR interface to new scot structure updated scot tried to delete plotting scot: mne compatibility update added tests for MVAR connectivity fixed order of mean and abs ENH: model order selection (cross-validated) example cleanup no testin for model order eric's suggestions incorporated ENH: circle plot show node names in the status bar ENH: connectivity matrix shows node names and value in status bar ENH: function to plot in/out connectivity for one seed ENH: connectivity statistics! fixed string type checking mvar connectivity returns p_vals=None if no statistics done adjusted test to new mvar_connectivity return values updated scot (parallelization) joblib support for mvar_connectivity scot: fixed some parallelization issues misc added references misc adjusted default fig size to two circles merged examples fixed verbosity fixed verbosity #2 misc fixed invalid ascii char deprecation warning in numpy (scot upstream fix) corrected newline at end of file fixed utf-8 character --- .../plot_mne_inverse_label_connectivity.py | 164 ++++ mne_sandbox/connectivity/__init__.py | 4 + mne_sandbox/connectivity/mvar.py | 267 ++++++ mne_sandbox/connectivity/tests/test_mvar.py | 132 +++ mne_sandbox/externals/scot/__init__.py | 24 + mne_sandbox/externals/scot/backend_builtin.py | 47 + mne_sandbox/externals/scot/backend_sklearn.py | 97 +++ mne_sandbox/externals/scot/binica.py | 167 ++++ mne_sandbox/externals/scot/config.py | 8 + mne_sandbox/externals/scot/connectivity.py | 369 ++++++++ .../externals/scot/connectivity_statistics.py | 299 +++++++ mne_sandbox/externals/scot/csp.py | 73 ++ mne_sandbox/externals/scot/datatools.py | 153 ++++ mne_sandbox/externals/scot/matfiles.py | 49 ++ mne_sandbox/externals/scot/ooapi.py | 802 ++++++++++++++++++ mne_sandbox/externals/scot/parallel.py | 33 + mne_sandbox/externals/scot/pca.py | 130 +++ mne_sandbox/externals/scot/plainica.py | 77 ++ mne_sandbox/externals/scot/plotting.py | 677 +++++++++++++++ mne_sandbox/externals/scot/utils.py | 203 +++++ mne_sandbox/externals/scot/var.py | 316 +++++++ mne_sandbox/externals/scot/varbase.py | 472 +++++++++++ mne_sandbox/externals/scot/varica.py | 258 ++++++ mne_sandbox/externals/scot/xvschema.py | 115 +++ mne_sandbox/viz/__init__.py | 6 + mne_sandbox/viz/connectivity.py | 583 +++++++++++++ setup.py | 5 +- 27 files changed, 5529 insertions(+), 1 deletion(-) create mode 100644 examples/connectivity/plot_mne_inverse_label_connectivity.py create mode 100644 mne_sandbox/connectivity/__init__.py create mode 100644 mne_sandbox/connectivity/mvar.py create mode 100644 mne_sandbox/connectivity/tests/test_mvar.py create mode 100644 mne_sandbox/externals/scot/__init__.py create mode 100644 mne_sandbox/externals/scot/backend_builtin.py create mode 100644 mne_sandbox/externals/scot/backend_sklearn.py create mode 100644 mne_sandbox/externals/scot/binica.py create mode 100644 mne_sandbox/externals/scot/config.py create mode 100644 mne_sandbox/externals/scot/connectivity.py create mode 100644 mne_sandbox/externals/scot/connectivity_statistics.py create mode 100644 mne_sandbox/externals/scot/csp.py create mode 100644 mne_sandbox/externals/scot/datatools.py create mode 100644 mne_sandbox/externals/scot/matfiles.py create mode 100644 mne_sandbox/externals/scot/ooapi.py create mode 100644 mne_sandbox/externals/scot/parallel.py create mode 100644 mne_sandbox/externals/scot/pca.py create mode 100644 mne_sandbox/externals/scot/plainica.py create mode 100644 mne_sandbox/externals/scot/plotting.py create mode 100644 mne_sandbox/externals/scot/utils.py create mode 100644 mne_sandbox/externals/scot/var.py create mode 100644 mne_sandbox/externals/scot/varbase.py create mode 100644 mne_sandbox/externals/scot/varica.py create mode 100644 mne_sandbox/externals/scot/xvschema.py create mode 100644 mne_sandbox/viz/__init__.py create mode 100644 mne_sandbox/viz/connectivity.py diff --git a/examples/connectivity/plot_mne_inverse_label_connectivity.py b/examples/connectivity/plot_mne_inverse_label_connectivity.py new file mode 100644 index 0000000..84af292 --- /dev/null +++ b/examples/connectivity/plot_mne_inverse_label_connectivity.py @@ -0,0 +1,164 @@ +""" +========================================================================= +Compute source space connectivity and visualize it using a circular graph +========================================================================= + +This example computes connectivity between 68 regions in source space based on +dSPM inverse solutions and a FreeSurfer cortical parcellation. All-to-all +functional and effective connectivity measures are obtained from two different +methods: non-parametric spectral estimates and multivariate autoregressive +(MVAR) models. The connectivity is visualized using a circular graph which is +ordered based on the locations of the regions. + +MVAR connectivity is computed with the Source Connectivity Toolbox (SCoT), see +http://scot-dev.github.io/scot-doc/index.html for details. +""" +# Authors: Martin Luessi +# Alexandre Gramfort +# Martin Billinger +# Nicolas P. Rougier (graph code borrowed from his matplotlib gallery) +# +# License: BSD (3-clause) + +import numpy as np +import matplotlib.pyplot as plt + +import mne +from mne.datasets import sample +from mne.minimum_norm import apply_inverse_epochs, read_inverse_operator +from mne.connectivity import spectral_connectivity +from mne_sandbox.connectivity import mvar_connectivity +from mne.viz import circular_layout +from mne_sandbox.viz import (plot_connectivity_circle, + plot_connectivity_inoutcircles) +from mne_sandbox.externals.scot.connectivity_statistics import significance_fdr + +print(__doc__) + +data_path = sample.data_path() +subjects_dir = data_path + '/subjects' +fname_inv = data_path + '/MEG/sample/sample_audvis-meg-oct-6-meg-inv.fif' +fname_raw = data_path + '/MEG/sample/sample_audvis_filt-0-40_raw.fif' +fname_event = data_path + '/MEG/sample/sample_audvis_filt-0-40_raw-eve.fif' + +# Load data +inverse_operator = read_inverse_operator(fname_inv) +raw = mne.io.read_raw_fif(fname_raw) +events = mne.read_events(fname_event) + +# Add a bad channel +raw.info['bads'] += ['MEG 2443'] + +# Pick MEG channels +picks = mne.pick_types(raw.info, meg=True, eeg=False, stim=False, eog=True, + exclude='bads') + +# Define epochs for left-auditory condition +event_id, tmin, tmax = 1, -0.2, 0.5 +epochs = mne.Epochs(raw, events, event_id, tmin, tmax, picks=picks, + baseline=(None, 0), reject=dict(mag=4e-12, grad=4000e-13, + eog=150e-6)) + +# Compute inverse solution and for each epoch. By using "return_generator=True" +# stcs will be a generator object instead of a list. +snr = 1.0 # use lower SNR for single epochs +lambda2 = 1.0 / snr ** 2 +method = "dSPM" # use dSPM method (could also be MNE or sLORETA) +stcs = apply_inverse_epochs(epochs, inverse_operator, lambda2, method, + pick_ori="normal", return_generator=True) + +# Get labels for FreeSurfer 'aparc' cortical parcellation with 34 labels/hemi +labels = mne.read_labels_from_annot('sample', parc='aparc', + subjects_dir=subjects_dir) +label_colors = [label.color for label in labels] + +# Average the source estimates within each label using sign-flips to reduce +# signal cancellations. We do not return a generator, because we want to use +# the estimates repeatedly. +src = inverse_operator['src'] +label_ts = mne.extract_label_time_course(stcs, labels, src, mode='mean_flip', + return_generator=False) + +# First, compute connectivity from spectral estimates in the alpha band. +fmin = 8. +fmax = 13. +sfreq = raw.info['sfreq'] # the sampling frequency +con_methods = ['wpli2_debiased', 'coh'] +con, freqs, times, n_epochs, n_tapers = spectral_connectivity( + label_ts, method=con_methods, mode='multitaper', sfreq=sfreq, fmin=fmin, + fmax=fmax, faverage=True, mt_adaptive=True, n_jobs=1) + +# con is a 3D array, get the connectivity for the first (and only) freq. band +# for each method +con_spec = dict() +for method, c in zip(con_methods, con): + con_spec[method] = c[:, :, 0] + +# Second, compute connectivity from multivariate autoregressive models. +mvar_methods = ['PDC', 'COH'] +con, freqs, order, p_vals = mvar_connectivity(label_ts, mvar_methods, + sfreq=sfreq, fmin=fmin, + fmax=fmax, ridge=10, + n_surrogates=100, n_jobs=1) + +# Get connectivity for the first frequency band. Set connectivity to 0 if not +# significant, while compensating for multiple testing by controlling the false +# discovery rate. +con_mvar = dict() +for method, c, p in zip(mvar_methods, con, p_vals): + con_mvar[method] = c[:, :, 0] * significance_fdr(p[:, :, 0], 0.01) + +# Now, we visualize the connectivity using a circular graph layout + +# First, we reorder the labels based on their location in the left hemi +label_names = [label.name for label in labels] + +lh_labels = [name for name in label_names if name.endswith('lh')] + +# Get the y-location of the label +label_ypos = list() +for name in lh_labels: + idx = label_names.index(name) + ypos = np.mean(labels[idx].pos[:, 1]) + label_ypos.append(ypos) + +# Reorder the labels based on their location +lh_labels = [label for (yp, label) in sorted(zip(label_ypos, lh_labels))] + +# For the right hemi +rh_labels = [label[:-2] + 'rh' for label in lh_labels] + +# Save the plot order and create a circular layout +node_order = list() +node_order.extend(lh_labels[::-1]) # reverse the order +node_order.extend(rh_labels) + +node_angles = circular_layout(label_names, node_order, start_pos=90, + group_boundaries=[0, len(label_names) / 2]) + +# Plot the graph using node colors from the FreeSurfer parcellation. We only +# show the 300 strongest connections. +plot_connectivity_circle(con_spec['wpli2_debiased'], label_names, n_lines=300, + node_angles=node_angles, node_colors=label_colors, + title='All-to-All Connectivity left-Auditory ' + 'Condition (WPLI^2, debiased)', show=False) +plt.savefig('circle.png', facecolor='black') + +# Compare coherence from both estimation methods +fig = plt.figure(num=None, figsize=(8, 4), facecolor='black') +for ii, (con, method) in enumerate(zip([con_spec['coh'], con_mvar['COH']], + ['Spectral', 'MVAR'])): + plot_connectivity_circle(con, label_names, n_lines=300, + node_angles=node_angles, node_colors=label_colors, + title=method, padding=0, fontsize_colorbar=6, + fig=fig, subplot=(1, 2, ii + 1), plot_names=False, + show=False) +plt.suptitle('All-to-all coherence', color='white', fontsize=14) + +# Show effective (directed) connectivity for one node +plot_connectivity_inoutcircles(con_mvar['PDC'], 'superiortemporal-lh', + label_names, node_angles=node_angles, padding=0, + node_colors=label_colors, plot_names=False, + title='Effective connectivity (PDC)', show=False) + +plt.show() diff --git a/mne_sandbox/connectivity/__init__.py b/mne_sandbox/connectivity/__init__.py new file mode 100644 index 0000000..0290e17 --- /dev/null +++ b/mne_sandbox/connectivity/__init__.py @@ -0,0 +1,4 @@ +""" Connectivity Analysis Tools +""" + +from .mvar import mvar_connectivity diff --git a/mne_sandbox/connectivity/mvar.py b/mne_sandbox/connectivity/mvar.py new file mode 100644 index 0000000..f888798 --- /dev/null +++ b/mne_sandbox/connectivity/mvar.py @@ -0,0 +1,267 @@ +# Authors: Martin Billinger +# +# License: BSD (3-clause) + +from __future__ import division +import numpy as np +import logging + +from mne.parallel import parallel_func +from mne.utils import logger, verbose +from ..externals.scot.varbase import VARBase +from ..externals.scot.var import VAR +from ..externals.scot.connectivity import connectivity +from ..externals.scot.connectivity_statistics import surrogate_connectivity +from ..externals.scot.xvschema import make_nfold + + +def _acm(x, l): + """Calculates autocorrelation matrix of x at lag l. + """ + if l == 0: + a, b = x, x + else: + a = x[:, l:] + b = x[:, 0:-l] + + return np.dot(a[:, :], b[:, :].T) / a.shape[1] + + +def _epoch_autocorrelations(epoch, max_lag): + return [_acm(epoch, l) for l in range(max_lag + 1)] + + +def _get_n_epochs(epochs, n): + """Generator that returns lists with at most n epochs""" + epochs_out = [] + for e in epochs: + epochs_out.append(e) + if len(epochs_out) >= n: + yield epochs_out + epochs_out = [] + yield epochs_out + + +def _fit_mvar_lsq(data, pmin, pmax, delta, n_jobs, verbose): + var = VAR(pmin, delta, xvschema=make_nfold(10)) + if pmin != pmax: + logger.info('MVAR order selection...') + var.optimize_order(data, pmin, pmax, n_jobs=n_jobs, verbose=verbose) + #todo: only convert if data is a generator + data = np.asarray(list(data)).transpose([2, 1, 0]) + var.fit(data) + return var + + +def _fit_mvar_yw(data, pmin, pmax, n_jobs=1, verbose=None): + if pmin != pmax: + raise NotImplementedError('Yule-Walker fitting does not support ' + 'automatic model order selection.') + order = pmin + + parallel, my_epoch_autocorrelations, _ = \ + parallel_func(_epoch_autocorrelations, n_jobs, + verbose=verbose) + n_epochs = 0 + logger.info('Accumulating autocovariance matrices...') + for epoch_block in _get_n_epochs(data, n_jobs): + out = parallel(my_epoch_autocorrelations(epoch, order) + for epoch in epoch_block) + if n_epochs == 0: + acm_estimates = np.sum(out, 0) + else: + acm_estimates += np.sum(out, 0) + n_epochs += len(epoch_block) + acm_estimates /= n_epochs + + var = VARBase(order) + var.from_yw(acm_estimates) + + return var + + +@verbose +def mvar_connectivity(data, method, order=(1, None), fitting_mode='lsq', + ridge=0, sfreq=2 * np.pi, fmin=0, fmax=np.inf, n_fft=64, + n_surrogates=None, buffer_size=8, n_jobs=1, + verbose=None): + """Estimate connectivity from multivariate autoregressive (MVAR) models. + + This function uses routines from SCoT [1] to fit MVAR models and compute + connectivity measures. + + Parameters + ---------- + data : array, shape=(n_epochs, n_signals, n_times) + or list/generator of array, shape =(n_signals, n_times) + The data from which to compute connectivity. + method : string | list of string + Connectivity measure(s) to compute. Supported measures: + 'COH' : coherence [2] + 'pCOH' : partial coherence [3] + 'PDC' : partial directed coherence [4] + 'PDCF' : partial directed coherence factor [4] + 'GPDC' : generalized partial directed coherence [5] + 'DTF' : directed transfer function [6] + 'ffDTF' : full-frequency directed transfer function [7] + 'dDTF' : "direct" directed transfer function [7] + 'GDTF' : generalized directed transfer function [5] + order : int | (int, int) + Order (length) of the underlying MVAR model. If order is a tuple + (p0, p1) of two ints, the function selects the best model order between + p0 and p1. p1 can be None, which causes the order selection to stop at + the lowest candidate. + fitting_mode : str + Determines how to fit the MVAR model. + 'lsq' : Least-Squares fitting + 'yw' : Solve Yule-Walker equations + Yule-Walker equations can utilize data generators, which makes them + more memory efficient than least-squares. However, yw-estimation may + fail if `order` or `n_signals` is too high for the amount of data + available. + ridge : float + Ridge-regression coefficient (l2 penalty) for least-squares fitting. + This parameter is ignored for Yule-Walker fitting. + sfreq : float + The sampling frequency. + fmin : float | tuple of floats + The lower frequency of interest. Multiple bands are defined using + a tuple, e.g., (8., 16.) for two bands with 8Hz and 16Hz lower freq. + fmax : float | tuple of floats + The upper frequency of interest. Multiple bands are defined using + a tuple, e.g. (12., 24.) for two band with 12Hz and 24Hz upper freq. + n_fft : int + Number of FFT bins to calculate. + n_surrogates : int | None + If set to None, no statistics are calculated. Otherwise, `surrogates` + is the number of surrogate datasets on which the chance level is + calculated. In this case the *p*-values are returned, which are related + to the probability that the observed connectivity is not caused by + chance. See scot.connectivity_statistics.surrogate_connectivity for + details on the procedure. + **Warning**: Correction for multiple testing is required if the + *p*-values are used as basis for significance testing. + buffer_size : int + Surrogates are calculated in `n_surrogates // buffer_size` blocks. + Lower buffer_size takes less memory but has more computational + overhead than higher buffer_size. + n_jobs : int + Number of jobs to run in parallel. This is used for model order + selection and statistics calculations. + verbose : bool, str, int, or None + If not None, override default verbose level (see mne.verbose). + + Returns + ------- + con : array | list of arrays + Computed connectivity measure(s). The shape of each array is + (n_signals, n_signals, n_frequencies) + freqs : array + Frequency points at which the connectivity was computed. + var_order : int + MVAR model order that was used for fitting the model. + p_values : array | list of arrays | None + *p*-values of connectivity measure(s). The shape of each array is + (n_signals, n_signals, n_frequencies). `p_values` is returned as None + if no statistics are calculated (i.e. `n_surrogates` evaluates to + False). + + References + ---------- + [1] M. Billinger, C.Brunner, G. R. Mueller-Putz. "SCoT: a Python toolbox + for EEG source connectivity", Frontiers in Neuroinformatics, 8:22, 2014 + + [2] P. L. Nunez, R. Srinivasan, A. F. Westdorp, R. S. Wijesinghe, + D. M. Tucker, R. B. Silverstein, P. J. Cadusch. EEG coherency: I: + statistics, reference electrode, volume conduction, Laplacians, + cortical imaging, and interpretation at multiple scales. Electroenceph. + Clin. Neurophysiol. 103(5): 499-515, 1997. + + [3] P. J. Franaszczuk, K. J. Blinowska, M. Kowalczyk. The application of + parametric multichannel spectral estimates in the study of electrical + brain activity. Biol. Cybernetics 51(4): 239-247, 1985. + + [4] L. A. Baccala, K. Sameshima. Partial directed coherence: a new concept + in neural structure determination. Biol. Cybernetics 84(6):463-474, + 2001. + + [5] L. Faes, S. Erla, G. Nollo. Measuring Connectivity in Linear + Multivariate Processes: Definitions, Interpretation, and Practical + Analysis. Comput. Math. Meth. Med. 2012:140513, 2012. + + [6] M. J. Kaminski, K. J. Blinowska. A new method of the description of the + information flow in the brain structures. Biol. Cybernetics 65(3): + 203-210, 1991. + + [7] A. Korzeniewska, M. Manczak, M. Kaminski, K. J. Blinowska, S. Kasicki. + Determination of information flow direction among brain structures by a + modified directed transfer function (dDTF) method. J. Neurosci. Meth. + 125(1-2): 195-207, 2003. + """ + scot_verbosity = 5 if logger.level <= logging.INFO else 0 + + if not isinstance(method, (list, tuple)): + method = [method] + + fmin = np.asarray((fmin,)).ravel() + fmax = np.asarray((fmax,)).ravel() + if len(fmin) != len(fmax): + raise ValueError('fmin and fmax must have the same length') + if np.any(fmin > fmax): + raise ValueError('fmax must be larger than fmin') + + try: + pmin, pmax = order[0], order[1] + except TypeError: + pmin, pmax = order, order + + logger.info('MVAR fitting...') + if fitting_mode == 'yw': + var = _fit_mvar_yw(data, pmin, pmax) + elif fitting_mode == 'lsq': + var = _fit_mvar_lsq(data, pmin, pmax, ridge, n_jobs=n_jobs, + verbose=scot_verbosity) + else: + raise ValueError('Unknown fitting mode: %s' % fitting_mode) + + freqs, fmask = [], [] + freq_range = np.linspace(0, sfreq / 2, n_fft) + for fl, fh in zip(fmin, fmax): + fmask.append(np.logical_and(fl <= freq_range, freq_range <= fh)) + freqs.append(freq_range[fmask[-1]]) + + logger.info('Connectivity computation...') + results = [] + con = connectivity(method, var.coef, var.rescov, n_fft) + for mth in method: + bands = [np.mean(np.abs(con[mth][:, :, fm]), axis=2) for fm in fmask] + results.append(np.transpose(bands, (1, 2, 0))) + + if n_surrogates is not None and n_surrogates > 0: + logger.info('Computing connectivity statistics...') + data = np.asarray(list(data)).transpose([2, 1, 0]) + + n_blocks = n_surrogates // buffer_size + + p_vals = [] + # do them in junks, in order to save memory + for i in range(n_blocks): + scon = surrogate_connectivity(method, data, var, nfft=n_fft, + repeats=buffer_size, n_jobs=n_jobs, + verbose=scot_verbosity) + + for m, mth in enumerate(method): + c, sc = np.abs(con[mth]), np.abs(scon[mth]) + bands = [np.mean(c[:, :, fm], axis=-1) for fm in fmask] + sbands = [np.mean(sc[:, :, :, fm], axis=-1) for fm in fmask] + + p = [np.sum(bs >= b, axis=0) for b, bs in zip(bands, sbands)] + p = np.array(p).transpose(1, 2, 0) / (n_blocks * buffer_size) + if i == 0: + p_vals.append(p) + else: + p_vals[m] += p + else: + p_vals = None + + return results, freqs, var.p, p_vals diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py new file mode 100644 index 0000000..f00a217 --- /dev/null +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -0,0 +1,132 @@ +import numpy as np +from numpy.testing import assert_array_almost_equal +from nose.tools import assert_raises, assert_equal + +from mne import SourceEstimate + +from mne_sandbox.connectivity import mvar_connectivity + + +def _stc_gen(data, sfreq, tmin, combo=False): + """Simulate a SourceEstimate generator""" + vertices = [np.arange(data.shape[1]), np.empty(0)] + for d in data: + if not combo: + stc = SourceEstimate(data=d, vertices=vertices, + tmin=tmin, tstep=1 / float(sfreq)) + yield stc + else: + # simulate a combination of array and source estimate + arr = d[0] + stc = SourceEstimate(data=d[1:], vertices=vertices, + tmin=tmin, tstep=1 / float(sfreq)) + yield (arr, stc) + + +def _make_data(var_coef, n_samples, n_epochs): + var_order = var_coef.shape[0] + n_signals = var_coef.shape[1] + + x = np.random.randn(n_signals, n_epochs * n_samples + 10 * var_order) + for i in range(var_order, x.shape[1]): + for k in range(var_order): + x[:, [i]] += np.dot(var_coef[k], x[:, [i-k-1]]) + + x = x[:, -n_epochs * n_samples:] + + win = np.arange(0, n_samples) + return [x[:, i + win] for i in range(0, n_epochs * n_samples, n_samples)] + + +def test_mvar_connectivity(): + """Test MVAR connectivity estimation""" + # Use a case known to have no spurious correlations (it would bad if + # nosetests could randomly fail): + np.random.seed(0) + + n_sigs = 3 + n_epochs = 100 + n_samples = 500 + + # test invalid fmin fmax settings + assert_raises(ValueError, mvar_connectivity, [], 'S', 5, fmin=10, fmax=5) + assert_raises(ValueError, mvar_connectivity, [], 'DTF', 1, fmin=(0, 11), + fmax=(5, 10)) + assert_raises(ValueError, mvar_connectivity, [], 'PDC', 99, fmin=(11,), + fmax=(12, 15)) + assert_raises(ValueError, mvar_connectivity, [], 'S', fitting_mode='') + assert_raises(NotImplementedError, mvar_connectivity, [], 'H', fitting_mode='yw') + + methods = ['S', 'COH', 'DTF', 'PDC', 'ffDTF', 'GPDC', 'GDTF', 'A'] + + # generate data without connectivity + var_coef = np.zeros((1, n_sigs, n_sigs)) + data = _make_data(var_coef, n_samples, n_epochs) + + con, freqs, p, p_vals = mvar_connectivity(data, methods, order=1, fitting_mode='yw') + con = dict((m, c) for m, c in zip(methods, con)) + assert_equal(p, 1) + + assert_array_almost_equal(con['S'][:, :, 0], np.eye(n_sigs), decimal=2) + assert_array_almost_equal(con['COH'][:, :, 0], np.eye(n_sigs), decimal=2) + assert_array_almost_equal(con['COH'][:, :, 0].diagonal(), np.ones(n_sigs)) + assert_array_almost_equal(con['DTF'][:, :, 0], np.eye(n_sigs), decimal=2) + assert_array_almost_equal(con['PDC'][:, :, 0], np.eye(n_sigs), decimal=2) + assert_array_almost_equal(con['ffDTF'][:, :, 0] / np.sqrt(len(freqs[0])), + np.eye(n_sigs), decimal=2) + assert_array_almost_equal(con['GPDC'][:, :, 0], np.eye(n_sigs), decimal=2) + assert_array_almost_equal(con['GDTF'][:, :, 0], np.eye(n_sigs), decimal=2) + + # generate data with strong directed connectivity + f = 1e3 + var_coef = np.zeros((1, n_sigs, n_sigs)) + var_coef[:, 1, 0] = f + data = _make_data(var_coef, n_samples, n_epochs) + + con, freqs, p, p_vals = mvar_connectivity(data, methods, order=(2, 5)) + con = dict((m, c) for m, c in zip(methods, con)) + + h = var_coef.squeeze() + np.eye(n_sigs) + + assert_array_almost_equal(con['S'][:, :, 0] / f**2, np.dot(h, h.T) / f**2, + decimal=2) + assert_array_almost_equal(con['COH'][:, :, 0], np.dot(h, h.T) > 0, + decimal=2) + assert_array_almost_equal(con['DTF'][:, :, 0], + h / np.sum(h, 1, keepdims=True), decimal=2) + assert_array_almost_equal(con['ffDTF'][:, :, 0] / np.sqrt(len(freqs[0])), + h / np.sum(h, 1, keepdims=True), decimal=2) + assert_array_almost_equal(con['GDTF'][:, :, 0], + h / np.sum(h, 1, keepdims=True), decimal=2) + assert_array_almost_equal(con['PDC'][:, :, 0], + h / np.sum(h, 0, keepdims=True), decimal=2) + assert_array_almost_equal(con['GPDC'][:, :, 0], + h / np.sum(h, 0, keepdims=True), decimal=2) + + # generate data with strong cascaded directed connectivity + f = 1e3 + var_coef = np.zeros((1, n_sigs, n_sigs)) + var_coef[:, 1, 0] = f + var_coef[:, 2, 1] = f + data = _make_data(var_coef, n_samples, n_epochs) + + con, freqs, p, p_vals = mvar_connectivity(data, methods, order=(1, None)) + con = dict((m, c) for m, c in zip(methods, con)) + + assert_array_almost_equal(con['S'][:, :, 0] / f**4, [[f**-4, f**-3, f**-2], + [f**-3, f**-2, f**-1], + [f**-2, f**-1, f**0]], + decimal=2) + assert_array_almost_equal(con['COH'][:, :, 0], np.ones((n_sigs, n_sigs)), + decimal=2) + assert_array_almost_equal(con['DTF'][:, :, 0], [[1, 0, 0], + [1, 0, 0], + [1, 0, 0]], decimal=2) + assert_array_almost_equal(con['ffDTF'][:, :, 0] / np.sqrt(len(freqs[0])), + [[1, 0, 0], [1, 0, 0], [1, 0, 0]], decimal=2) + assert_array_almost_equal(con['GDTF'], con['DTF'], decimal=2) + + h = var_coef.squeeze() + np.eye(n_sigs) + assert_array_almost_equal(con['PDC'][:, :, 0], + h / np.sum(h, 0, keepdims=True), decimal=2) + assert_array_almost_equal(con['GPDC'], con['PDC'], decimal=2) diff --git a/mne_sandbox/externals/scot/__init__.py b/mne_sandbox/externals/scot/__init__.py new file mode 100644 index 0000000..aeddce1 --- /dev/null +++ b/mne_sandbox/externals/scot/__init__.py @@ -0,0 +1,24 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" SCoT: The Source Connectivity Toolbox +""" + +from __future__ import absolute_import + +from . import config + +backends = ['backend_builtin', 'backend_sklearn'] + +# default backend +# TODO: set default backend in config +from . import backend_builtin + +from .ooapi import Workspace + +from .connectivity import Connectivity + +from . import datatools + +__all__ = ['Workspace', 'Connectivity', 'datatools'] diff --git a/mne_sandbox/externals/scot/backend_builtin.py b/mne_sandbox/externals/scot/backend_builtin.py new file mode 100644 index 0000000..bd3052d --- /dev/null +++ b/mne_sandbox/externals/scot/backend_builtin.py @@ -0,0 +1,47 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Use internally implemented functions as backend. +""" + +import numpy as np + +from . import config, datatools, binica, pca, csp +from .var import VAR + + +def wrapper_binica(data): + """ Call binica for ICA calculation. + """ + w, s = binica.binica(datatools.cat_trials(data)) + u = s.dot(w) + m = np.linalg.inv(u) + return m, u + +def wrapper_pca(x, reducedim): + """ Call SCoT's PCA algorithm. + """ + c, d = pca.pca(datatools.cat_trials(x), subtract_mean=False, reducedim=reducedim) + y = datatools.dot_special(x, c) + return c, d, y + +def wrapper_csp(x, cl, reducedim): + c, d = csp.csp(x, cl, numcomp=reducedim) + y = datatools.dot_special(x,c) + return c, d, y + + +backend = { + 'ica': wrapper_binica, + 'pca': wrapper_pca, + 'csp': wrapper_csp, + 'var': VAR +} + + +def activate(): + config.backend = backend + + +activate() diff --git a/mne_sandbox/externals/scot/backend_sklearn.py b/mne_sandbox/externals/scot/backend_sklearn.py new file mode 100644 index 0000000..11273cc --- /dev/null +++ b/mne_sandbox/externals/scot/backend_sklearn.py @@ -0,0 +1,97 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Use scikit-learn routines as backend. +""" + +from __future__ import absolute_import + +import scipy as sp +from . import backend_builtin as builtin +from . import config, datatools +from .varbase import VARBase + + +def wrapper_fastica(data): + """ Call FastICA implementation from scikit-learn. + """ + from sklearn.decomposition import FastICA + ica = FastICA() + ica.fit(datatools.cat_trials(data)) + u = ica.components_.T + m = ica.mixing_.T + return m, u + + +def wrapper_pca(x, reducedim): + """ Call PCA implementation from scikit-learn. + """ + from sklearn.decomposition import PCA + pca = PCA(n_components=reducedim) + pca.fit(datatools.cat_trials(x)) + d = pca.components_ + c = pca.components_.T + y = datatools.dot_special(x,c) + return c, d, y + + +class VAR(VARBase): + """ Scikit-learn based implementation of VARBase. + + This class fits VAR models using various implementations of generalized linear model fitting available in scikit-learn. + + Parameters + ---------- + model_order : int + Autoregressive model order + fitobj : class, optional + Instance of a linear model implementation. + """ + def __init__(self, model_order, fitobj=None): + VARBase.__init__(self, model_order) + if fitobj is None: + from sklearn.linear_model import LinearRegression + fitobj = LinearRegression() + self.fitting_model = fitobj + + def fit(self, data): + """ Fit VAR model to data. + + Parameters + ---------- + data : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + Continuous or segmented data set. + + Returns + ------- + self : :class:`VAR` + The :class:`VAR` object. + """ + data = sp.atleast_3d(data) + (x, y) = self._construct_eqns(data) + self.fitting_model.fit(x, y) + + self.coef = self.fitting_model.coef_ + + self.residuals = data - self.predict(data) + self.rescov = sp.cov(datatools.cat_trials(self.residuals[self.p:, :, :]), rowvar=False) + + return self + + +backend = builtin.backend.copy() +backend.update({ + 'ica': wrapper_fastica, + 'pca': wrapper_pca, + 'var': VAR +}) + + +def activate(): + """ Set backend attribute in the config module. + """ + config.backend = backend + + +activate() diff --git a/mne_sandbox/externals/scot/binica.py b/mne_sandbox/externals/scot/binica.py new file mode 100644 index 0000000..2419b52 --- /dev/null +++ b/mne_sandbox/externals/scot/binica.py @@ -0,0 +1,167 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +from __future__ import print_function + +from uuid import uuid4 +import os +import sys +import subprocess + +import numpy as np + +if not hasattr(__builtins__, 'FileNotFoundError'): + # PY27: subprocess.Popen raises OSError instead of FileNotFoundError + FileNotFoundError = OSError + + +binica_binary = os.path.dirname(os.path.abspath(__file__)) + '/binica/ica_linux' + +#noinspection PyNoneFunctionAssignment,PyTypeChecker +def binica(data, binary=binica_binary): + """ Simple wrapper for the BINICA binary. + + This function calculates the ICA transformation using the Infomax algorithm implemented in BINICA. + + BINICA is bundled with EEGLAB, or can be downloaded from here: + http://sccn.ucsd.edu/eeglab/binica/ + + This function attempts to automatically download and extract the BINICA binary. + + By default the binary is expected to be "binica/ica_linux" relative + to the directory where this module lies (typically scot/binica/ica_linux) + + Parameters + ---------- + data : array-like, shape = [n_samples, n_channels] + EEG data set + binary : str + Full path to the binica binary + + Returns + ------- + w : array, shape = [n_channels, n_channels] + ICA weights matrix + s : array, shape = [n_channels, n_channels] + Sphering matrix + + Notes + ----- + The unmixing matrix is obtained by multiplying U = dot(s, w) + """ + + check_binary_(binary) + + data = np.array(data, dtype=np.float32) + + nframes, nchans = data.shape + + uid = uuid4() + + scriptfile = 'binica-%s.sc' % uid + datafile = 'binica-%s.fdt' % uid + weightsfile = 'binica-%s.wts' % uid + #weightstmpfile = 'binicatmp-%s.wts' % uid + spherefile = 'binica-%s.sph' % uid + + config = {'DataFile': datafile, + 'WeightsOutFile': weightsfile, + 'SphereFile': spherefile, + 'chans': nchans, + 'frames': nframes, + 'extended': 1} + # config['WeightsTempFile'] = weightstmpfile + + # create data file + f = open(datafile, 'wb') + data.tofile(f) + f.close() + + # create script file + f = open(scriptfile, 'wt') + for h in config: + print(h, config[h], file=f) + f.close() + + # flush output streams otherwise things printed before might appear after the ICA output. + sys.stdout.flush() + sys.stderr.flush() + + if os.path.exists(binary): + with open(scriptfile) as sc: + try: + proc = subprocess.Popen(binary, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=sc) + print('waiting for binica to finish...') + proc.wait() + #print('binica output:') + #print(proc.stdout.read().decode()) + proc.stdout.close() + except FileNotFoundError: + raise RuntimeError('The BINICA binary ica_linux exists in the file system but could not be executed. ' + 'This indicates that 32 bit libraries are not installed on the system.') + else: + raise RuntimeError('the binary is not there!?') + + os.remove(scriptfile) + os.remove(datafile) + + # read weights + f = open(weightsfile, 'rb') + weights = np.fromfile(f, dtype=np.float32) + f.close() + weights = np.reshape(weights, (nchans,nchans)) + +# os.remove(weightstmpfile) + os.remove(weightsfile) + + # read sphering matrix + f = open( spherefile, 'rb' ) + sphere = np.fromfile(f, dtype=np.float32) + f.close() + sphere = np.reshape(sphere, (nchans,nchans)) + + os.remove(spherefile) + + return weights, sphere + + +def check_binary_(binary): + """check if binary is available, and try to download it if not""" + + if os.path.exists(binary): + print(binary, 'found') + return + + url = 'http://sccn.ucsd.edu/eeglab/binica/binica.zip' + print(binary+' not found. Trying to download from '+url) + + path = os.path.dirname(binary) + + if not os.path.exists(path): + os.makedirs(path) + + try: + # Python 3 + from urllib.request import urlretrieve as urlretrieve + except ImportError: + # Python 2.7 + from urllib import urlretrieve as urlretrieve + import zipfile + import stat + + urlretrieve(url, path + '/binica.zip') + + if not os.path.exists(path + '/binica.zip'): + raise RuntimeError('Error downloading binica.zip.') + + print('unzipping', path + '/binica.zip') + + with zipfile.ZipFile(path + '/binica.zip') as tgz: + tgz.extractall(path + '/..') + + if not os.path.exists(binary): + raise RuntimeError(binary + ' not found, even after extracting binica.zip.') + + mode = os.stat(binary).st_mode + os.chmod(binary, mode | stat.S_IXUSR) diff --git a/mne_sandbox/externals/scot/config.py b/mne_sandbox/externals/scot/config.py new file mode 100644 index 0000000..8aed27c --- /dev/null +++ b/mne_sandbox/externals/scot/config.py @@ -0,0 +1,8 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Global configuration +""" + +backend = {} diff --git a/mne_sandbox/externals/scot/connectivity.py b/mne_sandbox/externals/scot/connectivity.py new file mode 100644 index 0000000..0f40b03 --- /dev/null +++ b/mne_sandbox/externals/scot/connectivity.py @@ -0,0 +1,369 @@ +# coding=utf-8 + +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Connectivity Analysis """ + +import numpy as np +import scipy as sp +from scipy.fftpack import fft +from .utils import memoize + + +def connectivity(measure_names, b, c=None, nfft=512): + """ calculate connectivity measures. + + Parameters + ---------- + measure_names : {str, list of str} + Name(s) of the connectivity measure(s) to calculate. See :class:`Connectivity` for supported measures. + b : ndarray, shape = [n_channels, n_channels*model_order] + VAR model coefficients. See :ref:`var-model-coefficients` for details about the arrangement of coefficients. + c : ndarray, shape = [n_channels, n_channels], optional + Covariance matrix of the driving noise process. Identity matrix is used if set to None. + nfft : int, optional + Number of frequency bins to calculate. Note that these points cover the range between 0 and half the + sampling rate. + + Returns + ------- + result : ndarray, shape = [n_channels, n_channels, `nfft`] + An ndarray of shape [m, m, nfft] is returned if measures is a string. If measures is a list of strings a + dictionary is returned, where each key is the name of the measure, and the corresponding values are ndarrays + of shape [m, m, nfft]. + + Notes + ----- + When using this function it is more efficient to get several measures at once than calling the function multiple times. + + Examples + -------- + >>> c = connectivity(['DTF', 'PDC'], [[0.3, 0.6], [0.0, 0.9]]) + """ + con = Connectivity(b, c, nfft) + try: + return getattr(con, measure_names)() + except TypeError: + return dict((m, getattr(con, m)()) for m in measure_names) + + +#noinspection PyPep8Naming +class Connectivity: + """ Calculation of connectivity measures + + This class calculates various spectral connectivity measures from a vector autoregressive (VAR) model. + + Parameters + ---------- + b : ndarray, shape = [n_channels, n_channels*model_order] + VAR model coefficients. See :ref:`var-model-coefficients` for details about the arrangement of coefficients. + c : ndarray, shape = [n_channels, n_channels], optional + Covariance matrix of the driving noise process. Identity matrix is used if set to None. + nfft : int, optional + Number of frequency bins to calculate. Note that these points cover the range between 0 and half the + sampling rate. + + Methods + ------- + :func:`A` + Spectral representation of the VAR coefficients + :func:`H` + Transfer function that turns the innovation process into the VAR process + :func:`S` + Cross spectral density + :func:`logS` + Logarithm of the cross spectral density (S), for convenience. + :func:`G` + Inverse cross spectral density + :func:`logG` + Logarithm of the inverse cross spectral density + :func:`PHI` + Phase angle + :func:`COH` + Coherence + :func:`pCOH` + Partial coherence + :func:`PDC` + Partial directed coherence + :func:`ffPDC` + Full frequency partial directed coherence + :func:`PDCF` + PDC factor + :func:`GPDC` + Generalized partial directed coherence + :func:`DTF` + Directed transfer function + :func:`ffDTF` + Full frequency directed transfer function + :func:`dDTF` + Direct directed transfer function + :func:`GDTF` + Generalized directed transfer function + + Notes + ----- + Connectivity measures are returned by member functions that take no arguments and return a matrix of + shape [m,m,nfft]. The first dimension is the sink, the second dimension is the source, and the third dimension is + the frequency. + + A summary of most supported measures can be found in [1]_. + + References + ---------- + .. [1] M. Billinger et al, “Single-trial connectivity estimation for classification of motor imagery data”, + *J. Neural Eng.* 10, 2013. + """ + + def __init__(self, b, c=None, nfft=512): + b = np.asarray(b) + (m, mp) = b.shape + p = mp // m + if m * p != mp: + raise AttributeError('Second dimension of b must be an integer multiple of the first dimension.') + + if c is None: + self.c = None + else: + self.c = np.atleast_2d(c) + + self.b = np.reshape(b, (m, m, p), 'c') + self.m = m + self.p = p + self.nfft = nfft + + @memoize + def Cinv(self): + """ Inverse of the noise covariance + """ + try: + return np.linalg.inv(self.c) + except np.linalg.linalg.LinAlgError: + print('Warning: non invertible noise covariance matrix c!') + return np.eye(self.c.shape[0]) + + @memoize + def A(self): + """ Spectral VAR coefficients + + .. math:: \mathbf{A}(f) = \mathbf{I} - \sum_{k=1}^{p} \mathbf{a}^{(k)} \mathrm{e}^{-2\pi f} + """ + return fft(np.dstack([np.eye(self.m), -self.b]), self.nfft * 2 - 1)[:, :, :self.nfft] + + @memoize + def H(self): + """ VAR transfer function + + .. math:: \mathbf{H}(f) = \mathbf{A}(f)^{-1} + """ + return _inv3(self.A()) + + @memoize + def S(self): + """ Cross spectral density + + .. math:: \mathbf{S}(f) = \mathbf{H}(f) \mathbf{C} \mathbf{H}'(f) + """ + if self.c is None: + raise RuntimeError('Cross spectral density requires noise covariance matrix c.') + H = self.H() + #TODO can we do that more efficiently? + S = np.empty(H.shape, dtype=H.dtype) + for f in range(H.shape[2]): + S[:, :, f] = H[:, :, f].dot(self.c).dot(H[:, :, f].conj().T) + return S + + @memoize + def logS(self): + """ Logarithmic cross spectral density + + .. math:: \mathrm{logS}(f) = \log | \mathbf{S}(f) | + """ + return np.log10(np.abs(self.S())) + + @memoize + def absS(self): + """ Absolute cross spectral density + + .. math:: \mathrm{absS}(f) = | \mathbf{S}(f) | + """ + return np.abs(self.S()) + + @memoize + def G(self): + """ Inverse cross spectral density + + .. math:: \mathbf{G}(f) = \mathbf{A}(f) \mathbf{C}^{-1} \mathbf{A}'(f) + """ + if self.c is None: + raise RuntimeError('Inverse cross spectral density requires invertible noise covariance matrix c.') + A = self.A() + #TODO can we do that more efficiently? + G = np.einsum('ji..., jk... ->ik...', A.conj(), self.Cinv()) + G = np.einsum('ij..., jk... ->ik...', G, A) + return G + + @memoize + def logG(self): + """ Logarithmic inverse cross spectral density + + .. math:: \mathrm{logG}(f) = \log | \mathbf{G}(f) | + """ + return np.log10(np.abs(self.G())) + + @memoize + def COH(self): + """ Coherence + + .. math:: \mathrm{COH}_{ij}(f) = \\frac{S_{ij}(f)}{\sqrt{S_{ii}(f) S_{jj}(f)}} + + References + ---------- + P. L. Nunez, R. Srinivasan, A. F. Westdorp, R. S. Wijesinghe, D. M. Tucker, + R. B. Silverstein, P. J. Cadusch. EEG coherency: I: statistics, reference electrode, + volume conduction, Laplacians, cortical imaging, and interpretation at multiple scales. + Electroenceph. Clin. Neurophysiol. 103(5): 499-515, 1997. + """ + S = self.S() + #TODO can we do that more efficiently? + return S / np.sqrt(np.einsum('ii..., jj... ->ij...', S, S.conj())) + + @memoize + def PHI(self): + """ Phase angle + + Returns the phase angle of complex :func:`S`. + """ + return np.angle(self.S()) + + @memoize + def pCOH(self): + """ Partial coherence + + .. math:: \mathrm{pCOH}_{ij}(f) = \\frac{G_{ij}(f)}{\sqrt{G_{ii}(f) G_{jj}(f)}} + + References + ---------- + P. J. Franaszczuk, K. J. Blinowska, M. Kowalczyk. The application of parametric multichannel + spectral estimates in the study of electrical brain activity. Biol. Cybernetics 51(4): 239-247, 1985. + """ + G = self.G() + #TODO can we do that more efficiently? + return G / np.sqrt(np.einsum('ii..., jj... ->ij...', G, G)) + + @memoize + def PDC(self): + """ Partial directed coherence + + .. math:: \mathrm{PDC}_{ij}(f) = \\frac{A_{ij}(f)}{\sqrt{A_{:j}'(f) A_{:j}(f)}} + + References + ---------- + L. A. Baccalá, K. Sameshima. Partial directed coherence: a new concept in neural structure + determination. Biol. Cybernetics 84(6):463-474, 2001. + """ + A = self.A() + return np.abs(A / np.sqrt(np.sum(A.conj() * A, axis=0, keepdims=True))) + + @memoize + def ffPDC(self): + """ Full frequency partial directed coherence + + .. math:: \mathrm{ffPDC}_{ij}(f) = \\frac{A_{ij}(f)}{\sqrt{\sum_f A_{:j}'(f) A_{:j}(f)}} + """ + A = self.A() + return np.abs(A * self.nfft / np.sqrt(np.sum(A.conj() * A, axis=(0, 2), keepdims=True))) + + @memoize + def PDCF(self): + """ Partial directed coherence factor + + .. math:: \mathrm{PDCF}_{ij}(f) = \\frac{A_{ij}(f)}{\sqrt{A_{:j}'(f) \mathbf{C}^{-1} A_{:j}(f)}} + + References + ---------- + L. A. Baccalá, K. Sameshima. Partial directed coherence: a new concept in neural structure + determination. Biol. Cybernetics 84(6):463-474, 2001. + """ + A = self.A() + #TODO can we do that more efficiently? + return np.abs(A / np.sqrt(np.einsum('aj..., ab..., bj... ->j...', A.conj(), self.Cinv(), A))) + + @memoize + def GPDC(self): + """ Generalized partial directed coherence + + .. math:: \mathrm{GPDC}_{ij}(f) = \\frac{|A_{ij}(f)|} + {\sigma_i \sqrt{A_{:j}'(f) \mathrm{diag}(\mathbf{C})^{-1} A_{:j}(f)}} + + References + ---------- + L. Faes, S. Erla, G. Nollo. Measuring Connectivity in Linear Multivariate Processes: + Definitions, Interpretation, and Practical Analysis. Comput. Math. Meth. Med. 2012:140513, 2012. + """ + A = self.A() + return np.abs(A / np.sqrt(np.einsum('aj..., a..., aj..., ii... ->ij...', A.conj(), 1/np.diag(self.c), A, self.c))) + + @memoize + def DTF(self): + """ Directed transfer function + + .. math:: \mathrm{DTF}_{ij}(f) = \\frac{H_{ij}(f)}{\sqrt{H_{i:}(f) H_{i:}'(f)}} + + References + ---------- + M. J. Kaminski, K. J. Blinowska. A new method of the description of the information flow + in the brain structures. Biol. Cybernetics 65(3): 203-210, 1991. + """ + H = self.H() + return np.abs(H / np.sqrt(np.sum(H * H.conj(), axis=1, keepdims=True))) + + @memoize + def ffDTF(self): + """ Full frequency directed transfer function + + .. math:: \mathrm{ffDTF}_{ij}(f) = \\frac{H_{ij}(f)}{\sqrt{\sum_f H_{i:}(f) H_{i:}'(f)}} + + References + ---------- + A. Korzeniewska, M. Mańczak, M. Kaminski, K. J. Blinowska, S. Kasicki. Determination of + information flow direction among brain structures by a modified directed transfer + function (dDTF) method. J. Neurosci. Meth. 125(1-2): 195-207, 2003. + """ + H = self.H() + return np.abs(H * self.nfft / np.sqrt(np.sum(H * H.conj(), axis=(1, 2), keepdims=True))) + + @memoize + def dDTF(self): + """" Direct" directed transfer function + + .. math:: \mathrm{dDTF}_{ij}(f) = |\mathrm{pCOH}_{ij}(f)| \mathrm{ffDTF}_{ij}(f) + + References + ---------- + A. Korzeniewska, M. Mańczak, M. Kaminski, K. J. Blinowska, S. Kasicki. Determination of + information flow direction among brain structures by a modified directed transfer + function (dDTF) method. J. Neurosci. Meth. 125(1-2): 195-207, 2003. + """ + return np.abs(self.pCOH()) * self.ffDTF() + + @memoize + def GDTF(self): + """ Generalized directed transfer function + + .. math:: \mathrm{GPDC}_{ij}(f) = \\frac{\sigma_j |H_{ij}(f)|} + {\sqrt{H_{i:}(f) \mathrm{diag}(\mathbf{C}) H_{i:}'(f)}} + + References + ---------- + L. Faes, S. Erla, G. Nollo. Measuring Connectivity in Linear Multivariate Processes: + Definitions, Interpretation, and Practical Analysis. Comput. Math. Meth. Med. 2012:140513, 2012. + """ + H = self.H() + return np.abs(H / np.sqrt(np.einsum('ia..., aa..., ia..., j... ->ij...', H.conj(), self.c, H, 1/self.c.diagonal()))) + + +def _inv3(x): + identity = np.eye(x.shape[0]) + return np.array([sp.linalg.solve(a, identity) for a in x.T]).T diff --git a/mne_sandbox/externals/scot/connectivity_statistics.py b/mne_sandbox/externals/scot/connectivity_statistics.py new file mode 100644 index 0000000..53a34ae --- /dev/null +++ b/mne_sandbox/externals/scot/connectivity_statistics.py @@ -0,0 +1,299 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013-2014 SCoT Development Team + +""" Routines for statistical evaluation of connectivity. +""" + +from __future__ import division + +import numpy as np +import scipy as sp +from .datatools import randomize_phase +from .connectivity import connectivity +from .utils import cartesian +from .parallel import parallel_loop + + +def surrogate_connectivity(measure_names, data, var, nfft=512, repeats=100, + n_jobs=1, verbose=0): + """ Calculates surrogate connectivity for a multivariate time series by + phase randomization [1]_. + + .. note:: Parameter `var` will be modified by the function. Treat as + undefined after the function returned. + + Parameters + ---------- + measure_names : {str, list of str} + Name(s) of the connectivity measure(s) to calculate. See + :class:`Connectivity` for supported measures. + data : ndarray, shape = [n_samples, n_channels, (n_trials)] + Time series data (2D or 3D for multiple trials) + var : VARBase-like object + Instance of a VAR model. + nfft : int, optional + Number of frequency bins to calculate. Note that these points cover the + range between 0 and half the + sampling rate. + repeats : int, optional + How many surrogate samples to take. + n_jobs : int | None + number of jobs to run in parallel. See `joblib.Parallel` for details. + verbose : int + verbosity level passed to joblib. + + Returns + ------- + result : array, shape = [`repeats`, n_channels, n_channels, nfft] + Values of the connectivity measure for each surrogate. If + `measure_names` is a list of strings a dictionary + is returned, where each key is the name of the measure, and the + corresponding values are ndarrays of shape + [`repeats`, n_channels, n_channels, nfft]. + + .. [1] J. Theiler et al. "Testing for nonlinearity in time series: the + method of surrogate data", Physica D, vol 58, pp. 77-94, 1992 + """ + par, func = parallel_loop(_calc_surrogate, n_jobs=n_jobs, verbose=verbose) + output = par(func(randomize_phase(data), var, measure_names, nfft) + for _ in range(repeats)) + return convert_output_(output, measure_names) + + +def _calc_surrogate(data, var, measure_names, nfft): + var.fit(data) + return connectivity(measure_names, var.coef, var.rescov, nfft) + + +def jackknife_connectivity(measure_names, data, var, nfft=512, leaveout=1, + n_jobs=1, verbose=0): + """ Calculates Jackknife estimates of connectivity. + + For each Jackknife estimate a block of trials is left out. This is repeated + until each trial was left out exactly once. The number of estimates depends + on the number of trials and the value of `leaveout`. It is calculated by + repeats = `n_trials` // `leaveout`. + + .. note:: Parameter `var` will be modified by the function. Treat as + undefined after the function returned. + + Parameters + ---------- + measure_names : {str, list of str} + Name(s) of the connectivity measure(s) to calculate. See + :class:`Connectivity` for supported measures. + data : ndarray, shape = [n_samples, n_channels, (n_trials)] + Time series data (2D or 3D for multiple trials) + var : VARBase-like object + Instance of a VAR model. + nfft : int, optional + Number of frequency bins to calculate. Note that these points cover the + range between 0 and half the + sampling rate. + leaveout : int, optional + Number of trials to leave out in each estimate. + n_jobs : int | None + number of jobs to run in parallel. See `joblib.Parallel` for details. + verbose : int + verbosity level passed to joblib. + + Returns + ------- + result : array, shape = [`repeats`, n_channels, n_channels, nfft] + Values of the connectivity measure for each surrogate. If + `measure_names` is a list of strings a dictionary is returned, + where each key is the name of the measure, and the corresponding + values are ndarrays of shape + [`repeats`, n_channels, n_channels, nfft]. + """ + data = np.atleast_3d(data) + n, m, t = data.shape + + if leaveout < 1: + leaveout = int(leaveout * t) + + num_blocks = int(t / leaveout) + + mask = lambda block: [i for i in range(t) if i < block*leaveout or + i >= (block+1)*leaveout] + + par, func = parallel_loop(_calc_jackknife, n_jobs=n_jobs, verbose=verbose) + output = par(func(data[:, :, mask(b)], var, measure_names, nfft) + for b in range(num_blocks)) + return convert_output_(output, measure_names) + + +def _calc_jackknife(data_used, var, measure_names, nfft): + var.fit(data_used) + return connectivity(measure_names, var.coef, var.rescov, nfft) + + +def bootstrap_connectivity(measures, data, var, nfft=512, repeats=100, + num_samples=None, n_jobs=1, verbose=0): + """ Calculates Bootstrap estimates of connectivity. + + To obtain a bootstrap estimate trials are sampled randomly with replacement + from the data set. + + .. note:: Parameter `var` will be modified by the function. Treat as + undefined after the function returned. + + Parameters + ---------- + measure_names : {str, list of str} + Name(s) of the connectivity measure(s) to calculate. See + :class:`Connectivity` for supported measures. + data : ndarray, shape = [n_samples, n_channels, (n_trials)] + Time series data (2D or 3D for multiple trials) + var : VARBase-like object + Instance of a VAR model. + repeats : int, optional + How many bootstrap estimates to take. + num_samples : int, optional + How many samples to take for each bootstrap estimates. Defaults to the + same number of trials as present in the data. + n_jobs : int | None + number of jobs to run in parallel. See `joblib.Parallel` for details. + verbose : int + verbosity level passed to joblib. + + Returns + ------- + measure : array, shape = [`repeats`, n_channels, n_channels, nfft] + Values of the connectivity measure for each bootstrap estimate. If + `measure_names` is a list of strings a dictionary is returned, where + each key is the name of the measure, and the corresponding values are + ndarrays of shape [`repeats`, n_channels, n_channels, nfft]. + """ + data = np.atleast_3d(data) + n, m, t = data.shape + + if num_samples is None: + num_samples = t + + mask = lambda r: np.random.random_integers(0, data.shape[2]-1, num_samples) + + par, func = parallel_loop(_calc_bootstrap, n_jobs=n_jobs, verbose=verbose) + output = par(func(data[:, :, mask(r)], var, measures, nfft, num_samples) + for r in range(repeats)) + return convert_output_(output, measures) + + +def _calc_bootstrap(data, var, measures, nfft, num_samples): + var.fit(data) + return connectivity(measures, var.coef, var.rescov, nfft) + + +def test_bootstrap_difference(a, b): + """ Test mean difference between two bootstrap estimates. + + This function calculates the probability `p` of observing a more extreme + mean difference between `a` and `b` under the null hypothesis that `a` and + `b` come from the same distribution. + + If p is smaller than e.g. 0.05 we can reject the null hypothesis at an + alpha-level of 0.05 and conclude that `a` and `b` are likely to come from + different distributions. + + .. note:: *p*-values are calculated along the first dimension. Thus, + n_channels * n_channels * nfft individual *p*-values are + obtained. To determine if a difference is significant it is + important to correct for multiple testing. + + Parameters + ---------- + a, b : ndarray, shape = [`repeats`, n_channels, n_channels, nfft] + Two bootstrap estimates to compare. The number of repetitions (first + dimension) does not have be equal. + + Returns + ------- + p : ndarray, shape = [n_channels, n_channels, nfft] + *p*-values + + Notes + ----- + The function estimates the distribution of `b[j]` - `a[i]` by calculating + the difference for each combination of `i` and `j`. The total number of + difference samples available is therefore a.shape[0] * b.shape[0]. The + *p*-value is calculated as the smallest percentile of that distribution + that does not contain 0. + + See also + -------- + :func:`significance_fdr` : Correct for multiple testing by controlling the + false discovery rate. + """ + old_shape = a.shape[1:] + a = np.asarray(a).reshape((a.shape[0], -1)) + b = np.asarray(b).reshape((b.shape[0], -1)) + + n = a.shape[0] + + s1, s2 = 0, 0 + for i in cartesian((np.arange(n), np.arange(n))): + c = b[i[1], :] - a[i[0], :] + + s1 += c >= 0 + s2 += c <= 0 + + p = np.minimum(s1, s2) / (n*n) + + return p.reshape(old_shape) + + +def significance_fdr(p, alpha): + """ Calculate significance by controlling for the false discovery rate. + + This function determines which of the *p*-values in `p` can be considered + significant. Correction for multiple comparisons is performed by + controlling the false discovery rate (FDR). The FDR is the maximum fraction + of *p*-values that are wrongly considered significant [1]_. + + Parameters + ---------- + p : ndarray, shape = [n_channels, n_channels, nfft] + *p*-values + alpha : float + Maximum false discovery rate. + + Returns + ------- + s : ndarray, dtype=bool, shape = [n_channels, n_channels, nfft] + Significance of each *p*-value. + + References + ---------- + .. [1] Y. Benjamini, Y. Hochberg, "Controlling the false discovery rate: a + practical and powerful approach to multiple testing", Journal of the + Royal Statistical Society, Series B 57(1), pp 289-300, 1995 + """ + i = np.argsort(p, axis=None) + m = i.size - np.sum(np.isnan(p)) + + j = np.empty(p.shape, int) + j.flat[i] = np.arange(1, i.size+1) + + mask = p <= alpha*j/m + + if np.sum(mask) == 0: + return mask + + # find largest k so that p_k <= alpha*k/m + k = np.max(j[mask]) + + # reject all H_i for i = 0...k + s = j <= k + + return s + + +def convert_output_(output, measures): + if isinstance(measures, str): + return np.array(output) + else: + repeats = len(output) + output = dict((m, np.array([output[r][m] for r in range(repeats)])) + for m in measures) + return output \ No newline at end of file diff --git a/mne_sandbox/externals/scot/csp.py b/mne_sandbox/externals/scot/csp.py new file mode 100644 index 0000000..c5a7f7a --- /dev/null +++ b/mne_sandbox/externals/scot/csp.py @@ -0,0 +1,73 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +"""common spatial patterns (CSP) implementation""" + +import numpy as np +from scipy.linalg import eig + + +def csp(x, cl, numcomp=np.inf): + """ Calculate common spatial patterns (CSP) + + Parameters + ---------- + x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + EEG data set + cl : list of valid dict keys + Class labels associated with each trial. Currently only two classes are supported. + numcomp : {int}, optional + Number of patterns to keep after applying the CSP. If `numcomp` is greater than n_channels, all n_channels + patterns are returned. + + Returns + ------- + w : array, shape = [n_channels, n_components] + CSP weight matrix + v : array, shape = [n_components, n_channels] + CSP projection matrix + """ + + x = np.atleast_3d(x) + cl = np.asarray(cl).ravel() + + n, m, t = x.shape + + if t != cl.size: + raise AttributeError('CSP only works with multiple classes. Number of' + ' elemnts in cl (%d) must equal 3rd dimension of X (%d)' % (cl.size, t)) + + labels = np.unique(cl) + + if labels.size != 2: + raise AttributeError('CSP is currently ipmlemented for 2 classes (got %d)' % labels.size) + + x1 = x[:, :, cl == labels[0]] + x2 = x[:, :, cl == labels[1]] + + sigma1 = np.zeros((m, m)) + for t in range(x1.shape[2]): + sigma1 += np.cov(x1[:, :, t].transpose()) / x1.shape[2] + sigma1 /= sigma1.trace() + + sigma2 = np.zeros((m, m)) + for t in range(x2.shape[2]): + sigma2 += np.cov(x2[:, :, t].transpose()) / x2.shape[2] + sigma2 /= sigma2.trace() + + e, w = eig(sigma1, sigma1 + sigma2, overwrite_a=True, overwrite_b=True, check_finite=False) + + order = np.argsort(e)[::-1] + w = w[:, order] + # e = e[order] + + v = np.linalg.inv(w) + + # subsequently remove unwanted components from the middle of w and v + while w.shape[1] > numcomp: + i = int(np.floor(w.shape[1]/2)) + w = np.delete(w, i, 1) + v = np.delete(v, i, 0) + + return w, v diff --git a/mne_sandbox/externals/scot/datatools.py b/mne_sandbox/externals/scot/datatools.py new file mode 100644 index 0000000..783ee8f --- /dev/null +++ b/mne_sandbox/externals/scot/datatools.py @@ -0,0 +1,153 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" +Summary +------- +Tools for basic data manipulation. +""" + +import numpy as np + + +def cut_segments(rawdata, tr, start, stop): + """ Cut continuous signal into segments. + + This function cuts segments from a continuous signal. Segments are stop - start samples long. + + Parameters + ---------- + rawdata : array_like + Input data of shape [`n`,`m`], with `n` samples and `m` signals. + tr : list of int + Trigger positions. + start : int + Window start (offset relative to trigger) + stop : int + Window end (offset relative to trigger) + + Returns + ------- + x : ndarray + Segments cut from `rawdata`. Individual segments are stacked along the third dimension. + + See also + -------- + cat_trials : Concatenate segments + + Examples + -------- + >>> data = np.random.randn(1000, 5) + >>> tr = [250, 500, 750] + >>> x = cut_segments(data, tr, 50, 100) + >>> x.shape + (50, 5, 3) + """ + rawdata = np.atleast_2d(rawdata) + tr = np.array(tr, dtype='int').ravel() + win = range(start, stop) + return np.dstack([rawdata[tr[t] + win, :] for t in range(len(tr))]) + + +def cat_trials(x): + """ Concatenate trials along time axis. + + Parameters + ---------- + x : array_like + Segmented input data of shape [`n`,`m`,`t`], with `n` time samples, `m` signals, and `t` trials. + + Returns + ------- + out : ndarray + Trials are concatenated along the first (time) axis. Shape of the output is [`n``t`,`m`]. + + See also + -------- + cut_segments : Cut segments from continuous data + + Examples + -------- + >>> x = np.random.randn(150, 4, 6) + >>> y = cat_trials(x) + >>> y.shape + (900, 4) + """ + x = np.atleast_3d(x) + t = x.shape[2] + return np.squeeze(np.vstack(np.dsplit(x, t)), axis=2) + + +def dot_special(x, a): + """ Trial-wise dot product. + + This function calculates the dot product of `x[:,:,i]` with `a` for each `i`. + + Parameters + ---------- + x : array_like + Segmented input data of shape [`n`,`m`,`t`], with `n` time samples, `m` signals, and `t` trials. + The dot product is calculated for each trial. + a : array_like + Second argument + + Returns + ------- + out : ndarray + Returns the dot product of each trial. + + Examples + -------- + >>> x = np.random.randn(150, 40, 6) + >>> a = np.ones((40, 7)) + >>> y = dot_special(x, a) + >>> y.shape + (150, 7, 6) + """ + x = np.atleast_3d(x) + a = np.atleast_2d(a) + return np.dstack([x[:, :, i].dot(a) for i in range(x.shape[2])]) + + +def randomize_phase(data): + """ Phase randomization. + + This function randomizes the input array's spectral phase along the first dimension. + + Parameters + ---------- + data : array_like + Input array + + Returns + ------- + out : ndarray + Array of same shape as `data`. + + Notes + ----- + The algorithm randomizes the phase component of the input's complex fourier transform. + + Examples + -------- + .. plot:: + :include-source: + + from pylab import * + from scot.datatools import randomize_phase + np.random.seed(1234) + s = np.sin(np.linspace(0,10*np.pi,1000)).T + x = np.vstack([s, np.sign(s)]).T + y = randomize_phase(x) + subplot(2,1,1) + title('Phase randomization of sine wave and rectangular function') + plot(x), axis([0,1000,-3,3]) + subplot(2,1,2) + plot(y), axis([0,1000,-3,3]) + plt.show() + """ + data = np.asarray(data) + data_freq = np.fft.rfft(data, axis=0) + data_freq = np.abs(data_freq) * np.exp(1j*np.random.random_sample(data_freq.shape)*2*np.pi) + return np.fft.irfft(data_freq, data.shape[0], axis=0) \ No newline at end of file diff --git a/mne_sandbox/externals/scot/matfiles.py b/mne_sandbox/externals/scot/matfiles.py new file mode 100644 index 0000000..2d9bcb5 --- /dev/null +++ b/mne_sandbox/externals/scot/matfiles.py @@ -0,0 +1,49 @@ +""" +Summary +------- +Routines for loading and saving Matlab's .mat files. +""" + +from scipy.io import loadmat as sploadmat +from scipy.io import savemat as spsavemat +from scipy.io import matlab + + +def loadmat(filename): + """This function should be called instead of direct spio.loadmat + as it cures the problem of not properly recovering python dictionaries + from mat files. It calls the function check keys to cure all entries + which are still mat-objects + """ + data = sploadmat(filename, struct_as_record=False, squeeze_me=True) + return _check_keys(data) + + +savemat = spsavemat + + +def _check_keys(dictionary): + """ + checks if entries in dictionary are mat-objects. If yes + todict is called to change them to nested dictionaries + """ + for key in dictionary: + if isinstance(dictionary[key], matlab.mio5_params.mat_struct): + dictionary[key] = _todict(dictionary[key]) + return dictionary + + +def _todict(matobj): + """ + a recursive function which constructs from matobjects nested dictionaries + """ + dictionary = {} + #noinspection PyProtectedMember + for strg in matobj._fieldnames: + elem = matobj.__dict__[strg] + if isinstance(elem, matlab.mio5_params.mat_struct): + dictionary[strg] = _todict(elem) + else: + dictionary[strg] = elem + return dictionary + diff --git a/mne_sandbox/externals/scot/ooapi.py b/mne_sandbox/externals/scot/ooapi.py new file mode 100644 index 0000000..574818c --- /dev/null +++ b/mne_sandbox/externals/scot/ooapi.py @@ -0,0 +1,802 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" +Summary +------- +Object oriented API to SCoT. + +Extended Summary +---------------- +The object oriented API provides a the `Workspace` class, which provides high-level functionality and serves as an +example usage of the low-level API. +""" + +import numpy as np + +from . import config +from .varica import mvarica, cspvarica +from .plainica import plainica +from .datatools import dot_special +from .connectivity import Connectivity +from .connectivity_statistics import surrogate_connectivity, bootstrap_connectivity, test_bootstrap_difference +from .connectivity_statistics import significance_fdr + + +class Workspace: + """SCoT Workspace + + This class provides high-level functionality for source identification, connectivity estimation, and visualization. + + Parameters + ---------- + var : {:class:`~scot.var.VARBase`-like object, dict} + Vector autoregressive model (VAR) object that is used for model fitting. + This can also be a dictionary that is passed as `**kwargs` to backend['var']() in order to + construct a new VAR model object. + locations : array_like, optional + 3D Electrode locations. Each row holds the x, y, and z coordinates of an electrode. + reducedim : {int, float, 'no_pca'}, optional + A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All + components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. + An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. + If set to 'no_pca' the PCA step is skipped. + nfft : int, optional + Number of frequency bins for connectivity estimation. + backend : dict-like, optional + Specify backend to use. When set to None the backend configured in config.backend is used. + + Attributes + ---------- + `unmixing_` : array + Estimated unmixing matrix. + `mixing_` : array + Estimated mixing matrix. + `plot_diagonal` : str + Configures what is plotted in the diagonal subplots. + **'topo'** (default) plots topoplots on the diagonal, + **'S'** plots the spectral density of each component, and + **'fill'** plots connectivity on the diagonal. + `plot_outside_topo` : bool + Whether to place topoplots in the left column and top row. + `plot_f_range` : (int, int) + Lower and upper frequency limits for plotting. Defaults to [0, fs/2]. + """ + def __init__(self, var, locations=None, reducedim=0.99, nfft=512, fs=2, backend=None): + self.data_ = None + self.cl_ = None + self.fs_ = fs + self.time_offset_ = 0 + self.unmixing_ = None + self.mixing_ = None + self.premixing_ = None + self.activations_ = None + self.connectivity_ = None + self.locations_ = locations + self.reducedim_ = reducedim + self.nfft_ = nfft + self.backend_ = backend + + self.trial_mask_ = [] + + self.topo_ = None + self.mixmaps_ = [] + self.unmixmaps_ = [] + + self.var_multiclass_ = None + self.var_model_ = None + self.var_cov_ = None + + self.plot_diagonal = 'topo' + self.plot_outside_topo = False + self.plot_f_range = [0, fs/2] + + self._plotting = None + + if self.backend_ is None: + self.backend_ = config.backend + + try: + self.var_ = self.backend_['var'](**var) + except TypeError: + self.var_ = var + + def __str__(self): + if self.data_ is not None: + datastr = '%d samples, %d channels, %d trials' % self.data_.shape + else: + datastr = 'None' + + if self.cl_ is not None: + clstr = str(np.unique(self.cl_)) + else: + clstr = 'None' + + if self.unmixing_ is not None: + sourcestr = str(self.unmixing_.shape[1]) + else: + sourcestr = 'None' + + if self.var_ is None: + varstr = 'None' + else: + varstr = str(self.var_) + + s = 'Workspace:\n' + s += ' Data : ' + datastr + '\n' + s += ' Classes : ' + clstr + '\n' + s += ' Sources : ' + sourcestr + '\n' + s += ' VAR models: ' + varstr + '\n' + + return s + + def set_locations(self, locations): + """ Set sensor locations. + + Parameters + ---------- + locations : array_like + 3D Electrode locations. Each row holds the x, y, and z coordinates of an electrode. + """ + self.locations_ = locations + + def set_premixing(self, premixing): + """ Set premixing matrix. + + The premixing matrix maps data to physical channels. If the data is actual channel data, + the premixing matrix can be set to identity. Use this functionality if the data was pre- + transformed with e.g. PCA. + + Parameters + ---------- + premixing : array_like, shape = [n_signals, n_channels] + Matrix that maps data signals to physical channels. + """ + self.premixing_ = premixing + + def set_data(self, data, cl=None, time_offset=0): + """ Assign data to the workspace. + + This function assigns a new data set to the workspace. Doing so invalidates currently fitted VAR models, + connectivity estimates, and activations. + + Parameters + ---------- + data : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + EEG data set + cl : list of valid dict keys + Class labels associated with each trial. + time_offset : float, optional + Trial starting time; used for labelling the x-axis of time/frequency plots. + """ + self.data_ = np.atleast_3d(data) + self.cl_ = np.asarray(cl if cl is not None else [None]*self.data_.shape[2]) + self.time_offset_ = time_offset + self.var_model_ = None + self.var_cov_ = None + self.connectivity_ = None + + self.trial_mask_ = np.ones(self.cl_.size, dtype=bool) + + if self.unmixing_ is not None: + self.activations_ = dot_special(self.data_, self.unmixing_) + + def set_used_labels(self, labels): + """ Specify which trials to use in subsequent analysis steps. + + This function masks trials based on their class labels. + + Parameters + ---------- + labels : list of class labels + Marks all trials that have a label that is in the `labels` list for further processing. + """ + mask = np.zeros(self.cl_.size, dtype=bool) + for l in labels: + mask = np.logical_or(mask, self.cl_ == l) + self.trial_mask_ = mask + + def do_mvarica(self, varfit='ensemble'): + """ Perform MVARICA + + Perform MVARICA source decomposition and VAR model fitting. + + Parameters + ---------- + varfit : string + Determines how to calculate the residuals for source decomposition. + 'ensemble' (default) fits one model to the whole data set, + 'class' fits a different model for each class, and + 'trial' fits a different model for each individual trial. + + Returns + ------- + result : class + see :func:`mvarica` for a description of the return value. + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain data. + + See Also + -------- + :func:`mvarica` : MVARICA implementation + """ + if self.data_ is None: + raise RuntimeError("MVARICA requires data to be set") + result = mvarica(x=self.data_[:, :, self.trial_mask_], cl=self.cl_[self.trial_mask_], var=self.var_, + reducedim=self.reducedim_, backend=self.backend_, varfit=varfit) + self.mixing_ = result.mixing + self.unmixing_ = result.unmixing + self.var_ = result.b + self.connectivity_ = Connectivity(result.b.coef, result.b.rescov, self.nfft_) + self.activations_ = dot_special(self.data_, self.unmixing_) + self.mixmaps_ = [] + self.unmixmaps_ = [] + return result + + def do_cspvarica(self, varfit='ensemble'): + """ Perform CSPVARICA + + Perform CSPVARICA source decomposition and VAR model fitting. + + Parameters + ---------- + varfit : string + Determines how to calculate the residuals for source decomposition. + 'ensemble' (default) fits one model to the whole data set, + 'class' fits a different model for each class, and + 'trial' fits a different model for each individual trial. + + Returns + ------- + result : class + see :func:`cspvarica` for a description of the return value. + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain data. + + See Also + -------- + :func:`cspvarica` : CSPVARICA implementation + """ + if self.data_ is None: + raise RuntimeError("CSPVARICA requires data to be set") + try: + sorted(self.cl_) + for c in self.cl_: + assert(c is not None) + except (TypeError, AssertionError): + raise RuntimeError("CSPVARICA requires orderable and hashable class labels that are not None") + result = cspvarica(x=self.data_, var=self.var_, cl=self.cl_, + reducedim=self.reducedim_, backend=self.backend_, varfit=varfit) + self.mixing_ = result.mixing + self.unmixing_ = result.unmixing + self.var_ = result.b + self.connectivity_ = Connectivity(self.var_.coef, self.var_.rescov, self.nfft_) + self.activations_ = dot_special(self.data_, self.unmixing_) + self.mixmaps_ = [] + self.unmixmaps_ = [] + return result + + def do_ica(self): + """ Perform ICA + + Perform plain ICA source decomposition. + + Returns + ------- + result : class + see :func:`plainica` for a description of the return value. + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain data. + """ + if self.data_ is None: + raise RuntimeError("ICA requires data to be set") + result = plainica(x=self.data_[:, :, self.trial_mask_], reducedim=self.reducedim_, backend=self.backend_) + self.mixing_ = result.mixing + self.unmixing_ = result.unmixing + self.activations_ = dot_special(self.data_, self.unmixing_) + self.var_model_ = None + self.var_cov_ = None + self.connectivity_ = None + self.mixmaps_ = [] + self.unmixmaps_ = [] + return result + + def remove_sources(self, sources): + """ Remove sources from the decomposition. + + This function removes sources from the decomposition. Doing so invalidates currently fitted VAR models and + connectivity estimates. + + Parameters + ---------- + sources : {slice, int, array of ints} + Indices of components to remove. + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain a source decomposition. + """ + if self.unmixing_ is None or self.mixing_ is None: + raise RuntimeError("No sources available (run do_mvarica first)") + self.mixing_ = np.delete(self.mixing_, sources, 0) + self.unmixing_ = np.delete(self.unmixing_, sources, 1) + if self.activations_ is not None: + self.activations_ = np.delete(self.activations_, sources, 1) + self.var_model_ = None + self.var_cov_ = None + self.connectivity_ = None + self.mixmaps_ = [] + self.unmixmaps_ = [] + + def fit_var(self): + """ Fit a var model to the source activations. + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain source activations. + """ + if self.activations_ is None: + raise RuntimeError("VAR fitting requires source activations (run do_mvarica first)") + self.var_.fit(data=self.activations_[:, :, self.trial_mask_]) + self.connectivity_ = Connectivity(self.var_.coef, self.var_.rescov, self.nfft_) + + def optimize_var(self): + """ Optimize the var model's hyperparameters (such as regularization). + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain source activations. + """ + if self.activations_ is None: + raise RuntimeError("VAR fitting requires source activations (run do_mvarica first)") + + self.var_.optimize(self.activations_[:, :, self.trial_mask_]) + + def get_connectivity(self, measure_name, plot=False): + """ Calculate spectral connectivity measure. + + Parameters + ---------- + measure_name : str + Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. + plot : {False, None, Figure object}, optional + Whether and where to plot the connectivity. If set to **False**, nothing is plotted. Otherwise set to the + Figure object. If set to **None**, a new figure is created. + + Returns + ------- + measure : array, shape = [n_channels, n_channels, nfft] + Values of the connectivity measure. + fig : Figure object + Instance of the figure in which was plotted. This is only returned if `plot` is not **False**. + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain a fitted VAR model. + """ + if self.connectivity_ is None: + raise RuntimeError("Connectivity requires a VAR model (run do_mvarica or fit_var first)") + + cm = getattr(self.connectivity_, measure_name)() + + cm = np.abs(cm) if np.any(np.iscomplex(cm)) else cm + + if plot is None or plot: + fig = plot + if self.plot_diagonal == 'fill': + diagonal = 0 + elif self.plot_diagonal == 'S': + diagonal = -1 + sm = np.abs(self.connectivity_.S()) + sm /= np.max(sm) # scale to 1 since components are scaled arbitrarily anyway + fig = self.plotting.plot_connectivity_spectrum(sm, fs=self.fs_, freq_range=self.plot_f_range, + diagonal=1, border=self.plot_outside_topo, fig=fig) + else: + diagonal = -1 + + fig = self.plotting.plot_connectivity_spectrum(cm, fs=self.fs_, freq_range=self.plot_f_range, + diagonal=diagonal, border=self.plot_outside_topo, fig=fig) + + return cm, fig + + return cm + + def get_surrogate_connectivity(self, measure_name, repeats=100, plot=False): + """ Calculate spectral connectivity measure under the assumption of no actual connectivity. + + Repeatedly samples connectivity from phase-randomized data. This provides estimates of the connectivity + distribution if there was no causal structure in the data. + + Parameters + ---------- + measure_name : str + Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. + repeats : int, optional + How many surrogate samples to take. + + Returns + ------- + measure : array, shape = [`repeats`, n_channels, n_channels, nfft] + Values of the connectivity measure for each surrogate. + + See Also + -------- + :func:`scot.connectivity_statistics.surrogate_connectivity` : Calculates surrogate connectivity + """ + cs = surrogate_connectivity(measure_name, self.activations_[:, :, self.trial_mask_], + self.var_, self.nfft_, repeats) + + if plot is None or plot: + fig = plot + if self.plot_diagonal == 'fill': + diagonal = 0 + elif self.plot_diagonal == 'S': + diagonal = -1 + sb = self.get_surrogate_connectivity('absS', repeats) + sb /= np.max(sb) # scale to 1 since components are scaled arbitrarily anyway + su = np.percentile(sb, 95, axis=0) + fig = self.plotting.plot_connectivity_spectrum([su], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=1, border=self.plot_outside_topo, fig=fig) + else: + diagonal = -1 + cu = np.percentile(cs, 95, axis=0) + fig = self.plotting.plot_connectivity_spectrum([cu], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=diagonal, border=self.plot_outside_topo, fig=fig) + return cs, fig + + return cs + + def get_bootstrap_connectivity(self, measure_names, repeats=100, num_samples=None, plot=False): + """ Calculate bootstrap estimates of spectral connectivity measures. + + Bootstrapping is performed on trial level. + + Parameters + ---------- + measure_names : {str, list of str} + Name(s) of the connectivity measure(s) to calculate. See :class:`Connectivity` for supported measures. + repeats : int, optional + How many bootstrap estimates to take. + num_samples : int, optional + How many samples to take for each bootstrap estimates. Defaults to the same number of trials as present in + the data. + + Returns + ------- + measure : array, shape = [`repeats`, n_channels, n_channels, nfft] + Values of the connectivity measure for each bootstrap estimate. If `measure_names` is a list of strings a + dictionary is returned, where each key is the name of the measure, and the corresponding values are + ndarrays of shape [`repeats`, n_channels, n_channels, nfft]. + + See Also + -------- + :func:`scot.connectivity_statistics.bootstrap_connectivity` : Calculates bootstrap connectivity + """ + if num_samples is None: + num_samples = np.sum(self.trial_mask_) + + cb = bootstrap_connectivity(measure_names, self.activations_[:, :, self.trial_mask_], + self.var_, self.nfft_, repeats, num_samples) + + if plot is None or plot: + fig = plot + if self.plot_diagonal == 'fill': + diagonal = 0 + elif self.plot_diagonal == 'S': + diagonal = -1 + sb = self.get_bootstrap_connectivity('absS', repeats, num_samples) + sb /= np.max(sb) # scale to 1 since components are scaled arbitrarily anyway + sm = np.median(sb, axis=0) + sl = np.percentile(sb, 2.5, axis=0) + su = np.percentile(sb, 97.5, axis=0) + fig = self.plotting.plot_connectivity_spectrum([sm, sl, su], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=1, border=self.plot_outside_topo, fig=fig) + else: + diagonal = -1 + cm = np.median(cb, axis=0) + cl = np.percentile(cb, 2.5, axis=0) + cu = np.percentile(cb, 97.5, axis=0) + fig = self.plotting.plot_connectivity_spectrum([cm, cl, cu], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=diagonal, border=self.plot_outside_topo, fig=fig) + return cb, fig + + return cb + + def get_tf_connectivity(self, measure_name, winlen, winstep, plot=False, crange='default'): + """ Calculate estimate of time-varying connectivity. + + Connectivity is estimated in a sliding window approach on the current data set. The window is stepped + `n_steps` = (`n_samples` - `winlen`) // `winstep` times. + + Parameters + ---------- + measure_name : str + Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. + winlen : int + Length of the sliding window (in samples). + winstep : int + Step size for sliding window (in samples). + plot : {False, None, Figure object}, optional + Whether and where to plot the connectivity. If set to **False**, nothing is plotted. Otherwise set to the + Figure object. If set to **None**, a new figure is created. + + Returns + ------- + result : array, shape = [n_channels, n_channels, nfft, n_steps] + Values of the connectivity measure. + fig : Figure object, optional + Instance of the figure in which was plotted. This is only returned if `plot` is not **False**. + + Raises + ------ + RuntimeError + If the :class:`Workspace` instance does not contain a fitted VAR model. + """ + if self.activations_ is None: + raise RuntimeError("Time/Frequency Connectivity requires activations (call set_data after do_mvarica)") + [n, m, _] = self.activations_.shape + + nstep = (n - winlen) // winstep + + result = np.zeros((m, m, self.nfft_, nstep), np.complex64) + i = 0 + for j in range(0, n - winlen, winstep): + win = np.arange(winlen) + j + data = self.activations_[win, :, :] + data = data[:, :, self.trial_mask_] + self.var_.fit(data) + con = Connectivity(self.var_.coef, self.var_.rescov, self.nfft_) + result[:, :, :, i] = getattr(con, measure_name)() + i += 1 + + if plot is None or plot: + fig = plot + t0 = 0.5 * winlen / self.fs_ + self.time_offset_ + t1 = self.data_.shape[0] / self.fs_ - 0.5 * winlen / self.fs_ + self.time_offset_ + if self.plot_diagonal == 'fill': + diagonal = 0 + elif self.plot_diagonal == 'S': + diagonal = -1 + s = np.abs(self.get_tf_connectivity('S', winlen, winstep)) + if crange == 'default': + crange = [np.min(s), np.max(s)] + fig = self.plotting.plot_connectivity_timespectrum(s, fs=self.fs_, crange=[np.min(s), np.max(s)], + freq_range=self.plot_f_range, time_range=[t0, t1], + diagonal=1, border=self.plot_outside_topo, fig=fig) + else: + diagonal = -1 + + tfc = self._clean_measure(measure_name, result) + if crange == 'default': + if diagonal == -1: + for m in range(tfc.shape[0]): + tfc[m, m, :, :] = 0 + crange = [np.min(tfc), np.max(tfc)] + fig = self.plotting.plot_connectivity_timespectrum(tfc, fs=self.fs_, crange=crange, + freq_range=self.plot_f_range, time_range=[t0, t1], + diagonal=diagonal, border=self.plot_outside_topo, fig=fig) + + return result, fig + + return result + + def compare_conditions(self, labels1, labels2, measure_name, alpha=0.01, repeats=100, num_samples=None, plot=False): + """ Test for significant difference in connectivity of two sets of class labels. + + Connectivity estimates are obtained by bootstrapping. Correction for multiple testing is performed by + controlling the false discovery rate (FDR). + + Parameters + ---------- + labels1, labels2 : list of class labels + The two sets of class labels to compare. Each set may contain more than one label. + measure_name : str + Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. + alpha : float, optional + Maximum allowed FDR. The ratio of falsely detected significant differences is guaranteed to be less than + `alpha`. + repeats : int, optional + How many bootstrap estimates to take. + num_samples : int, optional + How many samples to take for each bootstrap estimates. Defaults to the same number of trials as present in + the data. + plot : {False, None, Figure object}, optional + Whether and where to plot the connectivity. If set to **False**, nothing is plotted. Otherwise set to the + Figure object. If set to **None**, a new figure is created. + + Returns + ------- + p : array, shape = [n_channels, n_channels, nfft] + Uncorrected p-values. + s : array, dtype=bool, shape = [n_channels, n_channels, nfft] + FDR corrected significance. True means the difference is significant in this location. + fig : Figure object, optional + Instance of the figure in which was plotted. This is only returned if `plot` is not **False**. + """ + self.set_used_labels(labels1) + ca = self.get_bootstrap_connectivity(measure_name, repeats, num_samples) + self.set_used_labels(labels2) + cb = self.get_bootstrap_connectivity(measure_name, repeats, num_samples) + + p = test_bootstrap_difference(ca, cb) + s = significance_fdr(p, alpha) + + if plot is None or plot: + fig = plot + if self.plot_diagonal == 'topo': + diagonal = -1 + elif self.plot_diagonal == 'fill': + diagonal = 0 + elif self.plot_diagonal is 'S': + diagonal = -1 + self.set_used_labels(labels1) + sa = self.get_bootstrap_connectivity('absS', repeats, num_samples) + sm = np.median(sa, axis=0) + sl = np.percentile(sa, 2.5, axis=0) + su = np.percentile(sa, 97.5, axis=0) + fig = self.plotting.plot_connectivity_spectrum([sm, sl, su], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=1, border=self.plot_outside_topo, fig=fig) + + self.set_used_labels(labels2) + sb = self.get_bootstrap_connectivity('absS', repeats, num_samples) + sm = np.median(sb, axis=0) + sl = np.percentile(sb, 2.5, axis=0) + su = np.percentile(sb, 97.5, axis=0) + fig = self.plotting.plot_connectivity_spectrum([sm, sl, su], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=1, border=self.plot_outside_topo, fig=fig) + + p_s = test_bootstrap_difference(ca, cb) + s_s = significance_fdr(p_s, alpha) + + self.plotting.plot_connectivity_significance(s_s, fs=self.fs_, freq_range=self.plot_f_range, + diagonal=1, border=self.plot_outside_topo, fig=fig) + else: + diagonal = -1 + + cm = np.median(ca, axis=0) + cl = np.percentile(ca, 2.5, axis=0) + cu = np.percentile(ca, 97.5, axis=0) + + fig = self.plotting.plot_connectivity_spectrum([cm, cl, cu], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=diagonal, border=self.plot_outside_topo, fig=fig) + + cm = np.median(cb, axis=0) + cl = np.percentile(cb, 2.5, axis=0) + cu = np.percentile(cb, 97.5, axis=0) + + fig = self.plotting.plot_connectivity_spectrum([cm, cl, cu], fs=self.fs_, freq_range=self.plot_f_range, + diagonal=diagonal, border=self.plot_outside_topo, fig=fig) + + self.plotting.plot_connectivity_significance(s, fs=self.fs_, freq_range=self.plot_f_range, + diagonal=diagonal, border=self.plot_outside_topo, fig=fig) + + return p, s, fig + + return p, s + + def show_plots(self): + """Show current plots. + + This is only a convenience wrapper around :func:`matplotlib.pyplot.show_plots`. + + """ + self.plotting.show_plots() + + def plot_source_topos(self, common_scale=None): + """ Plot topography of the Source decomposition. + + Parameters + ---------- + common_scale : float, optional + If set to None, each topoplot's color axis is scaled individually. Otherwise specifies the percentile + (1-99) of values in all plot. This value is taken as the maximum color scale. + """ + if self.unmixing_ is None and self.mixing_ is None: + raise RuntimeError("No sources available (run do_mvarica first)") + + self._prepare_plots(True, True) + + self.plotting.plot_sources(self.topo_, self.mixmaps_, self.unmixmaps_, common_scale) + + def plot_connectivity_topos(self, fig=None): + """ Plot scalp projections of the sources. + + This function only plots the topos. Use in combination with connectivity plotting. + + Parameters + ---------- + fig : {None, Figure object}, optional + Where to plot the topos. f set to **None**, a new figure is created. Otherwise plot into the provided + figure object. + + Returns + ------- + fig : Figure object + Instance of the figure in which was plotted. + """ + self._prepare_plots(True, False) + if self.plot_outside_topo: + fig = self.plotting.plot_connectivity_topos('outside', self.topo_, self.mixmaps_, fig) + elif self.plot_diagonal == 'topo': + fig = self.plotting.plot_connectivity_topos('diagonal', self.topo_, self.mixmaps_, fig) + return fig + + def plot_connectivity_surrogate(self, measure_name, repeats=100, fig=None): + """ Plot spectral connectivity measure under the assumption of no actual connectivity. + + Repeatedly samples connectivity from phase-randomized data. This provides estimates of the connectivity + distribution if there was no causal structure in the data. + + Parameters + ---------- + measure_name : str + Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. + repeats : int, optional + How many surrogate samples to take. + fig : {None, Figure object}, optional + Where to plot the topos. f set to **None**, a new figure is created. Otherwise plot into the provided + figure object. + + Returns + ------- + fig : Figure object + Instance of the figure in which was plotted. + """ + cb = self.get_surrogate_connectivity(measure_name, repeats) + + self._prepare_plots(True, False) + + cu = np.percentile(cb, 95, axis=0) + + fig = self.plotting.plot_connectivity_spectrum([cu], self.fs_, freq_range=self.plot_f_range, fig=fig) + + return fig + + @property + def plotting(self): + if not self._plotting: + from . import plotting + self._plotting = plotting + return self._plotting + + def _prepare_plots(self, mixing=False, unmixing=False): + if self.locations_ is None: + raise RuntimeError("Need sensor locations for plotting") + + if self.topo_ is None: + from scot.eegtopo.topoplot import Topoplot + self.topo_ = Topoplot() + self.topo_.set_locations(self.locations_) + + if mixing and not self.mixmaps_: + premix = self.premixing_ if self.premixing_ is not None else np.eye(self.mixing_.shape[1]) + self.mixmaps_ = self.plotting.prepare_topoplots(self.topo_, np.dot(self.mixing_, premix)) + #self.mixmaps_ = self.plotting.prepare_topoplots(self.topo_, self.mixing_) + + if unmixing and not self.unmixmaps_: + preinv = np.linalg.pinv(self.premixing_) if self.premixing_ is not None else np.eye(self.unmixing_.shape[0]) + self.unmixmaps_ = self.plotting.prepare_topoplots(self.topo_, np.dot(preinv, self.unmixing_).T) + #self.unmixmaps_ = self.plotting.prepare_topoplots(self.topo_, self.unmixing_.transpose()) + + @staticmethod + def _clean_measure(measure, a): + if measure in ['a', 'H', 'COH', 'pCOH']: + return np.abs(a) + elif measure in ['S', 'g']: + return np.log(np.abs(a)) + else: + return np.real(a) diff --git a/mne_sandbox/externals/scot/parallel.py b/mne_sandbox/externals/scot/parallel.py new file mode 100644 index 0000000..a7a1246 --- /dev/null +++ b/mne_sandbox/externals/scot/parallel.py @@ -0,0 +1,33 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2014 SCoT Development Team + + +def parallel_loop(func, n_jobs=1, verbose=1): + """run loops in parallel, if joblib is available. + + Parameters + ---------- + func : function + function to be executed in parallel + n_jobs : int + number of jobs + verbose : int + verbosity level + """ + try: + from joblib import Parallel, delayed + except ImportError: + n_jobs = None + + if n_jobs is None: + if verbose >= 10: + print('running ', func, ' serially') + par = lambda x: list(x) + else: + if verbose >= 10: + print('running ', func, ' in parallel') + func = delayed(func) + par = Parallel(n_jobs=n_jobs, verbose=verbose) + + return par, func diff --git a/mne_sandbox/externals/scot/pca.py b/mne_sandbox/externals/scot/pca.py new file mode 100644 index 0000000..2aae85c --- /dev/null +++ b/mne_sandbox/externals/scot/pca.py @@ -0,0 +1,130 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +"""principal component analysis (PCA) implementation""" + +import numpy as np +from .datatools import cat_trials + + +def pca_svd(data): + """calculate PCA using SVD + + Parameters + ---------- + data : array, shape = [n_samples, n_channels] + Two dimensional data array. + + Returns + ------- + w : array + Eigenvectors + s : array + Eigenvalues + """ + + (w, s, v) = np.linalg.svd(data.transpose()) + + return w, s ** 2 + + +def pca_eig(x): + """calculate PCA using Eigenvalue decomposition + + Parameters + ---------- + data : array, shape = [n_samples, n_channels] + Two dimensional data array. + + Returns + ------- + w : array + Eigenvectors + s : array + Eigenvalues + """ + + [s, w] = np.linalg.eigh(x.transpose().dot(x)) + + return w, s + + +def pca(x, subtract_mean=False, normalize=False, sort_components=True, reducedim=None, algorithm=pca_eig): + """ Calculate principal component analysis (PCA) + + Parameters + ---------- + x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + EEG data set + subtract_mean : bool, optional + Subtract sample mean from x. + normalize : bool, optional + Normalize variances to 1 before applying PCA. + sort_components : bool, optional + Sort principal components in order of decreasing eigenvalues. + reducedim : {float, int}, optional + A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All + components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. + An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. + algorithm : func, optional + Specify function to use for eigenvalue decomposition (:func:`pca_eig` or :func:`pca_svd`) + + Returns + ------- + w : array, shape = [n_channels, n_components] + PCA transformation matrix + v : array, shape = [n_components, n_channels] + PCA backtransformation matrix + """ + + x = cat_trials(np.atleast_3d(x)) + + if reducedim: + sort_components = True + + if subtract_mean: + for i in range(np.shape(x)[1]): + x[:, i] -= np.mean(x[:, i]) + + k, l = None, None + if normalize: + l = np.std(x, 0, ddof=1) + k = np.diag(1.0 / l) + l = np.diag(l) + x = x.dot(k) + + w, latent = algorithm(x) + + #v = np.linalg.inv(w) + # PCA is just a rotation, so inverse is equal transpose... + v = w.T + + if normalize: + w = k.dot(w) + v = v.dot(l) + + latent /= sum(latent) + + if sort_components: + order = np.argsort(latent)[::-1] + w = w[:, order] + v = v[order, :] + latent = latent[order] + + if reducedim and reducedim < 1: + selected = np.nonzero(np.cumsum(latent) < reducedim)[0] + try: + selected = np.concatenate([selected, [selected[-1] + 1]]) + except IndexError: + selected = [0] + if selected[-1] >= w.shape[1]: + selected = selected[0:-1] + w = w[:, selected] + v = v[selected, :] + + if reducedim and reducedim >= 1: + w = w[:, np.arange(reducedim)] + v = v[np.arange(reducedim), :] + + return w, v diff --git a/mne_sandbox/externals/scot/plainica.py b/mne_sandbox/externals/scot/plainica.py new file mode 100644 index 0000000..297329e --- /dev/null +++ b/mne_sandbox/externals/scot/plainica.py @@ -0,0 +1,77 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Source decomposition with ICA. +""" + +import numpy as np + +from . import config +from .datatools import cat_trials + + +class ResultICA: + """ Result of :func:`plainica` + + Attributes + ---------- + `mixing` : array + estimate of the mixing matrix + `unmixing` : array + estimate of the unmixing matrix + """ + def __init__(self, mx, ux): + self.mixing = mx + self.unmixing = ux + + +def plainica(x, reducedim=0.99, backend=None): + """ Source decomposition with ICA. + + Apply ICA to the data x, with optional PCA dimensionality reduction. + + Parameters + ---------- + x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + data set + reducedim : {int, float, 'no_pca'}, optional + A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All + components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. + An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. + If set to 'no_pca' the PCA step is skipped. + backend : dict-like, optional + Specify backend to use. When set to None the backend configured in config.backend is used. + + Returns + ------- + result : ResultICA + Source decomposition + """ + + x = np.atleast_3d(x) + l, m, t = np.shape(x) + + if backend is None: + backend = config.backend + + # pre-transform the data with PCA + if reducedim == 'no pca': + c = np.eye(m) + d = np.eye(m) + xpca = x + else: + c, d, xpca = backend['pca'](x, reducedim) + + # run on residuals ICA to estimate volume conduction + mx, ux = backend['ica'](cat_trials(xpca)) + + # correct (un)mixing matrix estimatees + mx = mx.dot(d) + ux = c.dot(ux) + + class Result: + unmixing = ux + mixing = mx + + return Result diff --git a/mne_sandbox/externals/scot/plotting.py b/mne_sandbox/externals/scot/plotting.py new file mode 100644 index 0000000..88d272b --- /dev/null +++ b/mne_sandbox/externals/scot/plotting.py @@ -0,0 +1,677 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Graphical output with matplotlib + +This module attempts to import matplotlib for plotting functionality. +If matplotlib is not available no error is raised, but plotting functions will not be available. + +""" + +import numpy as np + + +def show_plots(): + import matplotlib.pyplot as plt + plt.show() + + +def new_figure(*args, **kwargs): + import matplotlib.pyplot as plt + return plt.figure(*args, **kwargs) + + +def current_axis(): + import matplotlib.pyplot as plt + return plt.gca() + + +def MaxNLocator(*args, **kwargs): + from matplotlib.ticker import MaxNLocator as mnl + return mnl(*args, **kwargs) + + +def prepare_topoplots(topo, values): + """ Prepare multiple topo maps for cached plotting. + + .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_values`. + + Parameters + ---------- + topo : :class:`~eegtopo.topoplot.Topoplot` + Scalp maps are created with this class + values : array, shape = [n_topos, n_channels] + Channel values for each topo plot + + Returns + ------- + topomaps : list of array + The map for each topo plot + """ + values = np.atleast_2d(values) + + topomaps = [] + + for i in range(values.shape[0]): + topo.set_values(values[i, :]) + topo.create_map() + topomaps.append(topo.get_map()) + + return topomaps + + +def plot_topo(axis, topo, topomap, crange=None, offset=(0,0)): + """ Draw a topoplot in given axis. + + .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_map`. + + Parameters + ---------- + axis : axis + Axis to draw into. + topo : :class:`~eegtopo.topoplot.Topoplot` + This object draws the topo plot + topomap : array, shape = [w_pixels, h_pixels] + Scalp-projected data + crange : [int, int], optional + Range of values covered by the colormap. + If set to None, [-max(abs(topomap)), max(abs(topomap))] is substituted. + offset : [float, float], optional + Shift the topo plot by [x,y] in axis units. + + Returns + ------- + h : image + Image object the map was plotted into + """ + topo.set_map(topomap) + h = topo.plot_map(axis, crange=crange, offset=offset) + topo.plot_locations(axis, offset=offset) + topo.plot_head(axis, offset=offset) + return h + + +def plot_sources(topo, mixmaps, unmixmaps, global_scale=None, fig=None): + """ Plot all scalp projections of mixing- and unmixing-maps. + + .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_map`. + + Parameters + ---------- + topo : :class:`~eegtopo.topoplot.Topoplot` + This object draws the topo plot + mixmaps : array, shape = [w_pixels, h_pixels] + Scalp-projected mixing matrix + unmixmaps : array, shape = [w_pixels, h_pixels] + Scalp-projected unmixing matrix + global_scale : float, optional + Set common color scale as given percentile of all map values to use as the maximum. + `None` scales each plot individually (default). + fig : Figure object, optional + Figure to plot into. If set to `None`, a new figure is created. + + Returns + ------- + fig : Figure object + The figure into which was plotted. + """ + urange, mrange = None, None + + m = len(mixmaps) + + if global_scale: + tmp = np.asarray(unmixmaps) + tmp = tmp[np.logical_not(np.isnan(tmp))] + umax = np.percentile(np.abs(tmp), global_scale) + umin = -umax + urange = [umin, umax] + + tmp = np.asarray(mixmaps) + tmp = tmp[np.logical_not(np.isnan(tmp))] + mmax = np.percentile(np.abs(tmp), global_scale) + mmin = -mmax + mrange = [mmin, mmax] + + y = np.floor(np.sqrt(m * 3 / 4)) + x = np.ceil(m / y) + + if fig is None: + fig = new_figure() + + axes = [] + for i in range(m): + axes.append(fig.add_subplot(2 * y, x, i + 1)) + plot_topo(axes[-1], topo, unmixmaps[i], crange=urange) + axes[-1].set_title(str(i)) + + axes.append(fig.add_subplot(2 * y, x, m + i + 1)) + plot_topo(axes[-1], topo, mixmaps[i], crange=mrange) + axes[-1].set_title(str(i)) + + for a in axes: + a.set_yticks([]) + a.set_xticks([]) + a.set_frame_on(False) + + axes[0].set_ylabel('Unmixing weights') + axes[1].set_ylabel('Scalp projections') + + return fig + + +def plot_connectivity_topos(layout='diagonal', topo=None, topomaps=None, fig=None): + """ Place topo plots in a figure suitable for connectivity visualization. + + .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_map`. + + Parameters + ---------- + layout : str + 'diagonal' -> place topo plots on diagonal. + otherwise -> place topo plots in left column and top row. + topo : :class:`~eegtopo.topoplot.Topoplot` + This object draws the topo plot + topomaps : array, shape = [w_pixels, h_pixels] + Scalp-projected map + fig : Figure object, optional + Figure to plot into. If set to `None`, a new figure is created. + + Returns + ------- + fig : Figure object + The figure into which was plotted. + """ + + m = len(topomaps) + + if fig is None: + fig = new_figure() + + if layout == 'diagonal': + for i in range(m): + ax = fig.add_subplot(m, m, i*(1+m) + 1) + plot_topo(ax, topo, topomaps[i]) + ax.set_yticks([]) + ax.set_xticks([]) + ax.set_frame_on(False) + else: + for i in range(m): + for j in [i+2, (i+1)*(m+1)+1]: + ax = fig.add_subplot(m+1, m+1, j) + plot_topo(ax, topo, topomaps[i]) + ax.set_yticks([]) + ax.set_xticks([]) + ax.set_frame_on(False) + + return fig + + +def plot_connectivity_spectrum(a, fs=2, freq_range=(-np.inf, np.inf), diagonal=0, border=False, fig=None): + """ Draw connectivity plots. + + Parameters + ---------- + a : array, shape = [n_channels, n_channels, n_fft] or [1 or 3, n_channels, n_channels, n_fft] + If a.ndim == 3, normal plots are created, + If a.ndim == 4 and a.shape[0] == 1, the area between the curve and y=0 is filled transparently, + If a.ndim == 4 and a.shape[0] == 3, a[0,:,:,:] is plotted normally and the area between a[1,:,:,:] and + a[2,:,:,:] is filled transparently. + fs : float + Sampling frequency + freq_range : (float, float) + Frequency range to plot + diagonal : {-1, 0, 1} + If diagonal == -1 nothing is plotted on the diagonal (a[i,i,:] are not plotted), + if diagonal == 0, a is plotted on the diagonal too (all a[i,i,:] are plotted), + if diagonal == 1, a is plotted on the diagonal only (only a[i,i,:] are plotted) + border : bool + If border == true the leftmost column and the topmost row are left blank + fig : Figure object, optional + Figure to plot into. If set to `None`, a new figure is created. + + Returns + ------- + fig : Figure object + The figure into which was plotted. + """ + + a = np.atleast_3d(a) + if a.ndim == 3: + [_, m, f] = a.shape + l = 0 + else: + [l, _, m, f] = a.shape + freq = np.linspace(0, fs / 2, f) + + lowest, highest = np.inf, -np.inf + left = max(freq_range[0], freq[0]) + right = min(freq_range[1], freq[-1]) + + if fig is None: + fig = new_figure() + + axes = [] + for i in range(m): + if diagonal == 1: + jrange = [i] + elif diagonal == 0: + jrange = range(m) + else: + jrange = [j for j in range(m) if j != i] + for j in jrange: + if border: + ax = fig.add_subplot(m+1, m+1, j + (i+1) * (m+1) + 2) + else: + ax = fig.add_subplot(m, m, j + i * m + 1) + axes.append((i, j, ax)) + if l == 0: + ax.plot(freq, a[i, j, :]) + lowest = min(lowest, np.min(a[i, j, :])) + highest = max(highest, np.max(a[i, j, :])) + elif l == 1: + ax.fill_between(freq, 0, a[0, i, j, :], facecolor=[0.25, 0.25, 0.25], alpha=0.25) + lowest = min(lowest, np.min(a[0, i, j, :])) + highest = max(highest, np.max(a[0, i, j, :])) + else: + baseline, = ax.plot(freq, a[0, i, j, :]) + ax.fill_between(freq, a[1, i, j, :], a[2, i, j, :], facecolor=baseline.get_color(), alpha=0.25) + lowest = min(lowest, np.min(a[:, i, j, :])) + highest = max(highest, np.max(a[:, i, j, :])) + + for i, j, ax in axes: + ax.xaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) + ax.yaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) + al = ax.get_ylim() + ax.set_ylim(min(al[0], lowest), max(al[1], highest)) + ax.set_xlim(left, right) + + if 0 < i < m - 1: + ax.set_xticks([]) + if 0 < j < m - 1: + ax.set_yticks([]) + + if i == 0: + ax.xaxis.tick_top() + if i == m-1: + ax.xaxis.tick_bottom() + + if j == 0: + ax.yaxis.tick_left() + if j == m-1: + ax.yaxis.tick_right() + + _plot_labels(fig, + {'x': 0.5, 'y': 0.025, 's': 'frequency (Hz)', 'horizontalalignment': 'center'}, + {'x': 0.05, 'y': 0.5, 's': 'magnitude', 'horizontalalignment': 'center', 'rotation': 'vertical'}) + + return fig + + +def plot_connectivity_significance(s, fs=2, freq_range=(-np.inf, np.inf), diagonal=0, border=False, fig=None): + """ Plot significance. + + Significance is drawn as a background image where dark vertical stripes indicate freuquencies where a evaluates to + True. + + Parameters + ---------- + a : array, dtype=bool, shape = [n_channels, n_channels, n_fft] + Significance + fs : float + Sampling frequency + freq_range : (float, float) + Frequency range to plot + diagonal : {-1, 0, 1} + If diagonal == -1 nothing is plotted on the diagonal (a[i,i,:] are not plotted), + if diagonal == 0, a is plotted on the diagonal too (all a[i,i,:] are plotted), + if diagonal == 1, a is plotted on the diagonal only (only a[i,i,:] are plotted) + border : bool + If border == true the leftmost column and the topmost row are left blank + fig : Figure object, optional + Figure to plot into. If set to `None`, a new figure is created. + + Returns + ------- + fig : Figure object + The figure into which was plotted. + """ + + a = np.atleast_3d(s) + [_, m, f] = a.shape + freq = np.linspace(0, fs / 2, f) + + left = max(freq_range[0], freq[0]) + right = min(freq_range[1], freq[-1]) + + imext = (freq[0], freq[-1], -1e25, 1e25) + + if fig is None: + fig = new_figure() + + axes = [] + for i in range(m): + if diagonal == 1: + jrange = [i] + elif diagonal == 0: + jrange = range(m) + else: + jrange = [j for j in range(m) if j != i] + for j in jrange: + if border: + ax = fig.add_subplot(m+1, m+1, j + (i+1) * (m+1) + 2) + else: + ax = fig.add_subplot(m, m, j + i * m + 1) + axes.append((i, j, ax)) + ax.imshow(s[i, j, np.newaxis], vmin=0, vmax=2, cmap='binary', aspect='auto', extent=imext, zorder=-999) + + ax.xaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) + ax.yaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) + ax.set_xlim(left, right) + + if 0 < i < m - 1: + ax.set_xticks([]) + if 0 < j < m - 1: + ax.set_yticks([]) + + if j == 0: + ax.yaxis.tick_left() + if j == m-1: + ax.yaxis.tick_right() + + _plot_labels(fig, + {'x': 0.5, 'y': 0.025, 's': 'frequency (Hz)', 'horizontalalignment': 'center'}, + {'x': 0.05, 'y': 0.5, 's': 'magnitude', 'horizontalalignment': 'center', 'rotation': 'vertical'}) + + return fig + + +def plot_connectivity_timespectrum(a, fs=2, crange=None, freq_range=(-np.inf, np.inf), time_range=None, diagonal=0, border=False, fig=None): + """ Draw time/frequency connectivity plots. + + Parameters + ---------- + a : array, shape = [n_channels, n_channels, n_fft, n_timesteps] + Values to draw + fs : float + Sampling frequency + crange : [int, int], optional + Range of values covered by the colormap. + If set to None, [min(a), max(a)] is substituted. + freq_range : (float, float) + Frequency range to plot + time_range : (float, float) + Time range covered by `a` + diagonal : {-1, 0, 1} + If diagonal == -1 nothing is plotted on the diagonal (a[i,i,:] are not plotted), + if diagonal == 0, a is plotted on the diagonal too (all a[i,i,:] are plotted), + if diagonal == 1, a is plotted on the diagonal only (only a[i,i,:] are plotted) + border : bool + If border == true the leftmost column and the topmost row are left blank + fig : Figure object, optional + Figure to plot into. If set to `None`, a new figure is created. + + Returns + ------- + fig : Figure object + The figure into which was plotted. + """ + a = np.asarray(a) + [_, m, _, t] = a.shape + + if crange is None: + crange = [np.min(a), np.max(a)] + + if time_range is None: + t0 = 0 + t1 = t + else: + t0, t1 = time_range + + f0, f1 = fs / 2, 0 + extent = [t0, t1, f0, f1] + + ymin = max(freq_range[0], f1) + ymax = min(freq_range[1], f0) + + if fig is None: + fig = new_figure() + + axes = [] + for i in range(m): + if diagonal == 1: + jrange = [i] + elif diagonal == 0: + jrange = range(m) + else: + jrange = [j for j in range(m) if j != i] + for j in jrange: + if border: + ax = fig.add_subplot(m+1, m+1, j + (i+1) * (m+1) + 2) + else: + ax = fig.add_subplot(m, m, j + i * m + 1) + axes.append(ax) + ax.imshow(a[i, j, :, :], vmin=crange[0], vmax=crange[1], aspect='auto', extent=extent) + ax.invert_yaxis() + + ax.xaxis.set_major_locator(MaxNLocator(max(1, 9 - m))) + ax.yaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) + ax.set_ylim(ymin, ymax) + + if 0 < i < m - 1: + ax.set_xticks([]) + if 0 < j < m - 1: + ax.set_yticks([]) + + if i == 0: + ax.xaxis.tick_top() + if i == m-1: + ax.xaxis.tick_bottom() + + if j == 0: + ax.yaxis.tick_left() + if j == m-1: + ax.yaxis.tick_right() + + _plot_labels(fig, + {'x': 0.5, 'y': 0.025, 's': 'time (s)', 'horizontalalignment': 'center'}, + {'x': 0.05, 'y': 0.5, 's': 'frequency (Hz)', 'horizontalalignment': 'center', 'rotation': 'vertical'}) + + return fig + + +def plot_circular(widths, colors, curviness=0.2, mask=True, topo=None, topomaps=None, axes=None, order=None): + """ Circluar connectivity plot + + Topos are arranged in a circle, with arrows indicating connectivity + + Parameters + ---------- + widths : {float or array, shape = [n_channels, n_channels]} + Width of each arrow. Can be a scalar to assign the same width to all arrows. + colors : array, shape = [n_channels, n_channels, 3] or [3] + RGB color values for each arrow or one RGB color value for all arrows. + curviness : float, optional + Factor that determines how much arrows tend to deviate from a straight line. + mask : array, dtype = bool, shape = [n_channels, n_channels] + Enable or disable individual arrows + topo : :class:`~eegtopo.topoplot.Topoplot` + This object draws the topo plot + topomaps : array, shape = [w_pixels, h_pixels] + Scalp-projected map + axes : axis, optional + Axis to draw into. A new figure is created by default. + order : list of int + Rearrange channels. + + Returns + ------- + axes : Axes object + The axes into which was plotted. + """ + colors = np.asarray(colors) + widths = np.asarray(widths) + mask = np.asarray(mask) + + colors = np.maximum(colors, 0) + colors = np.minimum(colors, 1) + + if len(widths.shape) > 2: + [n, m] = widths.shape + elif len(colors.shape) > 3: + [n, m, c] = widths.shape + elif len(mask.shape) > 2: + [n, m] = mask.shape + else: + n = len(topomaps) + m = n + + if not order: + order = list(range(n)) + + #a = np.asarray(a) + #[n, m] = a.shape + + assert(n == m) + + if axes is None: + fig = new_figure() + axes = fig.add_subplot(111) + axes.set_yticks([]) + axes.set_xticks([]) + axes.set_frame_on(False) + + if len(colors.shape) < 3: + colors = np.tile(colors, (n,n,1)) + + if len(widths.shape) < 2: + widths = np.tile(widths, (n,n)) + + if len(mask.shape) < 2: + mask = np.tile(mask, (n,n)) + np.fill_diagonal(mask, False) + + if topo: + alpha = 1.5 if n < 10 else 1.25 + r = alpha * topo.head_radius / (np.sin(np.pi/n)) + else: + r = 1 + + for i in range(n): + if topo: + o = (r*np.sin(i*2*np.pi/n), r*np.cos(i*2*np.pi/n)) + plot_topo(axes, topo, topomaps[order[i]], offset=o) + + for i in range(n): + for j in range(n): + if not mask[order[i], order[j]]: + continue + a0 = j*2*np.pi/n + a1 = i*2*np.pi/n + + x0, y0 = r*np.sin(a0), r*np.cos(a0) + x1, y1 = r*np.sin(a1), r*np.cos(a1) + + ex = (x0 + x1) / 2 + ey = (y0 + y1) / 2 + en = np.sqrt(ex**2 + ey**2) + + if en < 1e-10: + en = 0 + ex = y0 / r + ey = -x0 / r + w = -r + else: + ex /= en + ey /= en + w = np.sqrt((x1-x0)**2 + (y1-y0)**2) / 2 + + if x0*y1-y0*x1 < 0: + w = -w + + d = en*(1-curviness) + h = en-d + + t = np.linspace(-1, 1, 100) + + dist = (t**2+2*t+1)*w**2 + (t**4-2*t**2+1)*h**2 + + tmask1 = dist >= (1.4*topo.head_radius)**2 + tmask2 = dist >= (1.2*topo.head_radius)**2 + tmask = np.logical_and(tmask1, tmask2[::-1]) + t = t[tmask] + + x = (h*t*t+d)*ex - w*t*ey + y = (h*t*t+d)*ey + w*t*ex + + # Arrow Head + s = np.sqrt((x[-2] - x[-1])**2 + (y[-2] - y[-1])**2) + + width = widths[order[i], order[j]] + + x1 = 0.1*width*(x[-2] - x[-1] + y[-2] - y[-1])/s + x[-1] + y1 = 0.1*width*(y[-2] - y[-1] - x[-2] + x[-1])/s + y[-1] + + x2 = 0.1*width*(x[-2] - x[-1] - y[-2] + y[-1])/s + x[-1] + y2 = 0.1*width*(y[-2] - y[-1] + x[-2] - x[-1])/s + y[-1] + + x = np.concatenate([x, [x1, x[-1], x2]]) + y = np.concatenate([y, [y1, y[-1], y2]]) + axes.plot(x, y, lw=width, color=colors[order[i], order[j]], solid_capstyle='round', solid_joinstyle='round') + + return axes + + +def plot_whiteness(var, h, repeats=1000, axis=None): + """ Draw distribution of the Portmanteu whiteness test. + + Parameters + ---------- + var : :class:`~scot.var.VARBase`-like object + Vector autoregressive model (VAR) object whose residuals are tested for whiteness. + h : int + Maximum lag to include in the test. + repeats : int, optional + Number of surrogate estimates to draw under the null hypothesis. + axis : axis, optional + Axis to draw into. By default draws into :func:`matplotlib.pyplot.gca()`. + + Returns + ------- + pr : float + *p*-value of whiteness under the null hypothesis + """ + pr, q0, q = var.test_whiteness(h, repeats, True) + + if axis is None: + axis = current_axis() + + pdf, _, _ = axis.hist(q0, 30, normed=True, label='surrogate distribution') + axis.plot([q,q], [0,np.max(pdf)], 'r-', label='fitted model') + + #df = m*m*(h-p) + #x = np.linspace(np.min(q0)*0.0, np.max(q0)*2.0, 100) + #y = sp.stats.chi2.pdf(x, df) + #hc = axis.plot(x, y, label='chi-squared distribution (df=%i)' % df) + + axis.set_title('significance: p = %f'%pr) + axis.set_xlabel('Li-McLeod statistic (Q)') + axis.set_ylabel('probability') + + axis.legend() + + return pr + + +def _plot_labels(target, *labels): + for l in labels: + have_label = False + for child in target.get_children(): + try: + if child.get_text() == l['s'] and child.get_position() == (l['x'], l['y']): + have_label = True + break + except AttributeError: + pass + if not have_label: + target.text(**l) diff --git a/mne_sandbox/externals/scot/utils.py b/mne_sandbox/externals/scot/utils.py new file mode 100644 index 0000000..8e924b0 --- /dev/null +++ b/mne_sandbox/externals/scot/utils.py @@ -0,0 +1,203 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Utility functions """ + +from __future__ import division + +import numpy as np + +from functools import partial + + +def cuthill_mckee(matrix): + """ Cuthill-McKee algorithm + + Permute a symmetric binary matrix into a band matrix form with a small bandwidth. + + Parameters + ---------- + matrix : ndarray, dtype=bool, shape = [n, n] + The matrix is internally converted to a symmetric matrix by setting each element [i,j] to True if either + [i,j] or [j,i] evaluates to true. + + Returns + ------- + order : list of int + Permutation intices + + Examples + -------- + >>> A = np.array([[0,0,1,1], [0,0,0,0], [1,0,1,0], [1,0,0,0]]) + >>> p = cuthill_mckee(A) + >>> A + array([[0, 0, 1, 1], + [0, 0, 0, 0], + [1, 0, 1, 0], + [1, 0, 0, 0]]) + >>> A[p,:][:,p] + array([[0, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 1], + [0, 0, 1, 1]]) + """ + matrix = np.atleast_2d(matrix) + n, m = matrix.shape + assert(n == m) + + # make sure the matrix is really symmetric. This is equivalent to + # converting a directed adjacency matrix into a undirected adjacency matrix. + matrix = np.logical_or(matrix, matrix.T) + + degree = np.sum(matrix, 0) + order = [np.argmin(degree)] + + for i in range(n): + adj = np.nonzero(matrix[order[i]])[0] + adj = [a for a in adj if a not in order] + if not adj: + idx = [i for i in range(n) if i not in order] + order.append(idx[np.argmin(degree[idx])]) + else: + if len(adj) == 1: + order.append(adj[0]) + else: + adj = np.asarray(adj) + i = adj[np.argsort(degree[adj])] + order.extend(i.tolist()) + if len(order) == n: + break + + return order + + +def acm(x, l): + """ Autocovariance matrix at lag l + + This function calculates the autocovariance matrix of `x` at lag `l`. + + Parameters + ---------- + x : ndarray, shape = [n_samples, n_channels, (n_trials)] + Signal data (2D or 3D for multiple trials) + l : int + Lag + + Returns + ------- + c : ndarray, shape = [nchannels, n_channels] + Autocovariance matrix of `x` at lag `l`. + """ + x = np.atleast_3d(x) + + if l > x.shape[0]-1: + raise AttributeError("lag exceeds data length") + + ## subtract mean from each trial + #for t in range(x.shape[2]): + # x[:, :, t] -= np.mean(x[:, :, t], axis=0) + + if l == 0: + a, b = x, x + else: + a = x[l:, :, :] + b = x[0:-l, :, :] + + c = np.zeros((x.shape[1], x.shape[1])) + for t in range(x.shape[2]): + c += a[:, :, t].T.dot(b[:, :, t]) / x.shape[0] + c /= x.shape[2] + + return c + + +#noinspection PyPep8Naming +class memoize(object): + """cache the return value of a method + + This class is meant to be used as a decorator of methods. The return value + from a given method invocation will be cached on the instance whose method + was invoked. All arguments passed to a method decorated with memoize must + be hashable. + + If a memoized method is invoked directly on its class the result will not + be cached. Instead the method will be invoked like a static method: + """ + + def __init__(self, func): + self.func = func + + #noinspection PyUnusedLocal + def __get__(self, obj, objtype=None): + if obj is None: + return self.func + return partial(self, obj) + + def __call__(self, *args, **kw): + obj = args[0] + try: + cache = obj.__cache + except AttributeError: + cache = obj.__cache = {} + key = (self.func, args[1:], frozenset(kw.items())) + try: + res = cache[key] + except KeyError: + res = cache[key] = self.func(*args, **kw) + return res + + +def cartesian(arrays, out=None): + """ + Generate a cartesian product of input arrays. + + Parameters + ---------- + arrays : list of array-like + 1-D arrays to form the cartesian product of. + out : ndarray + Array to place the cartesian product in. + + Returns + ------- + out : ndarray + 2-D array of shape (M, len(arrays)) containing cartesian products + formed of input arrays. + + Examples + -------- + >>> cartesian(([1, 2, 3], [4, 5], [6, 7])) + array([[1, 4, 6], + [1, 4, 7], + [1, 5, 6], + [1, 5, 7], + [2, 4, 6], + [2, 4, 7], + [2, 5, 6], + [2, 5, 7], + [3, 4, 6], + [3, 4, 7], + [3, 5, 6], + [3, 5, 7]]) + + References + ---------- + http://stackoverflow.com/a/1235363/3005167 + + """ + + arrays = [np.asarray(x) for x in arrays] + dtype = arrays[0].dtype + + n = np.prod([x.size for x in arrays]) + if out is None: + out = np.zeros([n, len(arrays)], dtype=dtype) + + m = n // arrays[0].size + out[:, 0] = np.repeat(arrays[0], m) + if arrays[1:]: + cartesian(arrays[1:], out=out[0:m, 1:]) + for j in range(1, arrays[0].size): + out[j * m: (j + 1) * m, 1:] = out[0:m, 1:] + return out \ No newline at end of file diff --git a/mne_sandbox/externals/scot/var.py b/mne_sandbox/externals/scot/var.py new file mode 100644 index 0000000..e15e914 --- /dev/null +++ b/mne_sandbox/externals/scot/var.py @@ -0,0 +1,316 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Vector autoregressive (VAR) model implementation +""" + +from __future__ import print_function + +import numpy as np +import scipy as sp +from .varbase import VARBase, _construct_var_eqns +from .datatools import cat_trials +from . import xvschema as xv +from .parallel import parallel_loop + + +class VAR(VARBase): + """ Builtin implementation of VARBase. + + This class provides least squares VAR model fitting with optional ridge + regression. + + Parameters + ---------- + model_order : int + Autoregressive model order + delta : float, optional + Ridge penalty parameter + xvschema : func, optional + Function that creates training and test sets for cross-validation. The + function takes two parameters: the current cross-validation run (int) + and the numer of trials (int). It returns a tuple of two arrays: the + training set and the testing set. + """ + def __init__(self, model_order, delta=0, xvschema=xv.multitrial): + VARBase.__init__(self, model_order) + self.delta = delta + self.xvschema = xvschema + + def fit(self, data): + """ Fit VAR model to data. + + Parameters + ---------- + data : array-like + shape = [n_samples, n_channels, n_trials] or + [n_samples, n_channels] + Continuous or segmented data set. + + Returns + ------- + self : :class:`VAR` + The :class:`VAR` object to facilitate method chaining (see usage + example) + """ + data = sp.atleast_3d(data) + + if self.delta == 0 or self.delta is None: + # ordinary least squares + (x, y) = self._construct_eqns(data) + else: + # regularized least squares (ridge regression) + (x, y) = self._construct_eqns_rls(data) + + (b, res, rank, s) = sp.linalg.lstsq(x, y) + + self.coef = b.transpose() + + self.residuals = data - self.predict(data) + self.rescov = sp.cov(cat_trials(self.residuals[self.p:, :, :]), + rowvar=False) + + return self + + def optimize_order(self, data, min_p=1, max_p=None, n_jobs=1, verbose=0): + """ Determine optimal model order by cross-validating the mean-squared + generalization error. + + Parameters + ---------- + data : array-like + shape = [n_samples, n_channels, n_trials] or + [n_samples, n_channels] + Continuous or segmented data set on which to optimize the model + order. + min_p : int + minimal model order to check + max_p : int + maximum model order to check + n_jobs : int | None + number of jobs to run in parallel. See `joblib.Parallel` for + details. + verbose : int + verbosity level passed to joblib. + """ + data = np.asarray(data) + assert (data.shape[2] > 1) + msge, prange = [], [] + + par, func = parallel_loop(_get_msge_with_gradient, + n_jobs=n_jobs, verbose=verbose) + if not n_jobs: + npar = 1 + elif n_jobs < 0: + npar = 4 # is this a sane default? + else: + npar = n_jobs + + p = min_p + while True: + result = par(func(data, self.delta, self.xvschema, 1, p_) + for p_ in range(p, p + npar)) + j, k = zip(*result) + prange.extend(range(p, p + npar)) + msge.extend(j) + p += npar + if max_p is None: + if len(msge) >= 2 and msge[-1] > msge[-2]: + break + else: + if prange[-1] >= max_p: + break + self.p = prange[np.argmin(msge)] + return zip(prange, msge) + + def optimize_delta_bisection(self, data, skipstep=1): + """ Find optimal ridge penalty with bisection search. + + Parameters + ---------- + data : array-like + shape = [n_samples, n_channels, n_trials] or + [n_samples, n_channels] + Continuous or segmented data set. + skipstep : int, optional + Speed up calculation by skipping samples during cost function + calculation + + Returns + ------- + self : :class:`VAR` + The :class:`VAR` object to facilitate method chaining (see usage + example) + """ + data = sp.atleast_3d(data) + (l, m, t) = data.shape + assert (t > 1) + + maxsteps = 10 + maxdelta = 1e50 + + a = -10 + b = 10 + + trform = lambda x: sp.sqrt(sp.exp(x)) + + msge = _get_msge_with_gradient_func(data.shape, self.p) + + (ja, ka) = msge(data, trform(a), self.xvschema, skipstep, self.p) + (jb, kb) = msge(data, trform(b), self.xvschema, skipstep, self.p) + + # before starting the real bisection, assure the interval contains 0 + while sp.sign(ka) == sp.sign(kb): + print('Bisection initial interval (%f,%f) does not contain zero. ' + 'New interval: (%f,%f)' % (a, b, a * 2, b * 2)) + a *= 2 + b *= 2 + (ja, ka) = msge(data, trform(a), self.xvschema, skipstep, self.p) + (jb, kb) = msge(data, trform(b), self.xvschema, skipstep, self.p) + + if trform(b) >= maxdelta: + print('Bisection: could not find initial interval.') + print(' ********* Delta set to zero! ************ ') + return 0 + + nsteps = 0 + + while nsteps < maxsteps: + + # point where the line between a and b crosses zero + # this is not very stable! + #c = a + (b-a) * np.abs(ka) / np.abs(kb-ka) + c = (a + b) / 2 + (j, k) = msge(data, trform(c), self.xvschema, skipstep, self.p) + if sp.sign(k) == sp.sign(ka): + a, ka = c, k + else: + b, kb = c, k + + nsteps += 1 + tmp = trform([a, b, a + (b - a) * np.abs(ka) / np.abs(kb - ka)]) + print('%d Bisection Interval: %f - %f, (projected: %f)' % + (nsteps, tmp[0], tmp[1], tmp[2])) + + self.delta = trform(a + (b - a) * np.abs(ka) / np.abs(kb - ka)) + print('Final point: %f' % self.delta) + return self + + optimize = optimize_delta_bisection + + def _construct_eqns_rls(self, data): + """Construct VAR equation system with RLS constraint. + """ + return _construct_var_eqns_rls(data, self.p, self.delta) + + +def _msge_with_gradient_underdetermined(data, delta, xvschema, skipstep, p): + """ Calculate the mean squared generalization error and it's gradient for + underdetermined equation system. + """ + (l, m, t) = data.shape + d = None + j, k = 0, 0 + nt = sp.ceil(t / skipstep) + for trainset, testset in xvschema(t, skipstep): + + (a, b) = _construct_var_eqns(sp.atleast_3d(data[:, :, trainset]), p) + (c, d) = _construct_var_eqns(sp.atleast_3d(data[:, :, testset]), p) + + e = sp.linalg.inv(sp.eye(a.shape[0]) * delta ** 2 + + a.dot(a.transpose())) + + cc = c.transpose().dot(c) + + be = b.transpose().dot(e) + bee = be.dot(e) + bea = be.dot(a) + beea = bee.dot(a) + beacc = bea.dot(cc) + dc = d.transpose().dot(c) + + j += sp.sum(beacc * bea - 2 * bea * dc) + sp.sum(d ** 2) + k += sp.sum(beea * dc - beacc * beea) * 4 * delta + + return j / (nt * d.size), k / (nt * d.size) + + +def _msge_with_gradient_overdetermined(data, delta, xvschema, skipstep, p): + """ Calculate the mean squared generalization error and it's gradient for + overdetermined equation system. + """ + (l, m, t) = data.shape + d = None + l, k = 0, 0 + nt = sp.ceil(t / skipstep) + for trainset, testset in xvschema(t, skipstep): + + (a, b) = _construct_var_eqns(sp.atleast_3d(data[:, :, trainset]), p) + (c, d) = _construct_var_eqns(sp.atleast_3d(data[:, :, testset]), p) + + e = sp.linalg.inv(sp.eye(a.shape[1]) * delta ** 2 + + a.transpose().dot(a)) + + ba = b.transpose().dot(a) + dc = d.transpose().dot(c) + bae = ba.dot(e) + baee = bae.dot(e) + baecc = bae.dot(c.transpose().dot(c)) + + l += sp.sum(baecc * bae - 2 * bae * dc) + sp.sum(d ** 2) + k += sp.sum(baee * dc - baecc * baee) * 4 * delta + + return l / (nt * d.size), k / (nt * d.size) + + +def _get_msge_with_gradient_func(shape, p): + """ Select which function to use for MSGE calculation (over- or + underdetermined). + """ + (l, m, t) = shape + + n = (l - p) * t + underdetermined = n < m * p + + if underdetermined: + return _msge_with_gradient_underdetermined + else: + return _msge_with_gradient_overdetermined + + +def _get_msge_with_gradient(data, delta, xvschema, skipstep, p): + """ Calculate the mean squared generalization error and it's gradient, + automatically selecting the best function. + """ + (l, m, t) = data.shape + + n = (l - p) * t + underdetermined = n < m * p + + if underdetermined: + return _msge_with_gradient_underdetermined(data, delta, xvschema, + skipstep, p) + else: + return _msge_with_gradient_overdetermined(data, delta, xvschema, + skipstep, p) + + +def _construct_var_eqns_rls(data, p, delta): + """Construct VAR equation system with RLS constraint. + """ + (l, m, t) = sp.shape(data) + n = (l - p) * t # number of linear relations + # Construct matrix x (predictor variables) + x = sp.zeros((n + m * p, m * p)) + for i in range(m): + for k in range(1, p + 1): + x[:n, i * p + k - 1] = sp.reshape(data[p - k:-k, i, :], n) + sp.fill_diagonal(x[n:, :], delta) + + # Construct vectors yi (response variables for each channel i) + y = sp.zeros((n + m * p, m)) + for i in range(m): + y[:n, i] = sp.reshape(data[p:, i, :], n) + + return x, y \ No newline at end of file diff --git a/mne_sandbox/externals/scot/varbase.py b/mne_sandbox/externals/scot/varbase.py new file mode 100644 index 0000000..f01e236 --- /dev/null +++ b/mne_sandbox/externals/scot/varbase.py @@ -0,0 +1,472 @@ +# coding=utf-8 + +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013-2014 SCoT Development Team + +""" vector autoregressive (VAR) model """ + +from __future__ import division + +import numbers +from functools import partial + +import numpy as np +import scipy as sp + +from . import datatools +from . import xvschema as xv +from .utils import acm +from .datatools import cat_trials + + +class Defaults: + xvschema = xv.multitrial + + +class VARBase(): + """ Represents a vector autoregressive (VAR) model. + + .. warning:: `VARBase` is an abstract class that defines the interface for + VAR model implementations. Several methods must be implemented by derived + classes. + + Parameters + ---------- + model_order : int + Autoregressive model order + + Notes + ----- + Note on the arrangement of model coefficients: + *b* is of shape [m, m*p], with sub matrices arranged as follows: + + +------+------+------+------+ + | b_00 | b_01 | ... | b_0m | + +------+------+------+------+ + | b_10 | b_11 | ... | b_1m | + +------+------+------+------+ + | ... | ... | ... | ... | + +------+------+------+------+ + | b_m0 | b_m1 | ... | b_mm | + +------+------+------+------+ + + Each sub matrix b_ij is a column vector of length p that contains the + filter coefficients from channel j (source) to channel i (sink). + """ + + def __init__(self, model_order): + self.p = model_order + self.coef = None + self.residuals = None + self.rescov = None + + def copy(self): + """ Create a copy of the VAR model.""" + other = self.__class__(self.p) + other.coef = self.coef.copy() + other.residuals = self.residuals.copy() + other.rescov = self.rescov.copy() + return other + + def fit(self, data): + """ Fit VAR model to data. + + .. warning:: This function must be implemented by derived classes. + + Parameters + ---------- + data : array-like + shape = [n_samples, n_channels, n_trials] or + [n_samples, n_channels] + Continuous or segmented data set. + + Returns + ------- + self : :class:`VAR` + The :class:`VAR` object to facilitate method chaining (see usage + example) + """ + raise NotImplementedError('method fit() is not implemented in ' + + str(self)) + + def optimize(self, data): + """ Optimize model fitting hyperparameters (such as regularization + penalty) + + .. warning:: This function must be implemented by derived classes. + + Parameters + ---------- + data : array-like, shape = [n_samples, n_channels, n_trials] or + [n_samples, n_channels] + Continuous or segmented data set. + """ + raise NotImplementedError('method optimize() is not implemented in ' + + str(self)) + return self + + def from_yw(self, acms): + """ Determine VAR model from autocorrelation matrices by solving the + Yule-Walker equations. + + Parameters + ---------- + acms : array-like, shape = [n_lags, n_channels, n_channels] + acms[l] contains the autocorrelation matrix at lag l. The highest + lag must equal the model order. + + Returns + ------- + self : :class:`VAR` + The :class:`VAR` object to facilitate method chaining (see usage + example) + """ + assert(len(acms) == self.p + 1) + + n_channels = acms[0].shape[0] + + acm = lambda l: acms[l] if l >= 0 else acms[-l].T + + r = np.concatenate(acms[1:], 0) + + rr = np.array([[acm(m-k) for k in range(self.p)] + for m in range(self.p)]) + rr = np.concatenate(np.concatenate(rr, -2), -1) + + c = sp.linalg.solve(rr, r) + + # calculate residual covariance + r = acm(0) + for k in range(self.p): + bs = k * n_channels + r -= np.dot(c[bs:bs + n_channels, :].T, acm(k + 1)) + + self.coef = np.concatenate([c[m::n_channels, :] + for m in range(n_channels)]).T + self.rescov = r + + return self + + def simulate(self, l, noisefunc=None): + """ Simulate vector autoregressive (VAR) model + + This function generates data from the VAR model. + + Parameters + ---------- + l : {int, [int, int]} + Specify number of samples to generate. Can be a tuple or list + where l[0] is the number of samples and l[1] is the number of + trials. + noisefunc : func, optional + This function is used to create the generating noise process. + If set to None Gaussian white noise with zero mean and unit + variance is used. + + Returns + ------- + data : array, shape = [n_samples, n_channels, n_trials] + """ + (m, n) = sp.shape(self.coef) + p = n // m + + try: + (l, t) = l + except TypeError: + t = 1 + + if noisefunc is None: + noisefunc = lambda: sp.random.normal(size=(1, m)) + + n = l + 10 * p + + y = sp.zeros((n, m, t)) + res = sp.zeros((n, m, t)) + + for s in range(t): + for i in range(p): + e = noisefunc() + res[i, :, s] = e + y[i, :, s] = e + for i in range(p, n): + e = noisefunc() + res[i, :, s] = e + y[i, :, s] = e + for k in range(1, p + 1): + y[i, :, s] += self.coef[:, (k - 1)::p].dot(y[i - k, :, s]) + + self.residuals = res[10 * p:, :, :] + self.rescov = sp.cov(cat_trials(self.residuals), rowvar=False) + + return y[10 * p:, :, :] + + def predict(self, data): + """ Predict samples on actual data. + + The result of this function is used for calculating the residuals. + + Parameters + ---------- + data : array-like + shape = [n_samples, n_channels, n_trials] or + [n_samples, n_channels] + Continuous or segmented data set. + + Returns + ------- + predicted : shape = `data`.shape + Data as predicted by the VAR model. + + Notes + ----- + Residuals are obtained by r = x - var.predict(x) + """ + data = sp.atleast_3d(data) + (l, m, t) = data.shape + + p = int(sp.shape(self.coef)[1] / m) + + y = sp.zeros(data.shape) + if t > l-p: # which takes less loop iterations + for k in range(1, p + 1): + bp = self.coef[:, (k - 1)::p] + for n in range(p, l): + y[n, :, :] += bp.dot(data[n - k, :, :]) + else: + for k in range(1, p + 1): + bp = self.coef[:, (k - 1)::p] + for s in range(t): + y[p:, :, s] += data[(p - k):(l - k), :, s].dot(bp.T) + + return y + + def is_stable(self): + """ Test if the VAR model is stable. + + This function tests stability of the VAR model as described in [1]_. + + Returns + ------- + out : bool + True if the model is stable. + + References + ---------- + .. [1] H. Lütkepohl, "New Introduction to Multiple Time Series + Analysis", 2005, Springer, Berlin, Germany + """ + m, mp = self.coef.shape + p = mp // m + assert(mp == m * p) + + top_block = [] + for i in range(p): + top_block.append(self.coef[:, i::p]) + top_block = np.hstack(top_block) + + im = np.eye(m) + eye_block = im + for i in range(p-2): + eye_block = sp.linalg.block_diag(im, eye_block) + eye_block = np.hstack([eye_block, np.zeros((m * (p - 1), m))]) + + tmp = np.vstack([top_block, eye_block]) + + return np.all(np.abs(np.linalg.eig(tmp)[0]) < 1) + + def test_whiteness(self, h, repeats=100, get_q=False): + """ Test if the VAR model residuals are white (uncorrelated up to a lag + of h). + + This function calculates the Li-McLeod as Portmanteau test statistic Q + to test against the null hypothesis H0: "the residuals are white" [1]_. + Surrogate data for H0 is created by sampling from random permutations + of the residuals. + + Usually the returned p-value is compared against a pre-defined type 1 + error level of alpha=0.05 or alpha=0.01. If p<=alpha, the hypothesis of + white residuals is rejected, which indicates that the VAR model does + not properly describe the data. + + Parameters + ---------- + h : int + Maximum lag that is included in the test statistic. + repeats : int, optional + Number of samples to create under the null hypothesis. + get_q : bool, optional + Return Q statistic along with *p*-value + + Returns + ------- + pr : float + Probability of observing a more extreme value of Q under the + assumption that H0 is true. + q0 : list of float, optional (`get_q`) + Individual surrogate estimates that were used for estimating the + distribution of Q under H0. + q : float, optional (`get_q`) + Value of the Q statistic of the residuals + + Notes + ----- + According to [2]_ h must satisfy h = O(n^0.5), where n is the length + (time samples) of the residuals. + + References + ---------- + .. [1] H. Lütkepohl, "New Introduction to Multiple Time Series + Analysis", 2005, Springer, Berlin, Germany + .. [2] J.R.M. Hosking, "The Multivariate Portmanteau Statistic", 1980, + J. Am. Statist. Assoc. + """ + + return test_whiteness(self.residuals, h, self.p, repeats, get_q) + + def _construct_eqns(self, data): + """ Construct VAR equation system + """ + return _construct_var_eqns(data, self.p) + + +############################################################################ + + +def _construct_var_eqns(data, p): + """ Construct VAR equation system + """ + (l, m, t) = np.shape(data) + n = (l - p) * t # number of linear relations + # Construct matrix x (predictor variables) + x = np.zeros((n, m * p)) + for i in range(m): + for k in range(1, p + 1): + x[:, i * p + k - 1] = np.reshape(data[p - k:-k, i, :], n) + + # Construct vectors yi (response variables for each channel i) + y = np.zeros((n, m)) + for i in range(m): + y[:, i] = np.reshape(data[p:, i, :], n) + + return x, y + + +def test_whiteness(data, h, p=0, repeats=100, get_q=False): + """ Test if signals are white (serially uncorrelated up to a lag of h). + + This function calculates the Li-McLeod as Portmanteau test statistic Q to + test against the null hypothesis H0: "the residuals are white" [1]_. + Surrogate data for H0 is created by sampling from random permutations of + the residuals. + + Usually the returned p-value is compared against a pre-defined type 1 error + level of alpha=0.05 or alpha=0.01. If p<=alpha, the hypothesis of white + residuals is rejected, which indicates that the VAR model does not properly + describe the data. + + Parameters + ---------- + signals : array-like + shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + Continuous or segmented data set. + h : int + Maximum lag that is included in the test statistic. + p : int, optional + Model order if the `signals` are the residuals resulting from fitting a + VAR model + repeats : int, optional + Number of samples to create under the null hypothesis. + get_q : bool, optional + Return Q statistic along with *p*-value + + Returns + ------- + pr : float + Probability of observing a more extreme value of Q under the assumption + that H0 is true. + q0 : list of float, optional (`get_q`) + Individual surrogate estimates that were used for estimating the + distribution of Q under H0. + q : float, optional (`get_q`) + Value of the Q statistic of the residuals + + Notes + ----- + According to [2]_ h must satisfy h = O(n^0.5), where n is the length (time + samples) of the residuals. + + References + ---------- + .. [1] H. Lütkepohl, "New Introduction to Multiple Time Series Analysis", + 2005, Springer, Berlin, Germany + .. [2] J.R.M. Hosking, "The Multivariate Portmanteau Statistic", 1980, J. + Am. Statist. Assoc. + """ + res = data[p:, :, :] + (n, m, t) = res.shape + nt = (n - p) * t + + q0 = _calc_q_h0(repeats, res, h, nt)[:, 2, -1] + q = _calc_q_statistic(res, h, nt)[2, -1] + + # probability of observing a result more extreme than q + # under the null-hypothesis + pr = np.sum(q0 >= q) / repeats + + if get_q: + return pr, q0, q + else: + return pr + + +def _calc_q_statistic(x, h, nt): + """ Calculate portmanteau statistics up to a lag of h. + """ + (n, m, t) = x.shape + + # covariance matrix of x + c0 = acm(x, 0) + + # LU factorization of covariance matrix + c0f = sp.linalg.lu_factor(c0, overwrite_a=False, check_finite=True) + + q = np.zeros((3, h + 1)) + for l in range(1, h + 1): + cl = acm(x, l) + + # calculate tr(cl' * c0^-1 * cl * c0^-1) + a = sp.linalg.lu_solve(c0f, cl) + b = sp.linalg.lu_solve(c0f, cl.T) + tmp = a.dot(b).trace() + + # Box-Pierce + q[0, l] = tmp + + # Ljung-Box + q[1, l] = tmp / (nt - l) + + # Li-McLeod + q[2, l] = tmp + + q *= nt + q[1, :] *= (nt+2) + + q = np.cumsum(q, axis=1) + + for l in range(1, h+1): + q[2, l] = q[0, l] + m*m*l * (l + 1) / (2 * nt) + + return q + + +def _calc_q_h0(n, x, h, nt): + """ Calculate q under the null-hypothesis of whiteness. + """ + x = x.copy() + + q = [] + for i in range(n): + np.random.shuffle(x) # shuffle along time axis + q.append(_calc_q_statistic(x, h, nt)) + return np.array(q) diff --git a/mne_sandbox/externals/scot/varica.py b/mne_sandbox/externals/scot/varica.py new file mode 100644 index 0000000..b1633f0 --- /dev/null +++ b/mne_sandbox/externals/scot/varica.py @@ -0,0 +1,258 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +import numpy as np + +from . import config +from .datatools import cat_trials, dot_special +from . import xvschema + + +def mvarica(x, var, cl=None, reducedim=0.99, optimize_var=False, backend=None, varfit='ensemble'): + """ Performs joint VAR model fitting and ICA source separation. + + This function implements the MVARICA procedure [1]_. + + Parameters + ---------- + x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + data set + var : :class:`~scot.var.VARBase`-like object + Vector autoregressive model (VAR) object that is used for model fitting. + cl : list of valid dict keys, optional + Class labels associated with each trial. + reducedim : {int, float, 'no_pca'}, optional + A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All + components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. + An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. + If set to 'no_pca' the PCA step is skipped. + optimize_var : bool, optional + Whether to call automatic optimization of the VAR fitting routine. + backend : dict-like, optional + Specify backend to use. When set to None the backend configured in config.backend is used. + varfit : string + Determines how to calculate the residuals for source decomposition. + 'ensemble' (default) fits one model to the whole data set, + 'class' fits a new model for each class, and + 'trial' fits a new model for each individual trial. + + Returns + ------- + result : class + A class with the following attributes is returned: + + +---------------+----------------------------------------------------------+ + | mixing | Source mixing matrix | + +---------------+----------------------------------------------------------+ + | unmixing | Source unmixing matrix | + +---------------+----------------------------------------------------------+ + | residuals | Residuals of the VAR model(s) in source space | + +---------------+----------------------------------------------------------+ + | var_residuals | Residuals of the VAR model(s) in EEG space (before ICA) | + +---------------+----------------------------------------------------------+ + | c | Noise covariance of the VAR model(s) in source space | + +---------------+----------------------------------------------------------+ + | b | VAR model coefficients (source space) | + +---------------+----------------------------------------------------------+ + | a | VAR model coefficients (EEG space) | + +---------------+----------------------------------------------------------+ + + Notes + ----- + MVARICA is performed with the following steps: + 1. Optional dimensionality reduction with PCA + 2. Fitting a VAR model tho the data + 3. Decomposing the VAR model residuals with ICA + 4. Correcting the VAR coefficients + + References + ---------- + .. [1] G. Gomez-Herrero et al. "Measuring directional coupling between EEG sources", NeuroImage, 2008 + """ + + x = np.atleast_3d(x) + l, m, t = np.shape(x) + + if backend is None: + backend = config.backend + + # pre-transform the data with PCA + if reducedim == 'no pca': + c = np.eye(m) + d = np.eye(m) + xpca = x + else: + c, d, xpca = backend['pca'](x, reducedim) + + if optimize_var: + var.optimize(xpca) + + if varfit == 'trial': + r = np.zeros(xpca.shape) + for i in range(t): + # fit MVAR model + a = var.fit(xpca[:, :, i]) + # residuals + r[:, :, i] = xpca[:, :, i] - var.predict(xpca[:, :, i])[:, :, 0] + elif varfit == 'class': + r = np.zeros(xpca.shape) + for i in np.unique(cl): + mask = cl == i + a = var.fit(xpca[:, :, mask]) + r[:, :, mask] = xpca[:, :, mask] - var.predict(xpca[:, :, mask]) + elif varfit == 'ensemble': + # fit MVAR model + a = var.fit(xpca) + # residuals + r = xpca - var.predict(xpca) + else: + raise ValueError('unknown VAR fitting mode: {}'.format(varfit)) + + # run on residuals ICA to estimate volume conduction + mx, ux = backend['ica'](cat_trials(r)) + + # driving process + e = dot_special(r, ux) + + # correct AR coefficients + b = a.copy() + for k in range(0, a.p): + b.coef[:, k::a.p] = mx.dot(a.coef[:, k::a.p].transpose()).dot(ux).transpose() + + # correct (un)mixing matrix estimatees + mx = mx.dot(d) + ux = c.dot(ux) + + class Result: + unmixing = ux + mixing = mx + residuals = e + var_residuals = r + c = np.cov(cat_trials(e), rowvar=False) + + Result.b = b + Result.a = a + Result.xpca = xpca + + return Result + + +def cspvarica(x, var, cl, reducedim=np.inf, optimize_var=False, backend=None, varfit='ensemble'): + """ Performs joint VAR model fitting and ICA source separation. + + This function implements the CSPVARICA procedure [1]_. + + Parameters + ---------- + x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] + data set + var : :class:`~scot.var.VARBase`-like object + Vector autoregressive model (VAR) object that is used for model fitting. + cl : list of valid dict keys + Class labels associated with each trial. + reducedim : {int}, optional + Number of (most discriminative) components to keep after applying the CSP. + optimize_var : bool, optional + Whether to call automatic optimization of the VAR fitting routine. + backend : dict-like, optional + Specify backend to use. When set to None the backend configured in config.backend is used. + varfit : string + Determines how to calculate the residuals for source decomposition. + 'ensemble' (default) fits one model to the whole data set, + 'class' fits a new model for each class, and + 'trial' fits a new model for each individual trial. + + Returns + ------- + Result : class + A class with the following attributes is returned: + + +---------------+----------------------------------------------------------+ + | mixing | Source mixing matrix | + +---------------+----------------------------------------------------------+ + | unmixing | Source unmixing matrix | + +---------------+----------------------------------------------------------+ + | residuals | Residuals of the VAR model(s) in source space | + +---------------+----------------------------------------------------------+ + | var_residuals | Residuals of the VAR model(s) in EEG space (before ICA) | + +---------------+----------------------------------------------------------+ + | c | Noise covariance of the VAR model(s) in source space | + +---------------+----------------------------------------------------------+ + | b | VAR model coefficients (source space) | + +---------------+----------------------------------------------------------+ + | a | VAR model coefficients (EEG space) | + +---------------+----------------------------------------------------------+ + + Notes + ----- + CSPVARICA is performed with the following steps: + 1. Dimensionality reduction with CSP + 2. Fitting a VAR model tho the data + 3. Decomposing the VAR model residuals with ICA + 4. Correcting the VAR coefficients + + References + ---------- + .. [1] M. Billinger et al. "SCoT: A Python Toolbox for EEG Source Connectivity", Frontiers in Neuroinformatics, 2014 + """ + + x = np.atleast_3d(x) + l, m, t = np.shape(x) + + if backend is None: + backend = config.backend + + # pre-transform the data with CSP + c, d, xcsp = backend['csp'](x, cl, reducedim) + + if optimize_var: + var.optimize(xcsp) + + if varfit == 'trial': + r = np.zeros(xcsp.shape) + for i in range(t): + # fit MVAR model + a = var.fit(xcsp[:, :, i]) + # residuals + r[:, :, i] = xcsp[:, :, i] - var.predict(xcsp[:, :, i])[:, :, 0] + elif varfit == 'class': + r = np.zeros(xcsp.shape) + for i in np.unique(cl): + mask = cl == i + a = var.fit(xcsp[:, :, mask]) + r[:, :, mask] = xcsp[:, :, mask] - var.predict(xcsp[:, :, mask]) + elif varfit == 'ensemble': + # fit MVAR model + a = var.fit(xcsp) + # residuals + r = xcsp - var.predict(xcsp) + else: + raise ValueError('unknown VAR fitting mode: {}'.format(varfit)) + + # run on residuals ICA to estimate volume conduction + mx, ux = backend['ica'](cat_trials(r)) + + # driving process + e = dot_special(r, ux) + + # correct AR coefficients + b = a.copy() + for k in range(0, a.p): + b.coef[:, k::a.p] = mx.dot(a.coef[:, k::a.p].transpose()).dot(ux).transpose() + + # correct (un)mixing matrix estimatees + mx = mx.dot(d) + ux = c.dot(ux) + + class Result: + unmixing = ux + mixing = mx + residuals = e + var_residuals = r + c = np.cov(cat_trials(e), rowvar=False) + Result.b = b + Result.a = a + Result.xcsp = xcsp + + return Result diff --git a/mne_sandbox/externals/scot/xvschema.py b/mne_sandbox/externals/scot/xvschema.py new file mode 100644 index 0000000..4e3a293 --- /dev/null +++ b/mne_sandbox/externals/scot/xvschema.py @@ -0,0 +1,115 @@ +# Released under The MIT License (MIT) +# http://opensource.org/licenses/MIT +# Copyright (c) 2013 SCoT Development Team + +""" Cross-validation schemas """ + +from __future__ import division + +import numpy as np +from numpy import sort +from functools import partial + + +def singletrial(num_trials, skipstep): + """ Single-trial cross-validation schema + + Use one trial for training, all others for testing. + + Parameters + ---------- + num_trials : int + Total number of trials + skipstep : int + only use every `skipstep` trial for training + + Returns + ------- + gen : generator object + the generator returns tuples (trainset, testset) + """ + for t in range(0, num_trials, skipstep): + trainset = [t] + testset = [i for i in range(trainset[0])] + \ + [i for i in range(trainset[-1] + 1, num_trials)] + testset = sort([t % num_trials for t in testset]) + yield trainset, testset + + +def multitrial(num_trials, skipstep): + """ Multi-trial cross-validation schema + + Use one trial for testing, all others for training. + + Parameters + ---------- + num_trials : int + Total number of trials + skipstep : int + only use every `skipstep` trial for testing + + Returns + ------- + gen : generator object + the generator returns tuples (trainset, testset) + """ + for t in range(0, num_trials, skipstep): + testset = [t] + trainset = [i for i in range(testset[0])] + \ + [i for i in range(testset[-1] + 1, num_trials)] + trainset = sort([t % num_trials for t in trainset]) + yield trainset, testset + + +def splitset(num_trials, skipstep): + """ Split-set cross validation + + Use half the trials for training, and the other half for testing. Then + repeat the other way round. + + Parameters + ---------- + num_trials : int + Total number of trials + skipstep : int + unused + + Returns + ------- + gen : generator object + the generator returns tuples (trainset, testset) + """ + split = num_trials // 2 + + a = list(range(0, split)) + b = list(range(split, num_trials)) + yield a, b + yield b, a + + +def make_nfold(n): + """ n-fold cross validation + + Use each of n blocks for testing once. + + Parameters + ---------- + n : int + number of blocks + + Returns + ------- + gengen : func + a function that returns the generator + """ + return partial(_nfold, n=n) + + +def _nfold(num_trials, skipstep, n): + blocksize = int(np.ceil(num_trials / n)) + for i in range(0, num_trials, blocksize): + testset = [k for k in (i + np.arange(blocksize)) if k < num_trials] + trainset = [i for i in range(testset[0])] + \ + [i for i in range(testset[-1] + 1, num_trials)] + trainset = sort([t % num_trials for t in trainset]) + yield trainset, testset diff --git a/mne_sandbox/viz/__init__.py b/mne_sandbox/viz/__init__.py new file mode 100644 index 0000000..ee30e0e --- /dev/null +++ b/mne_sandbox/viz/__init__.py @@ -0,0 +1,6 @@ +"""Visualization routines +""" + + +from .connectivity import (plot_connectivity_circle, plot_connectivity_matrix, + plot_connectivity_inoutcircles) diff --git a/mne_sandbox/viz/connectivity.py b/mne_sandbox/viz/connectivity.py new file mode 100644 index 0000000..49b8544 --- /dev/null +++ b/mne_sandbox/viz/connectivity.py @@ -0,0 +1,583 @@ +"""Functions to plot directed connectivity +""" +from __future__ import print_function + +# Authors: Martin Billinger +# +# License: Simplified BSD + + +from itertools import cycle +from functools import partial + +import numpy as np + +from mne.viz.utils import plt_show +from mne.externals.six import string_types +from mne.fixes import tril_indices, normalize_colors +from mne.viz.circle import _plot_connectivity_circle_onpick + + +def plot_connectivity_circle(con, node_names, indices=None, n_lines=None, + node_angles=None, node_width=None, + node_colors=None, facecolor='black', + textcolor='white', node_edgecolor='black', + linewidth=1.5, colormap='hot', vmin=None, + vmax=None, colorbar=True, title=None, + colorbar_size=0.2, colorbar_pos=(-0.3, 0.1), + fontsize_title=12, fontsize_names=8, + fontsize_colorbar=8, padding=6., + fig=None, subplot=111, interactive=True, + node_linewidth=2., plot_names=True, show=True): + """Visualize connectivity as a circular graph. + + Note: This code is based on the circle graph example by Nicolas P. Rougier + http://www.labri.fr/perso/nrougier/coding/. + + Parameters + ---------- + con : array + Connectivity scores. Can be a square matrix, or a 1D array. If a 1D + array is provided, "indices" has to be used to define the connection + indices. + node_names : list of str + Node names. The order corresponds to the order in con. + indices : tuple of arrays | None + Two arrays with indices of connections for which the connections + strenghts are defined in con. Only needed if con is a 1D array. + n_lines : int | None + If not None, only the n_lines strongest connections (strength=abs(con)) + are drawn. + node_angles : array, shape=(len(node_names,)) | None + Array with node positions in degrees. If None, the nodes are equally + spaced on the circle. See mne.viz.circular_layout. + node_width : float | None + Width of each node in degrees. If None, the minimum angle between any + two nodes is used as the width. + node_colors : list of tuples | list of str + List with the color to use for each node. If fewer colors than nodes + are provided, the colors will be repeated. Any color supported by + matplotlib can be used, e.g., RGBA tuples, named colors. + facecolor : str + Color to use for background. See matplotlib.colors. + textcolor : str + Color to use for text. See matplotlib.colors. + node_edgecolor : str + Color to use for lines around nodes. See matplotlib.colors. + linewidth : float + Line width to use for connections. + colormap : str + Colormap to use for coloring the connections. + vmin : float | None + Minimum value for colormap. If None, it is determined automatically. + vmax : float | None + Maximum value for colormap. If None, it is determined automatically. + colorbar : bool + Display a colorbar or not. + title : str + The figure title. + colorbar_size : float + Size of the colorbar. + colorbar_pos : 2-tuple + Position of the colorbar. + fontsize_title : int + Font size to use for title. + fontsize_names : int + Font size to use for node names. + fontsize_colorbar : int + Font size to use for colorbar. + padding : float + Space to add around figure to accommodate long labels. + fig : None | instance of matplotlib.pyplot.Figure + The figure to use. If None, a new figure with the specified background + color will be created. + subplot : int | 3-tuple + Location of the subplot when creating figures with multiple plots. E.g. + 121 or (1, 2, 1) for 1 row, 2 columns, plot 1. See + matplotlib.pyplot.subplot. + interactive : bool + When enabled, left-click on a node to show only connections to that + node. Right-click shows all connections. + node_linewidth : float + Line with for nodes. + plot_names : bool + Draw node names if True. + show : bool + Show figure if True. + + Returns + ------- + fig : instance of matplotlib.pyplot.Figure + The figure handle. + axes : instance of matplotlib.axes.PolarAxesSubplot + The subplot handle. + """ + import matplotlib.pyplot as plt + import matplotlib.path as m_path + import matplotlib.patches as m_patches + + n_nodes = len(node_names) + + if node_angles is not None: + if len(node_angles) != n_nodes: + raise ValueError('node_angles has to be the same length ' + 'as node_names') + # convert it to radians + node_angles = node_angles * np.pi / 180 + else: + # uniform layout on unit circle + node_angles = np.linspace(0, 2 * np.pi, n_nodes, endpoint=False) + + if node_width is None: + # widths correspond to the minimum angle between two nodes + dist_mat = node_angles[None, :] - node_angles[:, None] + dist_mat[np.diag_indices(n_nodes)] = 1e9 + node_width = np.min(np.abs(dist_mat)) + else: + node_width = node_width * np.pi / 180 + + if node_colors is not None: + if len(node_colors) < n_nodes: + node_colors = cycle(node_colors) + else: + # assign colors using colormap + node_colors = [plt.cm.spectral(i / float(n_nodes)) + for i in range(n_nodes)] + + # handle 1D and 2D connectivity information + if con.ndim == 1: + if indices is None: + raise ValueError('indices has to be provided if con.ndim == 1') + elif con.ndim == 2: + if con.shape[0] != n_nodes or con.shape[1] != n_nodes: + raise ValueError('con has to be 1D or a square matrix') + # we use the lower-triangular part + indices = tril_indices(n_nodes, -1) + con = con[indices] + else: + raise ValueError('con has to be 1D or a square matrix') + + # get the colormap + if isinstance(colormap, string_types): + colormap = plt.get_cmap(colormap) + + # Make figure background the same colors as axes + if fig is None: + fig = plt.figure(figsize=(8, 8), facecolor=facecolor) + + # Use a polar axes + if not isinstance(subplot, tuple): + subplot = (subplot,) + axes = plt.subplot(*subplot, polar=True, axisbg=facecolor) + + # No ticks, we'll put our own + plt.xticks([]) + plt.yticks([]) + + # Set y axes limit, add additonal space if requested + plt.ylim(0, 10 + padding) + + # Remove the black axes border which may obscure the labels + axes.spines['polar'].set_visible(False) + + # Draw lines between connected nodes, only draw the strongest connections + if n_lines is not None and len(con) > n_lines: + con_thresh = np.sort(np.abs(con).ravel())[-n_lines] + else: + con_thresh = 0. + + # get the connections which we are drawing and sort by connection strength + # this will allow us to draw the strongest connections first + con_abs = np.abs(con) + con_draw_idx = np.where(con_abs >= con_thresh)[0] + + con = con[con_draw_idx] + con_abs = con_abs[con_draw_idx] + indices = [ind[con_draw_idx] for ind in indices] + + # now sort them + sort_idx = np.argsort(con_abs) + con_abs = con_abs[sort_idx] + con = con[sort_idx] + indices = [ind[sort_idx] for ind in indices] + + # Get vmin vmax for color scaling + if vmin is None: + vmin = np.min(con[np.abs(con) >= con_thresh]) + if vmax is None: + vmax = np.max(con) + vrange = vmax - vmin + + # We want to add some "noise" to the start and end position of the + # edges: We modulate the noise with the number of connections of the + # node and the connection strength, such that the strongest connections + # are closer to the node center + nodes_n_con = np.zeros((n_nodes), dtype=np.int) + for i, j in zip(indices[0], indices[1]): + nodes_n_con[i] += 1 + nodes_n_con[j] += 1 + + # initalize random number generator so plot is reproducible + rng = np.random.mtrand.RandomState(seed=0) + + n_con = len(indices[0]) + noise_max = 0.25 * node_width + start_noise = rng.uniform(-noise_max, noise_max, n_con) + end_noise = rng.uniform(-noise_max, noise_max, n_con) + + nodes_n_con_seen = np.zeros_like(nodes_n_con) + for i, (start, end) in enumerate(zip(indices[0], indices[1])): + nodes_n_con_seen[start] += 1 + nodes_n_con_seen[end] += 1 + + start_noise[i] *= ((nodes_n_con[start] - nodes_n_con_seen[start]) / + float(nodes_n_con[start])) + end_noise[i] *= ((nodes_n_con[end] - nodes_n_con_seen[end]) / + float(nodes_n_con[end])) + + # scale connectivity for colormap (vmin<=>0, vmax<=>1) + con_val_scaled = (con - vmin) / vrange + + # Finally, we draw the connections + for pos, (i, j) in enumerate(zip(indices[0], indices[1])): + # Start point + t0, r0 = node_angles[i], 10 + + # End point + t1, r1 = node_angles[j], 10 + + # Some noise in start and end point + t0 += start_noise[pos] + t1 += end_noise[pos] + + verts = [(t0, r0), (t0, 5), (t1, 5), (t1, r1)] + codes = [m_path.Path.MOVETO, m_path.Path.CURVE4, m_path.Path.CURVE4, + m_path.Path.LINETO] + path = m_path.Path(verts, codes) + + color = colormap(con_val_scaled[pos]) + + # Actual line + patch = m_patches.PathPatch(path, fill=False, edgecolor=color, + linewidth=linewidth, alpha=1.) + axes.add_patch(patch) + + # Draw ring with colored nodes + height = np.ones(n_nodes) * 1.0 + bars = axes.bar(node_angles, height, width=node_width, bottom=9, + edgecolor=node_edgecolor, lw=node_linewidth, + facecolor='.9', align='center') + + for bar, color in zip(bars, node_colors): + bar.set_facecolor(color) + + # Draw node labels + if plot_names: + angles_deg = 180 * node_angles / np.pi + for name, angle_rad, angle_deg in zip(node_names, node_angles, angles_deg): + if angle_deg >= 270: + ha = 'left' + else: + # Flip the label, so text is always upright + angle_deg += 180 + ha = 'right' + + axes.text(angle_rad, 10.4, name, size=fontsize_names, + rotation=angle_deg, rotation_mode='anchor', + horizontalalignment=ha, verticalalignment='center', + color=textcolor) + + if title is not None: + plt.title(title, color=textcolor, fontsize=fontsize_title, + axes=axes) + + if colorbar: + norm = normalize_colors(vmin=vmin, vmax=vmax) + sm = plt.cm.ScalarMappable(cmap=colormap, norm=norm) + sm.set_array(np.linspace(vmin, vmax)) + cb = plt.colorbar(sm, ax=axes, use_gridspec=False, + shrink=colorbar_size, + anchor=colorbar_pos) + cb_yticks = plt.getp(cb.ax.axes, 'yticklabels') + cb.ax.tick_params(labelsize=fontsize_colorbar) + plt.setp(cb_yticks, color=textcolor) + + # Add callback for interaction + if interactive: + callback = partial(_plot_connectivity_circle_onpick, fig=fig, + axes=axes, indices=indices, n_nodes=n_nodes, + node_angles=node_angles) + + fig.canvas.mpl_connect('button_press_event', callback) + + plt_show(show) + return fig, axes + + +def _plot_connectivity_matrix_nodename(x, y, con, node_names): + x = int(round(x) - 2) + y = int(round(y) - 2) + if x < 0 or y < 0 or x >= len(node_names) or y >= len(node_names): + return '' + return '{} --> {}: {:.3g}'.format(node_names[x], node_names[y], + con[y + 2, x + 2]) + + +def plot_connectivity_matrix(con, node_names, indices=None, + node_colors=None, facecolor='black', + textcolor='white', colormap='hot', vmin=None, + vmax=None, colorbar=True, title=None, + colorbar_size=0.2, colorbar_pos=(-0.3, 0.1), + fontsize_title=12, fontsize_names=8, + fontsize_colorbar=8, fig=None, subplot=111, + show_names=True): + """Visualize connectivity as a matrix. + + Parameters + ---------- + con : array + Connectivity scores. Can be a square matrix, or a 1D array. If a 1D + array is provided, "indices" has to be used to define the connection + indices. + node_names : list of str + Node names. The order corresponds to the order in con. + indices : tuple of arrays | None + Two arrays with indices of connections for which the connections + strenghts are defined in con. Only needed if con is a 1D array. + node_colors : list of tuples | list of str + List with the color to use for each node. If fewer colors than nodes + are provided, the colors will be repeated. Any color supported by + matplotlib can be used, e.g., RGBA tuples, named colors. + facecolor : str + Color to use for background. See matplotlib.colors. + textcolor : str + Color to use for text. See matplotlib.colors. + colormap : str + Colormap to use for coloring the connections. + vmin : float | None + Minimum value for colormap. If None, it is determined automatically. + vmax : float | None + Maximum value for colormap. If None, it is determined automatically. + colorbar : bool + Display a colorbar or not. + title : str + The figure title. + colorbar_size : float + Size of the colorbar. + colorbar_pos : 2-tuple + Position of the colorbar. + fontsize_title : int + Font size to use for title. + fontsize_names : int + Font size to use for node names. + fontsize_colorbar : int + Font size to use for colorbar. + padding : float + Space to add around figure to accommodate long labels. + fig : None | instance of matplotlib.pyplot.Figure + The figure to use. If None, a new figure with the specified background + color will be created. + subplot : int | 3-tuple + Location of the subplot when creating figures with multiple plots. E.g. + 121 or (1, 2, 1) for 1 row, 2 columns, plot 1. See + matplotlib.pyplot.subplot. + show_names : bool + Enable or disable display of node names in the plot. The names are + always displayed in the status bar when hovering over them. + + Returns + ------- + fig : instance of matplotlib.pyplot.Figure + The figure handle. + axes : instance of matplotlib.axes.PolarAxesSubplot + The subplot handle. + """ + import matplotlib.pyplot as plt + + n_nodes = len(node_names) + + if node_colors is not None: + if len(node_colors) < n_nodes: + node_colors = cycle(node_colors) + else: + # assign colors using colormap + node_colors = [plt.cm.spectral(i / float(n_nodes)) + for i in range(n_nodes)] + + # handle 1D and 2D connectivity information + if con.ndim == 1: + if indices is None: + raise ValueError('indices must be provided if con.ndim == 1') + tmp = np.zeros((n_nodes, n_nodes)) * np.nan + for ci in zip(con, *indices): + tmp[ci[1:]] = ci[0] + con = tmp + elif con.ndim == 2: + if con.shape[0] != n_nodes or con.shape[1] != n_nodes: + raise ValueError('con has to be 1D or a square matrix') + else: + raise ValueError('con has to be 1D or a square matrix') + + # remove diagonal (do not show node's self-connectivity) + np.fill_diagonal(con, np.nan) + + # get the colormap + if isinstance(colormap, string_types): + colormap = plt.get_cmap(colormap) + + # Make figure background the same colors as axes + if fig is None: + fig = plt.figure(figsize=(8, 8), facecolor=facecolor) + + if not isinstance(subplot, tuple): + subplot = (subplot,) + axes = plt.subplot(*subplot, axisbg=facecolor) + + axes.spines['bottom'].set_visible(False) + axes.spines['right'].set_visible(False) + axes.spines['left'].set_visible(False) + axes.spines['top'].set_visible(False) + + tmp = np.empty((n_nodes + 4, n_nodes + 4)) * np.nan + tmp[2:-2, 2:-2] = con + con = tmp + + h = axes.imshow(con, cmap=colormap, interpolation='nearest', vmin=vmin, + vmax=vmax) + + nodes = np.empty((n_nodes + 4, n_nodes + 4, 4)) * np.nan + for i in range(n_nodes): + nodes[i + 2, 0, :] = node_colors[i] + nodes[i + 2, -1, :] = node_colors[i] + nodes[0, i + 2, :] = node_colors[i] + nodes[-1, i + 2, :] = node_colors[i] + axes.imshow(nodes, interpolation='nearest') + + if colorbar: + cb = plt.colorbar(h, ax=axes, use_gridspec=False, + shrink=colorbar_size, + anchor=colorbar_pos) + cb_yticks = plt.getp(cb.ax.axes, 'yticklabels') + cb.ax.tick_params(labelsize=fontsize_colorbar) + plt.setp(cb_yticks, color=textcolor) + + if title is not None: + plt.title(title, color=textcolor, fontsize=fontsize_title, + axes=axes) + + # Draw node labels + if show_names: + for i, name in enumerate(node_names): + axes.text(-1, i + 2, name, size=fontsize_names, + rotation=0, rotation_mode='anchor', + horizontalalignment='right', verticalalignment='center', + color=textcolor) + axes.text(i + 2, len(node_names) + 4, name, size=fontsize_names, + rotation=90, rotation_mode='anchor', + horizontalalignment='right', verticalalignment='center', + color=textcolor) + + axes.format_coord = partial(_plot_connectivity_matrix_nodename, con=con, + node_names=node_names) + + return fig, axes + + +def plot_connectivity_inoutcircles(con, seed, node_names, facecolor='black', + textcolor='white', colormap='hot', + title=None, fontsize_suptitle=14, fig=None, + subplot=(121, 122), **kwargs): + """Visualize effective connectivity with two circular graphs, one for + incoming, and one for outgoing connections. + + Note: This code is based on the circle graph example by Nicolas P. Rougier + http://www.loria.fr/~rougier/coding/recipes.html + + Parameters + ---------- + con : array + Connectivity scores. Can be a square matrix, or a 1D array. If a 1D + array is provided, "indices" has to be used to define the connection + indices. + seed : int | str + Index or name of the seed node. Connections towards and from that node + are displayed. The seed can be changed by clicking on a node in + interactive mode. + node_names : list of str + Node names. The order corresponds to the order in con. + facecolor : str + Color to use for background. See matplotlib.colors. + textcolor : str + Color to use for text. See matplotlib.colors. + colormap : str | (str, str) + Colormap to use for coloring the connections. Can be a tuple of two + strings, in which case the first colormap is used for incoming, and the + second colormap for outgoing connections. + title : str + The figure title. + fontsize_suptitle : int + Font size to use for title. + fig : None | instance of matplotlib.pyplot.Figure + The figure to use. If None, a new figure with the specified background + color will be created. + subplot : (int, int) | (3-tuple, 3-tuple) + Location of the two subplots for incoming and outgoing connections. + E.g. 121 or (1, 2, 1) for 1 row, 2 columns, plot 1. See + matplotlib.pyplot.subplot. + **kwargs : + The remaining keyword-arguments will be passed directly to + plot_connectivity_circle. + + Returns + ------- + fig : instance of matplotlib.pyplot.Figure + The figure handle. + axes_in : instance of matplotlib.axes.PolarAxesSubplot + The subplot handle. + axes_out : instance of matplotlib.axes.PolarAxesSubplot + The subplot handle. + """ + import matplotlib.pyplot as plt + + n_nodes = len(node_names) + + if any(isinstance(seed, t) for t in string_types): + try: + seed = node_names.index(seed) + except ValueError: + from difflib import get_close_matches + close = get_close_matches(seed, node_names) + raise ValueError('{} is not in the list of node names. Did you ' + 'mean {}?'.format(seed, close)) + + if seed < 0 or seed >= n_nodes: + raise ValueError('seed={} is not in range [0, {}].' + .format(seed, n_nodes - 1)) + + if type(colormap) not in (tuple, list): + colormap = (colormap, colormap) + + # Default figure size accomodates two horizontally arranged circles + if fig is None: + fig = plt.figure(figsize=(8, 4), facecolor=facecolor) + + index_in = (np.array([seed] * n_nodes), + np.array([i for i in range(n_nodes)])) + index_out = index_in[::-1] + + fig, axes_in = plot_connectivity_circle(con[seed, :].ravel(), node_names, + indices=index_in, + colormap=colormap[0], fig=fig, + subplot=subplot[0], + title='incoming', **kwargs) + + fig, axes_out = plot_connectivity_circle(con[:, seed].ravel(), node_names, + indices=index_out, + colormap=colormap[1], fig=fig, + subplot=subplot[1], + title='outgoing', **kwargs) + + if title is not None: + plt.suptitle(title, color=textcolor, fontsize=fontsize_suptitle) + + return fig, axes_in, axes_out diff --git a/setup.py b/setup.py index 5594c88..c1b08f2 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,9 @@ platforms='any', packages=[ 'mne_sandbox', + 'mne_sandbox.connectivity', + 'mne_sandbox.externals.scot', 'mne_sandbox.preprocessing', + 'mne_sandbox.viz' ], - ) + ) From 9bb5c690d3f236d4d86f1ce222f82c68ebe045a5 Mon Sep 17 00:00:00 2001 From: mbillinger Date: Thu, 21 Apr 2016 10:20:29 +0200 Subject: [PATCH 02/12] PEP 8 --- mne_sandbox/connectivity/mvar.py | 2 +- mne_sandbox/connectivity/tests/test_mvar.py | 8 +++++--- mne_sandbox/viz/connectivity.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mne_sandbox/connectivity/mvar.py b/mne_sandbox/connectivity/mvar.py index f888798..5a554ce 100644 --- a/mne_sandbox/connectivity/mvar.py +++ b/mne_sandbox/connectivity/mvar.py @@ -47,7 +47,7 @@ def _fit_mvar_lsq(data, pmin, pmax, delta, n_jobs, verbose): if pmin != pmax: logger.info('MVAR order selection...') var.optimize_order(data, pmin, pmax, n_jobs=n_jobs, verbose=verbose) - #todo: only convert if data is a generator + # todo: only convert if data is a generator data = np.asarray(list(data)).transpose([2, 1, 0]) var.fit(data) return var diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py index f00a217..92072e8 100644 --- a/mne_sandbox/connectivity/tests/test_mvar.py +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -30,7 +30,7 @@ def _make_data(var_coef, n_samples, n_epochs): x = np.random.randn(n_signals, n_epochs * n_samples + 10 * var_order) for i in range(var_order, x.shape[1]): for k in range(var_order): - x[:, [i]] += np.dot(var_coef[k], x[:, [i-k-1]]) + x[:, [i]] += np.dot(var_coef[k], x[:, [i - k - 1]]) x = x[:, -n_epochs * n_samples:] @@ -55,7 +55,8 @@ def test_mvar_connectivity(): assert_raises(ValueError, mvar_connectivity, [], 'PDC', 99, fmin=(11,), fmax=(12, 15)) assert_raises(ValueError, mvar_connectivity, [], 'S', fitting_mode='') - assert_raises(NotImplementedError, mvar_connectivity, [], 'H', fitting_mode='yw') + assert_raises(NotImplementedError, mvar_connectivity, [], 'H', + fitting_mode='yw') methods = ['S', 'COH', 'DTF', 'PDC', 'ffDTF', 'GPDC', 'GDTF', 'A'] @@ -63,7 +64,8 @@ def test_mvar_connectivity(): var_coef = np.zeros((1, n_sigs, n_sigs)) data = _make_data(var_coef, n_samples, n_epochs) - con, freqs, p, p_vals = mvar_connectivity(data, methods, order=1, fitting_mode='yw') + con, freqs, p, p_vals = mvar_connectivity(data, methods, order=1, + fitting_mode='yw') con = dict((m, c) for m, c in zip(methods, con)) assert_equal(p, 1) diff --git a/mne_sandbox/viz/connectivity.py b/mne_sandbox/viz/connectivity.py index 49b8544..8d257b4 100644 --- a/mne_sandbox/viz/connectivity.py +++ b/mne_sandbox/viz/connectivity.py @@ -274,7 +274,8 @@ def plot_connectivity_circle(con, node_names, indices=None, n_lines=None, # Draw node labels if plot_names: angles_deg = 180 * node_angles / np.pi - for name, angle_rad, angle_deg in zip(node_names, node_angles, angles_deg): + for name, angle_rad, angle_deg in zip(node_names, node_angles, + angles_deg): if angle_deg >= 270: ha = 'left' else: @@ -320,7 +321,7 @@ def _plot_connectivity_matrix_nodename(x, y, con, node_names): if x < 0 or y < 0 or x >= len(node_names) or y >= len(node_names): return '' return '{} --> {}: {:.3g}'.format(node_names[x], node_names[y], - con[y + 2, x + 2]) + con[y + 2, x + 2]) def plot_connectivity_matrix(con, node_names, indices=None, From b4116933557c8dba028571d72238f38f57588d82 Mon Sep 17 00:00:00 2001 From: mbilling Date: Thu, 21 Apr 2016 10:45:31 +0200 Subject: [PATCH 03/12] Made scot a dependency rather than an external --- .travis.yml | 1 + .../plot_mne_inverse_label_connectivity.py | 2 +- mne_sandbox/connectivity/mvar.py | 11 +- mne_sandbox/externals/scot/__init__.py | 24 - mne_sandbox/externals/scot/backend_builtin.py | 47 - mne_sandbox/externals/scot/backend_sklearn.py | 97 --- mne_sandbox/externals/scot/binica.py | 167 ---- mne_sandbox/externals/scot/config.py | 8 - mne_sandbox/externals/scot/connectivity.py | 369 -------- .../externals/scot/connectivity_statistics.py | 299 ------- mne_sandbox/externals/scot/csp.py | 73 -- mne_sandbox/externals/scot/datatools.py | 153 ---- mne_sandbox/externals/scot/matfiles.py | 49 -- mne_sandbox/externals/scot/ooapi.py | 802 ------------------ mne_sandbox/externals/scot/parallel.py | 33 - mne_sandbox/externals/scot/pca.py | 130 --- mne_sandbox/externals/scot/plainica.py | 77 -- mne_sandbox/externals/scot/plotting.py | 677 --------------- mne_sandbox/externals/scot/utils.py | 203 ----- mne_sandbox/externals/scot/var.py | 316 ------- mne_sandbox/externals/scot/varbase.py | 472 ----------- mne_sandbox/externals/scot/varica.py | 258 ------ mne_sandbox/externals/scot/xvschema.py | 115 --- setup.py | 1 - 24 files changed, 8 insertions(+), 4376 deletions(-) delete mode 100644 mne_sandbox/externals/scot/__init__.py delete mode 100644 mne_sandbox/externals/scot/backend_builtin.py delete mode 100644 mne_sandbox/externals/scot/backend_sklearn.py delete mode 100644 mne_sandbox/externals/scot/binica.py delete mode 100644 mne_sandbox/externals/scot/config.py delete mode 100644 mne_sandbox/externals/scot/connectivity.py delete mode 100644 mne_sandbox/externals/scot/connectivity_statistics.py delete mode 100644 mne_sandbox/externals/scot/csp.py delete mode 100644 mne_sandbox/externals/scot/datatools.py delete mode 100644 mne_sandbox/externals/scot/matfiles.py delete mode 100644 mne_sandbox/externals/scot/ooapi.py delete mode 100644 mne_sandbox/externals/scot/parallel.py delete mode 100644 mne_sandbox/externals/scot/pca.py delete mode 100644 mne_sandbox/externals/scot/plainica.py delete mode 100644 mne_sandbox/externals/scot/plotting.py delete mode 100644 mne_sandbox/externals/scot/utils.py delete mode 100644 mne_sandbox/externals/scot/var.py delete mode 100644 mne_sandbox/externals/scot/varbase.py delete mode 100644 mne_sandbox/externals/scot/varica.py delete mode 100644 mne_sandbox/externals/scot/xvschema.py diff --git a/.travis.yml b/.travis.yml index 4261a18..b84fadd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,6 +53,7 @@ install: - source ${MNE_ROOT}/bin/mne_setup_sh; - conda install --yes --quiet $ENSURE_PACKAGES pandas$PANDAS scikit-learn$SKLEARN patsy h5py pillow; - pip install -q joblib nibabel; + - pip install -q scot==0.1.0 - if [ "${PYTHON}" == "3.5" ]; then conda install --yes --quiet $ENSURE_PACKAGES ipython; else diff --git a/examples/connectivity/plot_mne_inverse_label_connectivity.py b/examples/connectivity/plot_mne_inverse_label_connectivity.py index 84af292..3d99419 100644 --- a/examples/connectivity/plot_mne_inverse_label_connectivity.py +++ b/examples/connectivity/plot_mne_inverse_label_connectivity.py @@ -31,7 +31,7 @@ from mne.viz import circular_layout from mne_sandbox.viz import (plot_connectivity_circle, plot_connectivity_inoutcircles) -from mne_sandbox.externals.scot.connectivity_statistics import significance_fdr +from scot.connectivity_statistics import significance_fdr print(__doc__) diff --git a/mne_sandbox/connectivity/mvar.py b/mne_sandbox/connectivity/mvar.py index 5a554ce..bfb9e8e 100644 --- a/mne_sandbox/connectivity/mvar.py +++ b/mne_sandbox/connectivity/mvar.py @@ -6,13 +6,14 @@ import numpy as np import logging +from scot.varbase import VARBase +from scot.var import VAR +from scot.connectivity import connectivity +from scot.connectivity_statistics import surrogate_connectivity +from scot.xvschema import make_nfold + from mne.parallel import parallel_func from mne.utils import logger, verbose -from ..externals.scot.varbase import VARBase -from ..externals.scot.var import VAR -from ..externals.scot.connectivity import connectivity -from ..externals.scot.connectivity_statistics import surrogate_connectivity -from ..externals.scot.xvschema import make_nfold def _acm(x, l): diff --git a/mne_sandbox/externals/scot/__init__.py b/mne_sandbox/externals/scot/__init__.py deleted file mode 100644 index aeddce1..0000000 --- a/mne_sandbox/externals/scot/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" SCoT: The Source Connectivity Toolbox -""" - -from __future__ import absolute_import - -from . import config - -backends = ['backend_builtin', 'backend_sklearn'] - -# default backend -# TODO: set default backend in config -from . import backend_builtin - -from .ooapi import Workspace - -from .connectivity import Connectivity - -from . import datatools - -__all__ = ['Workspace', 'Connectivity', 'datatools'] diff --git a/mne_sandbox/externals/scot/backend_builtin.py b/mne_sandbox/externals/scot/backend_builtin.py deleted file mode 100644 index bd3052d..0000000 --- a/mne_sandbox/externals/scot/backend_builtin.py +++ /dev/null @@ -1,47 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Use internally implemented functions as backend. -""" - -import numpy as np - -from . import config, datatools, binica, pca, csp -from .var import VAR - - -def wrapper_binica(data): - """ Call binica for ICA calculation. - """ - w, s = binica.binica(datatools.cat_trials(data)) - u = s.dot(w) - m = np.linalg.inv(u) - return m, u - -def wrapper_pca(x, reducedim): - """ Call SCoT's PCA algorithm. - """ - c, d = pca.pca(datatools.cat_trials(x), subtract_mean=False, reducedim=reducedim) - y = datatools.dot_special(x, c) - return c, d, y - -def wrapper_csp(x, cl, reducedim): - c, d = csp.csp(x, cl, numcomp=reducedim) - y = datatools.dot_special(x,c) - return c, d, y - - -backend = { - 'ica': wrapper_binica, - 'pca': wrapper_pca, - 'csp': wrapper_csp, - 'var': VAR -} - - -def activate(): - config.backend = backend - - -activate() diff --git a/mne_sandbox/externals/scot/backend_sklearn.py b/mne_sandbox/externals/scot/backend_sklearn.py deleted file mode 100644 index 11273cc..0000000 --- a/mne_sandbox/externals/scot/backend_sklearn.py +++ /dev/null @@ -1,97 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Use scikit-learn routines as backend. -""" - -from __future__ import absolute_import - -import scipy as sp -from . import backend_builtin as builtin -from . import config, datatools -from .varbase import VARBase - - -def wrapper_fastica(data): - """ Call FastICA implementation from scikit-learn. - """ - from sklearn.decomposition import FastICA - ica = FastICA() - ica.fit(datatools.cat_trials(data)) - u = ica.components_.T - m = ica.mixing_.T - return m, u - - -def wrapper_pca(x, reducedim): - """ Call PCA implementation from scikit-learn. - """ - from sklearn.decomposition import PCA - pca = PCA(n_components=reducedim) - pca.fit(datatools.cat_trials(x)) - d = pca.components_ - c = pca.components_.T - y = datatools.dot_special(x,c) - return c, d, y - - -class VAR(VARBase): - """ Scikit-learn based implementation of VARBase. - - This class fits VAR models using various implementations of generalized linear model fitting available in scikit-learn. - - Parameters - ---------- - model_order : int - Autoregressive model order - fitobj : class, optional - Instance of a linear model implementation. - """ - def __init__(self, model_order, fitobj=None): - VARBase.__init__(self, model_order) - if fitobj is None: - from sklearn.linear_model import LinearRegression - fitobj = LinearRegression() - self.fitting_model = fitobj - - def fit(self, data): - """ Fit VAR model to data. - - Parameters - ---------- - data : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - Continuous or segmented data set. - - Returns - ------- - self : :class:`VAR` - The :class:`VAR` object. - """ - data = sp.atleast_3d(data) - (x, y) = self._construct_eqns(data) - self.fitting_model.fit(x, y) - - self.coef = self.fitting_model.coef_ - - self.residuals = data - self.predict(data) - self.rescov = sp.cov(datatools.cat_trials(self.residuals[self.p:, :, :]), rowvar=False) - - return self - - -backend = builtin.backend.copy() -backend.update({ - 'ica': wrapper_fastica, - 'pca': wrapper_pca, - 'var': VAR -}) - - -def activate(): - """ Set backend attribute in the config module. - """ - config.backend = backend - - -activate() diff --git a/mne_sandbox/externals/scot/binica.py b/mne_sandbox/externals/scot/binica.py deleted file mode 100644 index 2419b52..0000000 --- a/mne_sandbox/externals/scot/binica.py +++ /dev/null @@ -1,167 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -from __future__ import print_function - -from uuid import uuid4 -import os -import sys -import subprocess - -import numpy as np - -if not hasattr(__builtins__, 'FileNotFoundError'): - # PY27: subprocess.Popen raises OSError instead of FileNotFoundError - FileNotFoundError = OSError - - -binica_binary = os.path.dirname(os.path.abspath(__file__)) + '/binica/ica_linux' - -#noinspection PyNoneFunctionAssignment,PyTypeChecker -def binica(data, binary=binica_binary): - """ Simple wrapper for the BINICA binary. - - This function calculates the ICA transformation using the Infomax algorithm implemented in BINICA. - - BINICA is bundled with EEGLAB, or can be downloaded from here: - http://sccn.ucsd.edu/eeglab/binica/ - - This function attempts to automatically download and extract the BINICA binary. - - By default the binary is expected to be "binica/ica_linux" relative - to the directory where this module lies (typically scot/binica/ica_linux) - - Parameters - ---------- - data : array-like, shape = [n_samples, n_channels] - EEG data set - binary : str - Full path to the binica binary - - Returns - ------- - w : array, shape = [n_channels, n_channels] - ICA weights matrix - s : array, shape = [n_channels, n_channels] - Sphering matrix - - Notes - ----- - The unmixing matrix is obtained by multiplying U = dot(s, w) - """ - - check_binary_(binary) - - data = np.array(data, dtype=np.float32) - - nframes, nchans = data.shape - - uid = uuid4() - - scriptfile = 'binica-%s.sc' % uid - datafile = 'binica-%s.fdt' % uid - weightsfile = 'binica-%s.wts' % uid - #weightstmpfile = 'binicatmp-%s.wts' % uid - spherefile = 'binica-%s.sph' % uid - - config = {'DataFile': datafile, - 'WeightsOutFile': weightsfile, - 'SphereFile': spherefile, - 'chans': nchans, - 'frames': nframes, - 'extended': 1} - # config['WeightsTempFile'] = weightstmpfile - - # create data file - f = open(datafile, 'wb') - data.tofile(f) - f.close() - - # create script file - f = open(scriptfile, 'wt') - for h in config: - print(h, config[h], file=f) - f.close() - - # flush output streams otherwise things printed before might appear after the ICA output. - sys.stdout.flush() - sys.stderr.flush() - - if os.path.exists(binary): - with open(scriptfile) as sc: - try: - proc = subprocess.Popen(binary, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=sc) - print('waiting for binica to finish...') - proc.wait() - #print('binica output:') - #print(proc.stdout.read().decode()) - proc.stdout.close() - except FileNotFoundError: - raise RuntimeError('The BINICA binary ica_linux exists in the file system but could not be executed. ' - 'This indicates that 32 bit libraries are not installed on the system.') - else: - raise RuntimeError('the binary is not there!?') - - os.remove(scriptfile) - os.remove(datafile) - - # read weights - f = open(weightsfile, 'rb') - weights = np.fromfile(f, dtype=np.float32) - f.close() - weights = np.reshape(weights, (nchans,nchans)) - -# os.remove(weightstmpfile) - os.remove(weightsfile) - - # read sphering matrix - f = open( spherefile, 'rb' ) - sphere = np.fromfile(f, dtype=np.float32) - f.close() - sphere = np.reshape(sphere, (nchans,nchans)) - - os.remove(spherefile) - - return weights, sphere - - -def check_binary_(binary): - """check if binary is available, and try to download it if not""" - - if os.path.exists(binary): - print(binary, 'found') - return - - url = 'http://sccn.ucsd.edu/eeglab/binica/binica.zip' - print(binary+' not found. Trying to download from '+url) - - path = os.path.dirname(binary) - - if not os.path.exists(path): - os.makedirs(path) - - try: - # Python 3 - from urllib.request import urlretrieve as urlretrieve - except ImportError: - # Python 2.7 - from urllib import urlretrieve as urlretrieve - import zipfile - import stat - - urlretrieve(url, path + '/binica.zip') - - if not os.path.exists(path + '/binica.zip'): - raise RuntimeError('Error downloading binica.zip.') - - print('unzipping', path + '/binica.zip') - - with zipfile.ZipFile(path + '/binica.zip') as tgz: - tgz.extractall(path + '/..') - - if not os.path.exists(binary): - raise RuntimeError(binary + ' not found, even after extracting binica.zip.') - - mode = os.stat(binary).st_mode - os.chmod(binary, mode | stat.S_IXUSR) diff --git a/mne_sandbox/externals/scot/config.py b/mne_sandbox/externals/scot/config.py deleted file mode 100644 index 8aed27c..0000000 --- a/mne_sandbox/externals/scot/config.py +++ /dev/null @@ -1,8 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Global configuration -""" - -backend = {} diff --git a/mne_sandbox/externals/scot/connectivity.py b/mne_sandbox/externals/scot/connectivity.py deleted file mode 100644 index 0f40b03..0000000 --- a/mne_sandbox/externals/scot/connectivity.py +++ /dev/null @@ -1,369 +0,0 @@ -# coding=utf-8 - -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Connectivity Analysis """ - -import numpy as np -import scipy as sp -from scipy.fftpack import fft -from .utils import memoize - - -def connectivity(measure_names, b, c=None, nfft=512): - """ calculate connectivity measures. - - Parameters - ---------- - measure_names : {str, list of str} - Name(s) of the connectivity measure(s) to calculate. See :class:`Connectivity` for supported measures. - b : ndarray, shape = [n_channels, n_channels*model_order] - VAR model coefficients. See :ref:`var-model-coefficients` for details about the arrangement of coefficients. - c : ndarray, shape = [n_channels, n_channels], optional - Covariance matrix of the driving noise process. Identity matrix is used if set to None. - nfft : int, optional - Number of frequency bins to calculate. Note that these points cover the range between 0 and half the - sampling rate. - - Returns - ------- - result : ndarray, shape = [n_channels, n_channels, `nfft`] - An ndarray of shape [m, m, nfft] is returned if measures is a string. If measures is a list of strings a - dictionary is returned, where each key is the name of the measure, and the corresponding values are ndarrays - of shape [m, m, nfft]. - - Notes - ----- - When using this function it is more efficient to get several measures at once than calling the function multiple times. - - Examples - -------- - >>> c = connectivity(['DTF', 'PDC'], [[0.3, 0.6], [0.0, 0.9]]) - """ - con = Connectivity(b, c, nfft) - try: - return getattr(con, measure_names)() - except TypeError: - return dict((m, getattr(con, m)()) for m in measure_names) - - -#noinspection PyPep8Naming -class Connectivity: - """ Calculation of connectivity measures - - This class calculates various spectral connectivity measures from a vector autoregressive (VAR) model. - - Parameters - ---------- - b : ndarray, shape = [n_channels, n_channels*model_order] - VAR model coefficients. See :ref:`var-model-coefficients` for details about the arrangement of coefficients. - c : ndarray, shape = [n_channels, n_channels], optional - Covariance matrix of the driving noise process. Identity matrix is used if set to None. - nfft : int, optional - Number of frequency bins to calculate. Note that these points cover the range between 0 and half the - sampling rate. - - Methods - ------- - :func:`A` - Spectral representation of the VAR coefficients - :func:`H` - Transfer function that turns the innovation process into the VAR process - :func:`S` - Cross spectral density - :func:`logS` - Logarithm of the cross spectral density (S), for convenience. - :func:`G` - Inverse cross spectral density - :func:`logG` - Logarithm of the inverse cross spectral density - :func:`PHI` - Phase angle - :func:`COH` - Coherence - :func:`pCOH` - Partial coherence - :func:`PDC` - Partial directed coherence - :func:`ffPDC` - Full frequency partial directed coherence - :func:`PDCF` - PDC factor - :func:`GPDC` - Generalized partial directed coherence - :func:`DTF` - Directed transfer function - :func:`ffDTF` - Full frequency directed transfer function - :func:`dDTF` - Direct directed transfer function - :func:`GDTF` - Generalized directed transfer function - - Notes - ----- - Connectivity measures are returned by member functions that take no arguments and return a matrix of - shape [m,m,nfft]. The first dimension is the sink, the second dimension is the source, and the third dimension is - the frequency. - - A summary of most supported measures can be found in [1]_. - - References - ---------- - .. [1] M. Billinger et al, “Single-trial connectivity estimation for classification of motor imagery data”, - *J. Neural Eng.* 10, 2013. - """ - - def __init__(self, b, c=None, nfft=512): - b = np.asarray(b) - (m, mp) = b.shape - p = mp // m - if m * p != mp: - raise AttributeError('Second dimension of b must be an integer multiple of the first dimension.') - - if c is None: - self.c = None - else: - self.c = np.atleast_2d(c) - - self.b = np.reshape(b, (m, m, p), 'c') - self.m = m - self.p = p - self.nfft = nfft - - @memoize - def Cinv(self): - """ Inverse of the noise covariance - """ - try: - return np.linalg.inv(self.c) - except np.linalg.linalg.LinAlgError: - print('Warning: non invertible noise covariance matrix c!') - return np.eye(self.c.shape[0]) - - @memoize - def A(self): - """ Spectral VAR coefficients - - .. math:: \mathbf{A}(f) = \mathbf{I} - \sum_{k=1}^{p} \mathbf{a}^{(k)} \mathrm{e}^{-2\pi f} - """ - return fft(np.dstack([np.eye(self.m), -self.b]), self.nfft * 2 - 1)[:, :, :self.nfft] - - @memoize - def H(self): - """ VAR transfer function - - .. math:: \mathbf{H}(f) = \mathbf{A}(f)^{-1} - """ - return _inv3(self.A()) - - @memoize - def S(self): - """ Cross spectral density - - .. math:: \mathbf{S}(f) = \mathbf{H}(f) \mathbf{C} \mathbf{H}'(f) - """ - if self.c is None: - raise RuntimeError('Cross spectral density requires noise covariance matrix c.') - H = self.H() - #TODO can we do that more efficiently? - S = np.empty(H.shape, dtype=H.dtype) - for f in range(H.shape[2]): - S[:, :, f] = H[:, :, f].dot(self.c).dot(H[:, :, f].conj().T) - return S - - @memoize - def logS(self): - """ Logarithmic cross spectral density - - .. math:: \mathrm{logS}(f) = \log | \mathbf{S}(f) | - """ - return np.log10(np.abs(self.S())) - - @memoize - def absS(self): - """ Absolute cross spectral density - - .. math:: \mathrm{absS}(f) = | \mathbf{S}(f) | - """ - return np.abs(self.S()) - - @memoize - def G(self): - """ Inverse cross spectral density - - .. math:: \mathbf{G}(f) = \mathbf{A}(f) \mathbf{C}^{-1} \mathbf{A}'(f) - """ - if self.c is None: - raise RuntimeError('Inverse cross spectral density requires invertible noise covariance matrix c.') - A = self.A() - #TODO can we do that more efficiently? - G = np.einsum('ji..., jk... ->ik...', A.conj(), self.Cinv()) - G = np.einsum('ij..., jk... ->ik...', G, A) - return G - - @memoize - def logG(self): - """ Logarithmic inverse cross spectral density - - .. math:: \mathrm{logG}(f) = \log | \mathbf{G}(f) | - """ - return np.log10(np.abs(self.G())) - - @memoize - def COH(self): - """ Coherence - - .. math:: \mathrm{COH}_{ij}(f) = \\frac{S_{ij}(f)}{\sqrt{S_{ii}(f) S_{jj}(f)}} - - References - ---------- - P. L. Nunez, R. Srinivasan, A. F. Westdorp, R. S. Wijesinghe, D. M. Tucker, - R. B. Silverstein, P. J. Cadusch. EEG coherency: I: statistics, reference electrode, - volume conduction, Laplacians, cortical imaging, and interpretation at multiple scales. - Electroenceph. Clin. Neurophysiol. 103(5): 499-515, 1997. - """ - S = self.S() - #TODO can we do that more efficiently? - return S / np.sqrt(np.einsum('ii..., jj... ->ij...', S, S.conj())) - - @memoize - def PHI(self): - """ Phase angle - - Returns the phase angle of complex :func:`S`. - """ - return np.angle(self.S()) - - @memoize - def pCOH(self): - """ Partial coherence - - .. math:: \mathrm{pCOH}_{ij}(f) = \\frac{G_{ij}(f)}{\sqrt{G_{ii}(f) G_{jj}(f)}} - - References - ---------- - P. J. Franaszczuk, K. J. Blinowska, M. Kowalczyk. The application of parametric multichannel - spectral estimates in the study of electrical brain activity. Biol. Cybernetics 51(4): 239-247, 1985. - """ - G = self.G() - #TODO can we do that more efficiently? - return G / np.sqrt(np.einsum('ii..., jj... ->ij...', G, G)) - - @memoize - def PDC(self): - """ Partial directed coherence - - .. math:: \mathrm{PDC}_{ij}(f) = \\frac{A_{ij}(f)}{\sqrt{A_{:j}'(f) A_{:j}(f)}} - - References - ---------- - L. A. Baccalá, K. Sameshima. Partial directed coherence: a new concept in neural structure - determination. Biol. Cybernetics 84(6):463-474, 2001. - """ - A = self.A() - return np.abs(A / np.sqrt(np.sum(A.conj() * A, axis=0, keepdims=True))) - - @memoize - def ffPDC(self): - """ Full frequency partial directed coherence - - .. math:: \mathrm{ffPDC}_{ij}(f) = \\frac{A_{ij}(f)}{\sqrt{\sum_f A_{:j}'(f) A_{:j}(f)}} - """ - A = self.A() - return np.abs(A * self.nfft / np.sqrt(np.sum(A.conj() * A, axis=(0, 2), keepdims=True))) - - @memoize - def PDCF(self): - """ Partial directed coherence factor - - .. math:: \mathrm{PDCF}_{ij}(f) = \\frac{A_{ij}(f)}{\sqrt{A_{:j}'(f) \mathbf{C}^{-1} A_{:j}(f)}} - - References - ---------- - L. A. Baccalá, K. Sameshima. Partial directed coherence: a new concept in neural structure - determination. Biol. Cybernetics 84(6):463-474, 2001. - """ - A = self.A() - #TODO can we do that more efficiently? - return np.abs(A / np.sqrt(np.einsum('aj..., ab..., bj... ->j...', A.conj(), self.Cinv(), A))) - - @memoize - def GPDC(self): - """ Generalized partial directed coherence - - .. math:: \mathrm{GPDC}_{ij}(f) = \\frac{|A_{ij}(f)|} - {\sigma_i \sqrt{A_{:j}'(f) \mathrm{diag}(\mathbf{C})^{-1} A_{:j}(f)}} - - References - ---------- - L. Faes, S. Erla, G. Nollo. Measuring Connectivity in Linear Multivariate Processes: - Definitions, Interpretation, and Practical Analysis. Comput. Math. Meth. Med. 2012:140513, 2012. - """ - A = self.A() - return np.abs(A / np.sqrt(np.einsum('aj..., a..., aj..., ii... ->ij...', A.conj(), 1/np.diag(self.c), A, self.c))) - - @memoize - def DTF(self): - """ Directed transfer function - - .. math:: \mathrm{DTF}_{ij}(f) = \\frac{H_{ij}(f)}{\sqrt{H_{i:}(f) H_{i:}'(f)}} - - References - ---------- - M. J. Kaminski, K. J. Blinowska. A new method of the description of the information flow - in the brain structures. Biol. Cybernetics 65(3): 203-210, 1991. - """ - H = self.H() - return np.abs(H / np.sqrt(np.sum(H * H.conj(), axis=1, keepdims=True))) - - @memoize - def ffDTF(self): - """ Full frequency directed transfer function - - .. math:: \mathrm{ffDTF}_{ij}(f) = \\frac{H_{ij}(f)}{\sqrt{\sum_f H_{i:}(f) H_{i:}'(f)}} - - References - ---------- - A. Korzeniewska, M. Mańczak, M. Kaminski, K. J. Blinowska, S. Kasicki. Determination of - information flow direction among brain structures by a modified directed transfer - function (dDTF) method. J. Neurosci. Meth. 125(1-2): 195-207, 2003. - """ - H = self.H() - return np.abs(H * self.nfft / np.sqrt(np.sum(H * H.conj(), axis=(1, 2), keepdims=True))) - - @memoize - def dDTF(self): - """" Direct" directed transfer function - - .. math:: \mathrm{dDTF}_{ij}(f) = |\mathrm{pCOH}_{ij}(f)| \mathrm{ffDTF}_{ij}(f) - - References - ---------- - A. Korzeniewska, M. Mańczak, M. Kaminski, K. J. Blinowska, S. Kasicki. Determination of - information flow direction among brain structures by a modified directed transfer - function (dDTF) method. J. Neurosci. Meth. 125(1-2): 195-207, 2003. - """ - return np.abs(self.pCOH()) * self.ffDTF() - - @memoize - def GDTF(self): - """ Generalized directed transfer function - - .. math:: \mathrm{GPDC}_{ij}(f) = \\frac{\sigma_j |H_{ij}(f)|} - {\sqrt{H_{i:}(f) \mathrm{diag}(\mathbf{C}) H_{i:}'(f)}} - - References - ---------- - L. Faes, S. Erla, G. Nollo. Measuring Connectivity in Linear Multivariate Processes: - Definitions, Interpretation, and Practical Analysis. Comput. Math. Meth. Med. 2012:140513, 2012. - """ - H = self.H() - return np.abs(H / np.sqrt(np.einsum('ia..., aa..., ia..., j... ->ij...', H.conj(), self.c, H, 1/self.c.diagonal()))) - - -def _inv3(x): - identity = np.eye(x.shape[0]) - return np.array([sp.linalg.solve(a, identity) for a in x.T]).T diff --git a/mne_sandbox/externals/scot/connectivity_statistics.py b/mne_sandbox/externals/scot/connectivity_statistics.py deleted file mode 100644 index 53a34ae..0000000 --- a/mne_sandbox/externals/scot/connectivity_statistics.py +++ /dev/null @@ -1,299 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013-2014 SCoT Development Team - -""" Routines for statistical evaluation of connectivity. -""" - -from __future__ import division - -import numpy as np -import scipy as sp -from .datatools import randomize_phase -from .connectivity import connectivity -from .utils import cartesian -from .parallel import parallel_loop - - -def surrogate_connectivity(measure_names, data, var, nfft=512, repeats=100, - n_jobs=1, verbose=0): - """ Calculates surrogate connectivity for a multivariate time series by - phase randomization [1]_. - - .. note:: Parameter `var` will be modified by the function. Treat as - undefined after the function returned. - - Parameters - ---------- - measure_names : {str, list of str} - Name(s) of the connectivity measure(s) to calculate. See - :class:`Connectivity` for supported measures. - data : ndarray, shape = [n_samples, n_channels, (n_trials)] - Time series data (2D or 3D for multiple trials) - var : VARBase-like object - Instance of a VAR model. - nfft : int, optional - Number of frequency bins to calculate. Note that these points cover the - range between 0 and half the - sampling rate. - repeats : int, optional - How many surrogate samples to take. - n_jobs : int | None - number of jobs to run in parallel. See `joblib.Parallel` for details. - verbose : int - verbosity level passed to joblib. - - Returns - ------- - result : array, shape = [`repeats`, n_channels, n_channels, nfft] - Values of the connectivity measure for each surrogate. If - `measure_names` is a list of strings a dictionary - is returned, where each key is the name of the measure, and the - corresponding values are ndarrays of shape - [`repeats`, n_channels, n_channels, nfft]. - - .. [1] J. Theiler et al. "Testing for nonlinearity in time series: the - method of surrogate data", Physica D, vol 58, pp. 77-94, 1992 - """ - par, func = parallel_loop(_calc_surrogate, n_jobs=n_jobs, verbose=verbose) - output = par(func(randomize_phase(data), var, measure_names, nfft) - for _ in range(repeats)) - return convert_output_(output, measure_names) - - -def _calc_surrogate(data, var, measure_names, nfft): - var.fit(data) - return connectivity(measure_names, var.coef, var.rescov, nfft) - - -def jackknife_connectivity(measure_names, data, var, nfft=512, leaveout=1, - n_jobs=1, verbose=0): - """ Calculates Jackknife estimates of connectivity. - - For each Jackknife estimate a block of trials is left out. This is repeated - until each trial was left out exactly once. The number of estimates depends - on the number of trials and the value of `leaveout`. It is calculated by - repeats = `n_trials` // `leaveout`. - - .. note:: Parameter `var` will be modified by the function. Treat as - undefined after the function returned. - - Parameters - ---------- - measure_names : {str, list of str} - Name(s) of the connectivity measure(s) to calculate. See - :class:`Connectivity` for supported measures. - data : ndarray, shape = [n_samples, n_channels, (n_trials)] - Time series data (2D or 3D for multiple trials) - var : VARBase-like object - Instance of a VAR model. - nfft : int, optional - Number of frequency bins to calculate. Note that these points cover the - range between 0 and half the - sampling rate. - leaveout : int, optional - Number of trials to leave out in each estimate. - n_jobs : int | None - number of jobs to run in parallel. See `joblib.Parallel` for details. - verbose : int - verbosity level passed to joblib. - - Returns - ------- - result : array, shape = [`repeats`, n_channels, n_channels, nfft] - Values of the connectivity measure for each surrogate. If - `measure_names` is a list of strings a dictionary is returned, - where each key is the name of the measure, and the corresponding - values are ndarrays of shape - [`repeats`, n_channels, n_channels, nfft]. - """ - data = np.atleast_3d(data) - n, m, t = data.shape - - if leaveout < 1: - leaveout = int(leaveout * t) - - num_blocks = int(t / leaveout) - - mask = lambda block: [i for i in range(t) if i < block*leaveout or - i >= (block+1)*leaveout] - - par, func = parallel_loop(_calc_jackknife, n_jobs=n_jobs, verbose=verbose) - output = par(func(data[:, :, mask(b)], var, measure_names, nfft) - for b in range(num_blocks)) - return convert_output_(output, measure_names) - - -def _calc_jackknife(data_used, var, measure_names, nfft): - var.fit(data_used) - return connectivity(measure_names, var.coef, var.rescov, nfft) - - -def bootstrap_connectivity(measures, data, var, nfft=512, repeats=100, - num_samples=None, n_jobs=1, verbose=0): - """ Calculates Bootstrap estimates of connectivity. - - To obtain a bootstrap estimate trials are sampled randomly with replacement - from the data set. - - .. note:: Parameter `var` will be modified by the function. Treat as - undefined after the function returned. - - Parameters - ---------- - measure_names : {str, list of str} - Name(s) of the connectivity measure(s) to calculate. See - :class:`Connectivity` for supported measures. - data : ndarray, shape = [n_samples, n_channels, (n_trials)] - Time series data (2D or 3D for multiple trials) - var : VARBase-like object - Instance of a VAR model. - repeats : int, optional - How many bootstrap estimates to take. - num_samples : int, optional - How many samples to take for each bootstrap estimates. Defaults to the - same number of trials as present in the data. - n_jobs : int | None - number of jobs to run in parallel. See `joblib.Parallel` for details. - verbose : int - verbosity level passed to joblib. - - Returns - ------- - measure : array, shape = [`repeats`, n_channels, n_channels, nfft] - Values of the connectivity measure for each bootstrap estimate. If - `measure_names` is a list of strings a dictionary is returned, where - each key is the name of the measure, and the corresponding values are - ndarrays of shape [`repeats`, n_channels, n_channels, nfft]. - """ - data = np.atleast_3d(data) - n, m, t = data.shape - - if num_samples is None: - num_samples = t - - mask = lambda r: np.random.random_integers(0, data.shape[2]-1, num_samples) - - par, func = parallel_loop(_calc_bootstrap, n_jobs=n_jobs, verbose=verbose) - output = par(func(data[:, :, mask(r)], var, measures, nfft, num_samples) - for r in range(repeats)) - return convert_output_(output, measures) - - -def _calc_bootstrap(data, var, measures, nfft, num_samples): - var.fit(data) - return connectivity(measures, var.coef, var.rescov, nfft) - - -def test_bootstrap_difference(a, b): - """ Test mean difference between two bootstrap estimates. - - This function calculates the probability `p` of observing a more extreme - mean difference between `a` and `b` under the null hypothesis that `a` and - `b` come from the same distribution. - - If p is smaller than e.g. 0.05 we can reject the null hypothesis at an - alpha-level of 0.05 and conclude that `a` and `b` are likely to come from - different distributions. - - .. note:: *p*-values are calculated along the first dimension. Thus, - n_channels * n_channels * nfft individual *p*-values are - obtained. To determine if a difference is significant it is - important to correct for multiple testing. - - Parameters - ---------- - a, b : ndarray, shape = [`repeats`, n_channels, n_channels, nfft] - Two bootstrap estimates to compare. The number of repetitions (first - dimension) does not have be equal. - - Returns - ------- - p : ndarray, shape = [n_channels, n_channels, nfft] - *p*-values - - Notes - ----- - The function estimates the distribution of `b[j]` - `a[i]` by calculating - the difference for each combination of `i` and `j`. The total number of - difference samples available is therefore a.shape[0] * b.shape[0]. The - *p*-value is calculated as the smallest percentile of that distribution - that does not contain 0. - - See also - -------- - :func:`significance_fdr` : Correct for multiple testing by controlling the - false discovery rate. - """ - old_shape = a.shape[1:] - a = np.asarray(a).reshape((a.shape[0], -1)) - b = np.asarray(b).reshape((b.shape[0], -1)) - - n = a.shape[0] - - s1, s2 = 0, 0 - for i in cartesian((np.arange(n), np.arange(n))): - c = b[i[1], :] - a[i[0], :] - - s1 += c >= 0 - s2 += c <= 0 - - p = np.minimum(s1, s2) / (n*n) - - return p.reshape(old_shape) - - -def significance_fdr(p, alpha): - """ Calculate significance by controlling for the false discovery rate. - - This function determines which of the *p*-values in `p` can be considered - significant. Correction for multiple comparisons is performed by - controlling the false discovery rate (FDR). The FDR is the maximum fraction - of *p*-values that are wrongly considered significant [1]_. - - Parameters - ---------- - p : ndarray, shape = [n_channels, n_channels, nfft] - *p*-values - alpha : float - Maximum false discovery rate. - - Returns - ------- - s : ndarray, dtype=bool, shape = [n_channels, n_channels, nfft] - Significance of each *p*-value. - - References - ---------- - .. [1] Y. Benjamini, Y. Hochberg, "Controlling the false discovery rate: a - practical and powerful approach to multiple testing", Journal of the - Royal Statistical Society, Series B 57(1), pp 289-300, 1995 - """ - i = np.argsort(p, axis=None) - m = i.size - np.sum(np.isnan(p)) - - j = np.empty(p.shape, int) - j.flat[i] = np.arange(1, i.size+1) - - mask = p <= alpha*j/m - - if np.sum(mask) == 0: - return mask - - # find largest k so that p_k <= alpha*k/m - k = np.max(j[mask]) - - # reject all H_i for i = 0...k - s = j <= k - - return s - - -def convert_output_(output, measures): - if isinstance(measures, str): - return np.array(output) - else: - repeats = len(output) - output = dict((m, np.array([output[r][m] for r in range(repeats)])) - for m in measures) - return output \ No newline at end of file diff --git a/mne_sandbox/externals/scot/csp.py b/mne_sandbox/externals/scot/csp.py deleted file mode 100644 index c5a7f7a..0000000 --- a/mne_sandbox/externals/scot/csp.py +++ /dev/null @@ -1,73 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -"""common spatial patterns (CSP) implementation""" - -import numpy as np -from scipy.linalg import eig - - -def csp(x, cl, numcomp=np.inf): - """ Calculate common spatial patterns (CSP) - - Parameters - ---------- - x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - EEG data set - cl : list of valid dict keys - Class labels associated with each trial. Currently only two classes are supported. - numcomp : {int}, optional - Number of patterns to keep after applying the CSP. If `numcomp` is greater than n_channels, all n_channels - patterns are returned. - - Returns - ------- - w : array, shape = [n_channels, n_components] - CSP weight matrix - v : array, shape = [n_components, n_channels] - CSP projection matrix - """ - - x = np.atleast_3d(x) - cl = np.asarray(cl).ravel() - - n, m, t = x.shape - - if t != cl.size: - raise AttributeError('CSP only works with multiple classes. Number of' - ' elemnts in cl (%d) must equal 3rd dimension of X (%d)' % (cl.size, t)) - - labels = np.unique(cl) - - if labels.size != 2: - raise AttributeError('CSP is currently ipmlemented for 2 classes (got %d)' % labels.size) - - x1 = x[:, :, cl == labels[0]] - x2 = x[:, :, cl == labels[1]] - - sigma1 = np.zeros((m, m)) - for t in range(x1.shape[2]): - sigma1 += np.cov(x1[:, :, t].transpose()) / x1.shape[2] - sigma1 /= sigma1.trace() - - sigma2 = np.zeros((m, m)) - for t in range(x2.shape[2]): - sigma2 += np.cov(x2[:, :, t].transpose()) / x2.shape[2] - sigma2 /= sigma2.trace() - - e, w = eig(sigma1, sigma1 + sigma2, overwrite_a=True, overwrite_b=True, check_finite=False) - - order = np.argsort(e)[::-1] - w = w[:, order] - # e = e[order] - - v = np.linalg.inv(w) - - # subsequently remove unwanted components from the middle of w and v - while w.shape[1] > numcomp: - i = int(np.floor(w.shape[1]/2)) - w = np.delete(w, i, 1) - v = np.delete(v, i, 0) - - return w, v diff --git a/mne_sandbox/externals/scot/datatools.py b/mne_sandbox/externals/scot/datatools.py deleted file mode 100644 index 783ee8f..0000000 --- a/mne_sandbox/externals/scot/datatools.py +++ /dev/null @@ -1,153 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" -Summary -------- -Tools for basic data manipulation. -""" - -import numpy as np - - -def cut_segments(rawdata, tr, start, stop): - """ Cut continuous signal into segments. - - This function cuts segments from a continuous signal. Segments are stop - start samples long. - - Parameters - ---------- - rawdata : array_like - Input data of shape [`n`,`m`], with `n` samples and `m` signals. - tr : list of int - Trigger positions. - start : int - Window start (offset relative to trigger) - stop : int - Window end (offset relative to trigger) - - Returns - ------- - x : ndarray - Segments cut from `rawdata`. Individual segments are stacked along the third dimension. - - See also - -------- - cat_trials : Concatenate segments - - Examples - -------- - >>> data = np.random.randn(1000, 5) - >>> tr = [250, 500, 750] - >>> x = cut_segments(data, tr, 50, 100) - >>> x.shape - (50, 5, 3) - """ - rawdata = np.atleast_2d(rawdata) - tr = np.array(tr, dtype='int').ravel() - win = range(start, stop) - return np.dstack([rawdata[tr[t] + win, :] for t in range(len(tr))]) - - -def cat_trials(x): - """ Concatenate trials along time axis. - - Parameters - ---------- - x : array_like - Segmented input data of shape [`n`,`m`,`t`], with `n` time samples, `m` signals, and `t` trials. - - Returns - ------- - out : ndarray - Trials are concatenated along the first (time) axis. Shape of the output is [`n``t`,`m`]. - - See also - -------- - cut_segments : Cut segments from continuous data - - Examples - -------- - >>> x = np.random.randn(150, 4, 6) - >>> y = cat_trials(x) - >>> y.shape - (900, 4) - """ - x = np.atleast_3d(x) - t = x.shape[2] - return np.squeeze(np.vstack(np.dsplit(x, t)), axis=2) - - -def dot_special(x, a): - """ Trial-wise dot product. - - This function calculates the dot product of `x[:,:,i]` with `a` for each `i`. - - Parameters - ---------- - x : array_like - Segmented input data of shape [`n`,`m`,`t`], with `n` time samples, `m` signals, and `t` trials. - The dot product is calculated for each trial. - a : array_like - Second argument - - Returns - ------- - out : ndarray - Returns the dot product of each trial. - - Examples - -------- - >>> x = np.random.randn(150, 40, 6) - >>> a = np.ones((40, 7)) - >>> y = dot_special(x, a) - >>> y.shape - (150, 7, 6) - """ - x = np.atleast_3d(x) - a = np.atleast_2d(a) - return np.dstack([x[:, :, i].dot(a) for i in range(x.shape[2])]) - - -def randomize_phase(data): - """ Phase randomization. - - This function randomizes the input array's spectral phase along the first dimension. - - Parameters - ---------- - data : array_like - Input array - - Returns - ------- - out : ndarray - Array of same shape as `data`. - - Notes - ----- - The algorithm randomizes the phase component of the input's complex fourier transform. - - Examples - -------- - .. plot:: - :include-source: - - from pylab import * - from scot.datatools import randomize_phase - np.random.seed(1234) - s = np.sin(np.linspace(0,10*np.pi,1000)).T - x = np.vstack([s, np.sign(s)]).T - y = randomize_phase(x) - subplot(2,1,1) - title('Phase randomization of sine wave and rectangular function') - plot(x), axis([0,1000,-3,3]) - subplot(2,1,2) - plot(y), axis([0,1000,-3,3]) - plt.show() - """ - data = np.asarray(data) - data_freq = np.fft.rfft(data, axis=0) - data_freq = np.abs(data_freq) * np.exp(1j*np.random.random_sample(data_freq.shape)*2*np.pi) - return np.fft.irfft(data_freq, data.shape[0], axis=0) \ No newline at end of file diff --git a/mne_sandbox/externals/scot/matfiles.py b/mne_sandbox/externals/scot/matfiles.py deleted file mode 100644 index 2d9bcb5..0000000 --- a/mne_sandbox/externals/scot/matfiles.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Summary -------- -Routines for loading and saving Matlab's .mat files. -""" - -from scipy.io import loadmat as sploadmat -from scipy.io import savemat as spsavemat -from scipy.io import matlab - - -def loadmat(filename): - """This function should be called instead of direct spio.loadmat - as it cures the problem of not properly recovering python dictionaries - from mat files. It calls the function check keys to cure all entries - which are still mat-objects - """ - data = sploadmat(filename, struct_as_record=False, squeeze_me=True) - return _check_keys(data) - - -savemat = spsavemat - - -def _check_keys(dictionary): - """ - checks if entries in dictionary are mat-objects. If yes - todict is called to change them to nested dictionaries - """ - for key in dictionary: - if isinstance(dictionary[key], matlab.mio5_params.mat_struct): - dictionary[key] = _todict(dictionary[key]) - return dictionary - - -def _todict(matobj): - """ - a recursive function which constructs from matobjects nested dictionaries - """ - dictionary = {} - #noinspection PyProtectedMember - for strg in matobj._fieldnames: - elem = matobj.__dict__[strg] - if isinstance(elem, matlab.mio5_params.mat_struct): - dictionary[strg] = _todict(elem) - else: - dictionary[strg] = elem - return dictionary - diff --git a/mne_sandbox/externals/scot/ooapi.py b/mne_sandbox/externals/scot/ooapi.py deleted file mode 100644 index 574818c..0000000 --- a/mne_sandbox/externals/scot/ooapi.py +++ /dev/null @@ -1,802 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" -Summary -------- -Object oriented API to SCoT. - -Extended Summary ----------------- -The object oriented API provides a the `Workspace` class, which provides high-level functionality and serves as an -example usage of the low-level API. -""" - -import numpy as np - -from . import config -from .varica import mvarica, cspvarica -from .plainica import plainica -from .datatools import dot_special -from .connectivity import Connectivity -from .connectivity_statistics import surrogate_connectivity, bootstrap_connectivity, test_bootstrap_difference -from .connectivity_statistics import significance_fdr - - -class Workspace: - """SCoT Workspace - - This class provides high-level functionality for source identification, connectivity estimation, and visualization. - - Parameters - ---------- - var : {:class:`~scot.var.VARBase`-like object, dict} - Vector autoregressive model (VAR) object that is used for model fitting. - This can also be a dictionary that is passed as `**kwargs` to backend['var']() in order to - construct a new VAR model object. - locations : array_like, optional - 3D Electrode locations. Each row holds the x, y, and z coordinates of an electrode. - reducedim : {int, float, 'no_pca'}, optional - A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All - components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. - An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. - If set to 'no_pca' the PCA step is skipped. - nfft : int, optional - Number of frequency bins for connectivity estimation. - backend : dict-like, optional - Specify backend to use. When set to None the backend configured in config.backend is used. - - Attributes - ---------- - `unmixing_` : array - Estimated unmixing matrix. - `mixing_` : array - Estimated mixing matrix. - `plot_diagonal` : str - Configures what is plotted in the diagonal subplots. - **'topo'** (default) plots topoplots on the diagonal, - **'S'** plots the spectral density of each component, and - **'fill'** plots connectivity on the diagonal. - `plot_outside_topo` : bool - Whether to place topoplots in the left column and top row. - `plot_f_range` : (int, int) - Lower and upper frequency limits for plotting. Defaults to [0, fs/2]. - """ - def __init__(self, var, locations=None, reducedim=0.99, nfft=512, fs=2, backend=None): - self.data_ = None - self.cl_ = None - self.fs_ = fs - self.time_offset_ = 0 - self.unmixing_ = None - self.mixing_ = None - self.premixing_ = None - self.activations_ = None - self.connectivity_ = None - self.locations_ = locations - self.reducedim_ = reducedim - self.nfft_ = nfft - self.backend_ = backend - - self.trial_mask_ = [] - - self.topo_ = None - self.mixmaps_ = [] - self.unmixmaps_ = [] - - self.var_multiclass_ = None - self.var_model_ = None - self.var_cov_ = None - - self.plot_diagonal = 'topo' - self.plot_outside_topo = False - self.plot_f_range = [0, fs/2] - - self._plotting = None - - if self.backend_ is None: - self.backend_ = config.backend - - try: - self.var_ = self.backend_['var'](**var) - except TypeError: - self.var_ = var - - def __str__(self): - if self.data_ is not None: - datastr = '%d samples, %d channels, %d trials' % self.data_.shape - else: - datastr = 'None' - - if self.cl_ is not None: - clstr = str(np.unique(self.cl_)) - else: - clstr = 'None' - - if self.unmixing_ is not None: - sourcestr = str(self.unmixing_.shape[1]) - else: - sourcestr = 'None' - - if self.var_ is None: - varstr = 'None' - else: - varstr = str(self.var_) - - s = 'Workspace:\n' - s += ' Data : ' + datastr + '\n' - s += ' Classes : ' + clstr + '\n' - s += ' Sources : ' + sourcestr + '\n' - s += ' VAR models: ' + varstr + '\n' - - return s - - def set_locations(self, locations): - """ Set sensor locations. - - Parameters - ---------- - locations : array_like - 3D Electrode locations. Each row holds the x, y, and z coordinates of an electrode. - """ - self.locations_ = locations - - def set_premixing(self, premixing): - """ Set premixing matrix. - - The premixing matrix maps data to physical channels. If the data is actual channel data, - the premixing matrix can be set to identity. Use this functionality if the data was pre- - transformed with e.g. PCA. - - Parameters - ---------- - premixing : array_like, shape = [n_signals, n_channels] - Matrix that maps data signals to physical channels. - """ - self.premixing_ = premixing - - def set_data(self, data, cl=None, time_offset=0): - """ Assign data to the workspace. - - This function assigns a new data set to the workspace. Doing so invalidates currently fitted VAR models, - connectivity estimates, and activations. - - Parameters - ---------- - data : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - EEG data set - cl : list of valid dict keys - Class labels associated with each trial. - time_offset : float, optional - Trial starting time; used for labelling the x-axis of time/frequency plots. - """ - self.data_ = np.atleast_3d(data) - self.cl_ = np.asarray(cl if cl is not None else [None]*self.data_.shape[2]) - self.time_offset_ = time_offset - self.var_model_ = None - self.var_cov_ = None - self.connectivity_ = None - - self.trial_mask_ = np.ones(self.cl_.size, dtype=bool) - - if self.unmixing_ is not None: - self.activations_ = dot_special(self.data_, self.unmixing_) - - def set_used_labels(self, labels): - """ Specify which trials to use in subsequent analysis steps. - - This function masks trials based on their class labels. - - Parameters - ---------- - labels : list of class labels - Marks all trials that have a label that is in the `labels` list for further processing. - """ - mask = np.zeros(self.cl_.size, dtype=bool) - for l in labels: - mask = np.logical_or(mask, self.cl_ == l) - self.trial_mask_ = mask - - def do_mvarica(self, varfit='ensemble'): - """ Perform MVARICA - - Perform MVARICA source decomposition and VAR model fitting. - - Parameters - ---------- - varfit : string - Determines how to calculate the residuals for source decomposition. - 'ensemble' (default) fits one model to the whole data set, - 'class' fits a different model for each class, and - 'trial' fits a different model for each individual trial. - - Returns - ------- - result : class - see :func:`mvarica` for a description of the return value. - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain data. - - See Also - -------- - :func:`mvarica` : MVARICA implementation - """ - if self.data_ is None: - raise RuntimeError("MVARICA requires data to be set") - result = mvarica(x=self.data_[:, :, self.trial_mask_], cl=self.cl_[self.trial_mask_], var=self.var_, - reducedim=self.reducedim_, backend=self.backend_, varfit=varfit) - self.mixing_ = result.mixing - self.unmixing_ = result.unmixing - self.var_ = result.b - self.connectivity_ = Connectivity(result.b.coef, result.b.rescov, self.nfft_) - self.activations_ = dot_special(self.data_, self.unmixing_) - self.mixmaps_ = [] - self.unmixmaps_ = [] - return result - - def do_cspvarica(self, varfit='ensemble'): - """ Perform CSPVARICA - - Perform CSPVARICA source decomposition and VAR model fitting. - - Parameters - ---------- - varfit : string - Determines how to calculate the residuals for source decomposition. - 'ensemble' (default) fits one model to the whole data set, - 'class' fits a different model for each class, and - 'trial' fits a different model for each individual trial. - - Returns - ------- - result : class - see :func:`cspvarica` for a description of the return value. - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain data. - - See Also - -------- - :func:`cspvarica` : CSPVARICA implementation - """ - if self.data_ is None: - raise RuntimeError("CSPVARICA requires data to be set") - try: - sorted(self.cl_) - for c in self.cl_: - assert(c is not None) - except (TypeError, AssertionError): - raise RuntimeError("CSPVARICA requires orderable and hashable class labels that are not None") - result = cspvarica(x=self.data_, var=self.var_, cl=self.cl_, - reducedim=self.reducedim_, backend=self.backend_, varfit=varfit) - self.mixing_ = result.mixing - self.unmixing_ = result.unmixing - self.var_ = result.b - self.connectivity_ = Connectivity(self.var_.coef, self.var_.rescov, self.nfft_) - self.activations_ = dot_special(self.data_, self.unmixing_) - self.mixmaps_ = [] - self.unmixmaps_ = [] - return result - - def do_ica(self): - """ Perform ICA - - Perform plain ICA source decomposition. - - Returns - ------- - result : class - see :func:`plainica` for a description of the return value. - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain data. - """ - if self.data_ is None: - raise RuntimeError("ICA requires data to be set") - result = plainica(x=self.data_[:, :, self.trial_mask_], reducedim=self.reducedim_, backend=self.backend_) - self.mixing_ = result.mixing - self.unmixing_ = result.unmixing - self.activations_ = dot_special(self.data_, self.unmixing_) - self.var_model_ = None - self.var_cov_ = None - self.connectivity_ = None - self.mixmaps_ = [] - self.unmixmaps_ = [] - return result - - def remove_sources(self, sources): - """ Remove sources from the decomposition. - - This function removes sources from the decomposition. Doing so invalidates currently fitted VAR models and - connectivity estimates. - - Parameters - ---------- - sources : {slice, int, array of ints} - Indices of components to remove. - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain a source decomposition. - """ - if self.unmixing_ is None or self.mixing_ is None: - raise RuntimeError("No sources available (run do_mvarica first)") - self.mixing_ = np.delete(self.mixing_, sources, 0) - self.unmixing_ = np.delete(self.unmixing_, sources, 1) - if self.activations_ is not None: - self.activations_ = np.delete(self.activations_, sources, 1) - self.var_model_ = None - self.var_cov_ = None - self.connectivity_ = None - self.mixmaps_ = [] - self.unmixmaps_ = [] - - def fit_var(self): - """ Fit a var model to the source activations. - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain source activations. - """ - if self.activations_ is None: - raise RuntimeError("VAR fitting requires source activations (run do_mvarica first)") - self.var_.fit(data=self.activations_[:, :, self.trial_mask_]) - self.connectivity_ = Connectivity(self.var_.coef, self.var_.rescov, self.nfft_) - - def optimize_var(self): - """ Optimize the var model's hyperparameters (such as regularization). - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain source activations. - """ - if self.activations_ is None: - raise RuntimeError("VAR fitting requires source activations (run do_mvarica first)") - - self.var_.optimize(self.activations_[:, :, self.trial_mask_]) - - def get_connectivity(self, measure_name, plot=False): - """ Calculate spectral connectivity measure. - - Parameters - ---------- - measure_name : str - Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. - plot : {False, None, Figure object}, optional - Whether and where to plot the connectivity. If set to **False**, nothing is plotted. Otherwise set to the - Figure object. If set to **None**, a new figure is created. - - Returns - ------- - measure : array, shape = [n_channels, n_channels, nfft] - Values of the connectivity measure. - fig : Figure object - Instance of the figure in which was plotted. This is only returned if `plot` is not **False**. - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain a fitted VAR model. - """ - if self.connectivity_ is None: - raise RuntimeError("Connectivity requires a VAR model (run do_mvarica or fit_var first)") - - cm = getattr(self.connectivity_, measure_name)() - - cm = np.abs(cm) if np.any(np.iscomplex(cm)) else cm - - if plot is None or plot: - fig = plot - if self.plot_diagonal == 'fill': - diagonal = 0 - elif self.plot_diagonal == 'S': - diagonal = -1 - sm = np.abs(self.connectivity_.S()) - sm /= np.max(sm) # scale to 1 since components are scaled arbitrarily anyway - fig = self.plotting.plot_connectivity_spectrum(sm, fs=self.fs_, freq_range=self.plot_f_range, - diagonal=1, border=self.plot_outside_topo, fig=fig) - else: - diagonal = -1 - - fig = self.plotting.plot_connectivity_spectrum(cm, fs=self.fs_, freq_range=self.plot_f_range, - diagonal=diagonal, border=self.plot_outside_topo, fig=fig) - - return cm, fig - - return cm - - def get_surrogate_connectivity(self, measure_name, repeats=100, plot=False): - """ Calculate spectral connectivity measure under the assumption of no actual connectivity. - - Repeatedly samples connectivity from phase-randomized data. This provides estimates of the connectivity - distribution if there was no causal structure in the data. - - Parameters - ---------- - measure_name : str - Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. - repeats : int, optional - How many surrogate samples to take. - - Returns - ------- - measure : array, shape = [`repeats`, n_channels, n_channels, nfft] - Values of the connectivity measure for each surrogate. - - See Also - -------- - :func:`scot.connectivity_statistics.surrogate_connectivity` : Calculates surrogate connectivity - """ - cs = surrogate_connectivity(measure_name, self.activations_[:, :, self.trial_mask_], - self.var_, self.nfft_, repeats) - - if plot is None or plot: - fig = plot - if self.plot_diagonal == 'fill': - diagonal = 0 - elif self.plot_diagonal == 'S': - diagonal = -1 - sb = self.get_surrogate_connectivity('absS', repeats) - sb /= np.max(sb) # scale to 1 since components are scaled arbitrarily anyway - su = np.percentile(sb, 95, axis=0) - fig = self.plotting.plot_connectivity_spectrum([su], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=1, border=self.plot_outside_topo, fig=fig) - else: - diagonal = -1 - cu = np.percentile(cs, 95, axis=0) - fig = self.plotting.plot_connectivity_spectrum([cu], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=diagonal, border=self.plot_outside_topo, fig=fig) - return cs, fig - - return cs - - def get_bootstrap_connectivity(self, measure_names, repeats=100, num_samples=None, plot=False): - """ Calculate bootstrap estimates of spectral connectivity measures. - - Bootstrapping is performed on trial level. - - Parameters - ---------- - measure_names : {str, list of str} - Name(s) of the connectivity measure(s) to calculate. See :class:`Connectivity` for supported measures. - repeats : int, optional - How many bootstrap estimates to take. - num_samples : int, optional - How many samples to take for each bootstrap estimates. Defaults to the same number of trials as present in - the data. - - Returns - ------- - measure : array, shape = [`repeats`, n_channels, n_channels, nfft] - Values of the connectivity measure for each bootstrap estimate. If `measure_names` is a list of strings a - dictionary is returned, where each key is the name of the measure, and the corresponding values are - ndarrays of shape [`repeats`, n_channels, n_channels, nfft]. - - See Also - -------- - :func:`scot.connectivity_statistics.bootstrap_connectivity` : Calculates bootstrap connectivity - """ - if num_samples is None: - num_samples = np.sum(self.trial_mask_) - - cb = bootstrap_connectivity(measure_names, self.activations_[:, :, self.trial_mask_], - self.var_, self.nfft_, repeats, num_samples) - - if plot is None or plot: - fig = plot - if self.plot_diagonal == 'fill': - diagonal = 0 - elif self.plot_diagonal == 'S': - diagonal = -1 - sb = self.get_bootstrap_connectivity('absS', repeats, num_samples) - sb /= np.max(sb) # scale to 1 since components are scaled arbitrarily anyway - sm = np.median(sb, axis=0) - sl = np.percentile(sb, 2.5, axis=0) - su = np.percentile(sb, 97.5, axis=0) - fig = self.plotting.plot_connectivity_spectrum([sm, sl, su], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=1, border=self.plot_outside_topo, fig=fig) - else: - diagonal = -1 - cm = np.median(cb, axis=0) - cl = np.percentile(cb, 2.5, axis=0) - cu = np.percentile(cb, 97.5, axis=0) - fig = self.plotting.plot_connectivity_spectrum([cm, cl, cu], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=diagonal, border=self.plot_outside_topo, fig=fig) - return cb, fig - - return cb - - def get_tf_connectivity(self, measure_name, winlen, winstep, plot=False, crange='default'): - """ Calculate estimate of time-varying connectivity. - - Connectivity is estimated in a sliding window approach on the current data set. The window is stepped - `n_steps` = (`n_samples` - `winlen`) // `winstep` times. - - Parameters - ---------- - measure_name : str - Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. - winlen : int - Length of the sliding window (in samples). - winstep : int - Step size for sliding window (in samples). - plot : {False, None, Figure object}, optional - Whether and where to plot the connectivity. If set to **False**, nothing is plotted. Otherwise set to the - Figure object. If set to **None**, a new figure is created. - - Returns - ------- - result : array, shape = [n_channels, n_channels, nfft, n_steps] - Values of the connectivity measure. - fig : Figure object, optional - Instance of the figure in which was plotted. This is only returned if `plot` is not **False**. - - Raises - ------ - RuntimeError - If the :class:`Workspace` instance does not contain a fitted VAR model. - """ - if self.activations_ is None: - raise RuntimeError("Time/Frequency Connectivity requires activations (call set_data after do_mvarica)") - [n, m, _] = self.activations_.shape - - nstep = (n - winlen) // winstep - - result = np.zeros((m, m, self.nfft_, nstep), np.complex64) - i = 0 - for j in range(0, n - winlen, winstep): - win = np.arange(winlen) + j - data = self.activations_[win, :, :] - data = data[:, :, self.trial_mask_] - self.var_.fit(data) - con = Connectivity(self.var_.coef, self.var_.rescov, self.nfft_) - result[:, :, :, i] = getattr(con, measure_name)() - i += 1 - - if plot is None or plot: - fig = plot - t0 = 0.5 * winlen / self.fs_ + self.time_offset_ - t1 = self.data_.shape[0] / self.fs_ - 0.5 * winlen / self.fs_ + self.time_offset_ - if self.plot_diagonal == 'fill': - diagonal = 0 - elif self.plot_diagonal == 'S': - diagonal = -1 - s = np.abs(self.get_tf_connectivity('S', winlen, winstep)) - if crange == 'default': - crange = [np.min(s), np.max(s)] - fig = self.plotting.plot_connectivity_timespectrum(s, fs=self.fs_, crange=[np.min(s), np.max(s)], - freq_range=self.plot_f_range, time_range=[t0, t1], - diagonal=1, border=self.plot_outside_topo, fig=fig) - else: - diagonal = -1 - - tfc = self._clean_measure(measure_name, result) - if crange == 'default': - if diagonal == -1: - for m in range(tfc.shape[0]): - tfc[m, m, :, :] = 0 - crange = [np.min(tfc), np.max(tfc)] - fig = self.plotting.plot_connectivity_timespectrum(tfc, fs=self.fs_, crange=crange, - freq_range=self.plot_f_range, time_range=[t0, t1], - diagonal=diagonal, border=self.plot_outside_topo, fig=fig) - - return result, fig - - return result - - def compare_conditions(self, labels1, labels2, measure_name, alpha=0.01, repeats=100, num_samples=None, plot=False): - """ Test for significant difference in connectivity of two sets of class labels. - - Connectivity estimates are obtained by bootstrapping. Correction for multiple testing is performed by - controlling the false discovery rate (FDR). - - Parameters - ---------- - labels1, labels2 : list of class labels - The two sets of class labels to compare. Each set may contain more than one label. - measure_name : str - Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. - alpha : float, optional - Maximum allowed FDR. The ratio of falsely detected significant differences is guaranteed to be less than - `alpha`. - repeats : int, optional - How many bootstrap estimates to take. - num_samples : int, optional - How many samples to take for each bootstrap estimates. Defaults to the same number of trials as present in - the data. - plot : {False, None, Figure object}, optional - Whether and where to plot the connectivity. If set to **False**, nothing is plotted. Otherwise set to the - Figure object. If set to **None**, a new figure is created. - - Returns - ------- - p : array, shape = [n_channels, n_channels, nfft] - Uncorrected p-values. - s : array, dtype=bool, shape = [n_channels, n_channels, nfft] - FDR corrected significance. True means the difference is significant in this location. - fig : Figure object, optional - Instance of the figure in which was plotted. This is only returned if `plot` is not **False**. - """ - self.set_used_labels(labels1) - ca = self.get_bootstrap_connectivity(measure_name, repeats, num_samples) - self.set_used_labels(labels2) - cb = self.get_bootstrap_connectivity(measure_name, repeats, num_samples) - - p = test_bootstrap_difference(ca, cb) - s = significance_fdr(p, alpha) - - if plot is None or plot: - fig = plot - if self.plot_diagonal == 'topo': - diagonal = -1 - elif self.plot_diagonal == 'fill': - diagonal = 0 - elif self.plot_diagonal is 'S': - diagonal = -1 - self.set_used_labels(labels1) - sa = self.get_bootstrap_connectivity('absS', repeats, num_samples) - sm = np.median(sa, axis=0) - sl = np.percentile(sa, 2.5, axis=0) - su = np.percentile(sa, 97.5, axis=0) - fig = self.plotting.plot_connectivity_spectrum([sm, sl, su], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=1, border=self.plot_outside_topo, fig=fig) - - self.set_used_labels(labels2) - sb = self.get_bootstrap_connectivity('absS', repeats, num_samples) - sm = np.median(sb, axis=0) - sl = np.percentile(sb, 2.5, axis=0) - su = np.percentile(sb, 97.5, axis=0) - fig = self.plotting.plot_connectivity_spectrum([sm, sl, su], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=1, border=self.plot_outside_topo, fig=fig) - - p_s = test_bootstrap_difference(ca, cb) - s_s = significance_fdr(p_s, alpha) - - self.plotting.plot_connectivity_significance(s_s, fs=self.fs_, freq_range=self.plot_f_range, - diagonal=1, border=self.plot_outside_topo, fig=fig) - else: - diagonal = -1 - - cm = np.median(ca, axis=0) - cl = np.percentile(ca, 2.5, axis=0) - cu = np.percentile(ca, 97.5, axis=0) - - fig = self.plotting.plot_connectivity_spectrum([cm, cl, cu], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=diagonal, border=self.plot_outside_topo, fig=fig) - - cm = np.median(cb, axis=0) - cl = np.percentile(cb, 2.5, axis=0) - cu = np.percentile(cb, 97.5, axis=0) - - fig = self.plotting.plot_connectivity_spectrum([cm, cl, cu], fs=self.fs_, freq_range=self.plot_f_range, - diagonal=diagonal, border=self.plot_outside_topo, fig=fig) - - self.plotting.plot_connectivity_significance(s, fs=self.fs_, freq_range=self.plot_f_range, - diagonal=diagonal, border=self.plot_outside_topo, fig=fig) - - return p, s, fig - - return p, s - - def show_plots(self): - """Show current plots. - - This is only a convenience wrapper around :func:`matplotlib.pyplot.show_plots`. - - """ - self.plotting.show_plots() - - def plot_source_topos(self, common_scale=None): - """ Plot topography of the Source decomposition. - - Parameters - ---------- - common_scale : float, optional - If set to None, each topoplot's color axis is scaled individually. Otherwise specifies the percentile - (1-99) of values in all plot. This value is taken as the maximum color scale. - """ - if self.unmixing_ is None and self.mixing_ is None: - raise RuntimeError("No sources available (run do_mvarica first)") - - self._prepare_plots(True, True) - - self.plotting.plot_sources(self.topo_, self.mixmaps_, self.unmixmaps_, common_scale) - - def plot_connectivity_topos(self, fig=None): - """ Plot scalp projections of the sources. - - This function only plots the topos. Use in combination with connectivity plotting. - - Parameters - ---------- - fig : {None, Figure object}, optional - Where to plot the topos. f set to **None**, a new figure is created. Otherwise plot into the provided - figure object. - - Returns - ------- - fig : Figure object - Instance of the figure in which was plotted. - """ - self._prepare_plots(True, False) - if self.plot_outside_topo: - fig = self.plotting.plot_connectivity_topos('outside', self.topo_, self.mixmaps_, fig) - elif self.plot_diagonal == 'topo': - fig = self.plotting.plot_connectivity_topos('diagonal', self.topo_, self.mixmaps_, fig) - return fig - - def plot_connectivity_surrogate(self, measure_name, repeats=100, fig=None): - """ Plot spectral connectivity measure under the assumption of no actual connectivity. - - Repeatedly samples connectivity from phase-randomized data. This provides estimates of the connectivity - distribution if there was no causal structure in the data. - - Parameters - ---------- - measure_name : str - Name of the connectivity measure to calculate. See :class:`Connectivity` for supported measures. - repeats : int, optional - How many surrogate samples to take. - fig : {None, Figure object}, optional - Where to plot the topos. f set to **None**, a new figure is created. Otherwise plot into the provided - figure object. - - Returns - ------- - fig : Figure object - Instance of the figure in which was plotted. - """ - cb = self.get_surrogate_connectivity(measure_name, repeats) - - self._prepare_plots(True, False) - - cu = np.percentile(cb, 95, axis=0) - - fig = self.plotting.plot_connectivity_spectrum([cu], self.fs_, freq_range=self.plot_f_range, fig=fig) - - return fig - - @property - def plotting(self): - if not self._plotting: - from . import plotting - self._plotting = plotting - return self._plotting - - def _prepare_plots(self, mixing=False, unmixing=False): - if self.locations_ is None: - raise RuntimeError("Need sensor locations for plotting") - - if self.topo_ is None: - from scot.eegtopo.topoplot import Topoplot - self.topo_ = Topoplot() - self.topo_.set_locations(self.locations_) - - if mixing and not self.mixmaps_: - premix = self.premixing_ if self.premixing_ is not None else np.eye(self.mixing_.shape[1]) - self.mixmaps_ = self.plotting.prepare_topoplots(self.topo_, np.dot(self.mixing_, premix)) - #self.mixmaps_ = self.plotting.prepare_topoplots(self.topo_, self.mixing_) - - if unmixing and not self.unmixmaps_: - preinv = np.linalg.pinv(self.premixing_) if self.premixing_ is not None else np.eye(self.unmixing_.shape[0]) - self.unmixmaps_ = self.plotting.prepare_topoplots(self.topo_, np.dot(preinv, self.unmixing_).T) - #self.unmixmaps_ = self.plotting.prepare_topoplots(self.topo_, self.unmixing_.transpose()) - - @staticmethod - def _clean_measure(measure, a): - if measure in ['a', 'H', 'COH', 'pCOH']: - return np.abs(a) - elif measure in ['S', 'g']: - return np.log(np.abs(a)) - else: - return np.real(a) diff --git a/mne_sandbox/externals/scot/parallel.py b/mne_sandbox/externals/scot/parallel.py deleted file mode 100644 index a7a1246..0000000 --- a/mne_sandbox/externals/scot/parallel.py +++ /dev/null @@ -1,33 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2014 SCoT Development Team - - -def parallel_loop(func, n_jobs=1, verbose=1): - """run loops in parallel, if joblib is available. - - Parameters - ---------- - func : function - function to be executed in parallel - n_jobs : int - number of jobs - verbose : int - verbosity level - """ - try: - from joblib import Parallel, delayed - except ImportError: - n_jobs = None - - if n_jobs is None: - if verbose >= 10: - print('running ', func, ' serially') - par = lambda x: list(x) - else: - if verbose >= 10: - print('running ', func, ' in parallel') - func = delayed(func) - par = Parallel(n_jobs=n_jobs, verbose=verbose) - - return par, func diff --git a/mne_sandbox/externals/scot/pca.py b/mne_sandbox/externals/scot/pca.py deleted file mode 100644 index 2aae85c..0000000 --- a/mne_sandbox/externals/scot/pca.py +++ /dev/null @@ -1,130 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -"""principal component analysis (PCA) implementation""" - -import numpy as np -from .datatools import cat_trials - - -def pca_svd(data): - """calculate PCA using SVD - - Parameters - ---------- - data : array, shape = [n_samples, n_channels] - Two dimensional data array. - - Returns - ------- - w : array - Eigenvectors - s : array - Eigenvalues - """ - - (w, s, v) = np.linalg.svd(data.transpose()) - - return w, s ** 2 - - -def pca_eig(x): - """calculate PCA using Eigenvalue decomposition - - Parameters - ---------- - data : array, shape = [n_samples, n_channels] - Two dimensional data array. - - Returns - ------- - w : array - Eigenvectors - s : array - Eigenvalues - """ - - [s, w] = np.linalg.eigh(x.transpose().dot(x)) - - return w, s - - -def pca(x, subtract_mean=False, normalize=False, sort_components=True, reducedim=None, algorithm=pca_eig): - """ Calculate principal component analysis (PCA) - - Parameters - ---------- - x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - EEG data set - subtract_mean : bool, optional - Subtract sample mean from x. - normalize : bool, optional - Normalize variances to 1 before applying PCA. - sort_components : bool, optional - Sort principal components in order of decreasing eigenvalues. - reducedim : {float, int}, optional - A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All - components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. - An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. - algorithm : func, optional - Specify function to use for eigenvalue decomposition (:func:`pca_eig` or :func:`pca_svd`) - - Returns - ------- - w : array, shape = [n_channels, n_components] - PCA transformation matrix - v : array, shape = [n_components, n_channels] - PCA backtransformation matrix - """ - - x = cat_trials(np.atleast_3d(x)) - - if reducedim: - sort_components = True - - if subtract_mean: - for i in range(np.shape(x)[1]): - x[:, i] -= np.mean(x[:, i]) - - k, l = None, None - if normalize: - l = np.std(x, 0, ddof=1) - k = np.diag(1.0 / l) - l = np.diag(l) - x = x.dot(k) - - w, latent = algorithm(x) - - #v = np.linalg.inv(w) - # PCA is just a rotation, so inverse is equal transpose... - v = w.T - - if normalize: - w = k.dot(w) - v = v.dot(l) - - latent /= sum(latent) - - if sort_components: - order = np.argsort(latent)[::-1] - w = w[:, order] - v = v[order, :] - latent = latent[order] - - if reducedim and reducedim < 1: - selected = np.nonzero(np.cumsum(latent) < reducedim)[0] - try: - selected = np.concatenate([selected, [selected[-1] + 1]]) - except IndexError: - selected = [0] - if selected[-1] >= w.shape[1]: - selected = selected[0:-1] - w = w[:, selected] - v = v[selected, :] - - if reducedim and reducedim >= 1: - w = w[:, np.arange(reducedim)] - v = v[np.arange(reducedim), :] - - return w, v diff --git a/mne_sandbox/externals/scot/plainica.py b/mne_sandbox/externals/scot/plainica.py deleted file mode 100644 index 297329e..0000000 --- a/mne_sandbox/externals/scot/plainica.py +++ /dev/null @@ -1,77 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Source decomposition with ICA. -""" - -import numpy as np - -from . import config -from .datatools import cat_trials - - -class ResultICA: - """ Result of :func:`plainica` - - Attributes - ---------- - `mixing` : array - estimate of the mixing matrix - `unmixing` : array - estimate of the unmixing matrix - """ - def __init__(self, mx, ux): - self.mixing = mx - self.unmixing = ux - - -def plainica(x, reducedim=0.99, backend=None): - """ Source decomposition with ICA. - - Apply ICA to the data x, with optional PCA dimensionality reduction. - - Parameters - ---------- - x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - data set - reducedim : {int, float, 'no_pca'}, optional - A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All - components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. - An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. - If set to 'no_pca' the PCA step is skipped. - backend : dict-like, optional - Specify backend to use. When set to None the backend configured in config.backend is used. - - Returns - ------- - result : ResultICA - Source decomposition - """ - - x = np.atleast_3d(x) - l, m, t = np.shape(x) - - if backend is None: - backend = config.backend - - # pre-transform the data with PCA - if reducedim == 'no pca': - c = np.eye(m) - d = np.eye(m) - xpca = x - else: - c, d, xpca = backend['pca'](x, reducedim) - - # run on residuals ICA to estimate volume conduction - mx, ux = backend['ica'](cat_trials(xpca)) - - # correct (un)mixing matrix estimatees - mx = mx.dot(d) - ux = c.dot(ux) - - class Result: - unmixing = ux - mixing = mx - - return Result diff --git a/mne_sandbox/externals/scot/plotting.py b/mne_sandbox/externals/scot/plotting.py deleted file mode 100644 index 88d272b..0000000 --- a/mne_sandbox/externals/scot/plotting.py +++ /dev/null @@ -1,677 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Graphical output with matplotlib - -This module attempts to import matplotlib for plotting functionality. -If matplotlib is not available no error is raised, but plotting functions will not be available. - -""" - -import numpy as np - - -def show_plots(): - import matplotlib.pyplot as plt - plt.show() - - -def new_figure(*args, **kwargs): - import matplotlib.pyplot as plt - return plt.figure(*args, **kwargs) - - -def current_axis(): - import matplotlib.pyplot as plt - return plt.gca() - - -def MaxNLocator(*args, **kwargs): - from matplotlib.ticker import MaxNLocator as mnl - return mnl(*args, **kwargs) - - -def prepare_topoplots(topo, values): - """ Prepare multiple topo maps for cached plotting. - - .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_values`. - - Parameters - ---------- - topo : :class:`~eegtopo.topoplot.Topoplot` - Scalp maps are created with this class - values : array, shape = [n_topos, n_channels] - Channel values for each topo plot - - Returns - ------- - topomaps : list of array - The map for each topo plot - """ - values = np.atleast_2d(values) - - topomaps = [] - - for i in range(values.shape[0]): - topo.set_values(values[i, :]) - topo.create_map() - topomaps.append(topo.get_map()) - - return topomaps - - -def plot_topo(axis, topo, topomap, crange=None, offset=(0,0)): - """ Draw a topoplot in given axis. - - .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_map`. - - Parameters - ---------- - axis : axis - Axis to draw into. - topo : :class:`~eegtopo.topoplot.Topoplot` - This object draws the topo plot - topomap : array, shape = [w_pixels, h_pixels] - Scalp-projected data - crange : [int, int], optional - Range of values covered by the colormap. - If set to None, [-max(abs(topomap)), max(abs(topomap))] is substituted. - offset : [float, float], optional - Shift the topo plot by [x,y] in axis units. - - Returns - ------- - h : image - Image object the map was plotted into - """ - topo.set_map(topomap) - h = topo.plot_map(axis, crange=crange, offset=offset) - topo.plot_locations(axis, offset=offset) - topo.plot_head(axis, offset=offset) - return h - - -def plot_sources(topo, mixmaps, unmixmaps, global_scale=None, fig=None): - """ Plot all scalp projections of mixing- and unmixing-maps. - - .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_map`. - - Parameters - ---------- - topo : :class:`~eegtopo.topoplot.Topoplot` - This object draws the topo plot - mixmaps : array, shape = [w_pixels, h_pixels] - Scalp-projected mixing matrix - unmixmaps : array, shape = [w_pixels, h_pixels] - Scalp-projected unmixing matrix - global_scale : float, optional - Set common color scale as given percentile of all map values to use as the maximum. - `None` scales each plot individually (default). - fig : Figure object, optional - Figure to plot into. If set to `None`, a new figure is created. - - Returns - ------- - fig : Figure object - The figure into which was plotted. - """ - urange, mrange = None, None - - m = len(mixmaps) - - if global_scale: - tmp = np.asarray(unmixmaps) - tmp = tmp[np.logical_not(np.isnan(tmp))] - umax = np.percentile(np.abs(tmp), global_scale) - umin = -umax - urange = [umin, umax] - - tmp = np.asarray(mixmaps) - tmp = tmp[np.logical_not(np.isnan(tmp))] - mmax = np.percentile(np.abs(tmp), global_scale) - mmin = -mmax - mrange = [mmin, mmax] - - y = np.floor(np.sqrt(m * 3 / 4)) - x = np.ceil(m / y) - - if fig is None: - fig = new_figure() - - axes = [] - for i in range(m): - axes.append(fig.add_subplot(2 * y, x, i + 1)) - plot_topo(axes[-1], topo, unmixmaps[i], crange=urange) - axes[-1].set_title(str(i)) - - axes.append(fig.add_subplot(2 * y, x, m + i + 1)) - plot_topo(axes[-1], topo, mixmaps[i], crange=mrange) - axes[-1].set_title(str(i)) - - for a in axes: - a.set_yticks([]) - a.set_xticks([]) - a.set_frame_on(False) - - axes[0].set_ylabel('Unmixing weights') - axes[1].set_ylabel('Scalp projections') - - return fig - - -def plot_connectivity_topos(layout='diagonal', topo=None, topomaps=None, fig=None): - """ Place topo plots in a figure suitable for connectivity visualization. - - .. note:: Parameter `topo` is modified by the function by calling :func:`~eegtopo.topoplot.Topoplot.set_map`. - - Parameters - ---------- - layout : str - 'diagonal' -> place topo plots on diagonal. - otherwise -> place topo plots in left column and top row. - topo : :class:`~eegtopo.topoplot.Topoplot` - This object draws the topo plot - topomaps : array, shape = [w_pixels, h_pixels] - Scalp-projected map - fig : Figure object, optional - Figure to plot into. If set to `None`, a new figure is created. - - Returns - ------- - fig : Figure object - The figure into which was plotted. - """ - - m = len(topomaps) - - if fig is None: - fig = new_figure() - - if layout == 'diagonal': - for i in range(m): - ax = fig.add_subplot(m, m, i*(1+m) + 1) - plot_topo(ax, topo, topomaps[i]) - ax.set_yticks([]) - ax.set_xticks([]) - ax.set_frame_on(False) - else: - for i in range(m): - for j in [i+2, (i+1)*(m+1)+1]: - ax = fig.add_subplot(m+1, m+1, j) - plot_topo(ax, topo, topomaps[i]) - ax.set_yticks([]) - ax.set_xticks([]) - ax.set_frame_on(False) - - return fig - - -def plot_connectivity_spectrum(a, fs=2, freq_range=(-np.inf, np.inf), diagonal=0, border=False, fig=None): - """ Draw connectivity plots. - - Parameters - ---------- - a : array, shape = [n_channels, n_channels, n_fft] or [1 or 3, n_channels, n_channels, n_fft] - If a.ndim == 3, normal plots are created, - If a.ndim == 4 and a.shape[0] == 1, the area between the curve and y=0 is filled transparently, - If a.ndim == 4 and a.shape[0] == 3, a[0,:,:,:] is plotted normally and the area between a[1,:,:,:] and - a[2,:,:,:] is filled transparently. - fs : float - Sampling frequency - freq_range : (float, float) - Frequency range to plot - diagonal : {-1, 0, 1} - If diagonal == -1 nothing is plotted on the diagonal (a[i,i,:] are not plotted), - if diagonal == 0, a is plotted on the diagonal too (all a[i,i,:] are plotted), - if diagonal == 1, a is plotted on the diagonal only (only a[i,i,:] are plotted) - border : bool - If border == true the leftmost column and the topmost row are left blank - fig : Figure object, optional - Figure to plot into. If set to `None`, a new figure is created. - - Returns - ------- - fig : Figure object - The figure into which was plotted. - """ - - a = np.atleast_3d(a) - if a.ndim == 3: - [_, m, f] = a.shape - l = 0 - else: - [l, _, m, f] = a.shape - freq = np.linspace(0, fs / 2, f) - - lowest, highest = np.inf, -np.inf - left = max(freq_range[0], freq[0]) - right = min(freq_range[1], freq[-1]) - - if fig is None: - fig = new_figure() - - axes = [] - for i in range(m): - if diagonal == 1: - jrange = [i] - elif diagonal == 0: - jrange = range(m) - else: - jrange = [j for j in range(m) if j != i] - for j in jrange: - if border: - ax = fig.add_subplot(m+1, m+1, j + (i+1) * (m+1) + 2) - else: - ax = fig.add_subplot(m, m, j + i * m + 1) - axes.append((i, j, ax)) - if l == 0: - ax.plot(freq, a[i, j, :]) - lowest = min(lowest, np.min(a[i, j, :])) - highest = max(highest, np.max(a[i, j, :])) - elif l == 1: - ax.fill_between(freq, 0, a[0, i, j, :], facecolor=[0.25, 0.25, 0.25], alpha=0.25) - lowest = min(lowest, np.min(a[0, i, j, :])) - highest = max(highest, np.max(a[0, i, j, :])) - else: - baseline, = ax.plot(freq, a[0, i, j, :]) - ax.fill_between(freq, a[1, i, j, :], a[2, i, j, :], facecolor=baseline.get_color(), alpha=0.25) - lowest = min(lowest, np.min(a[:, i, j, :])) - highest = max(highest, np.max(a[:, i, j, :])) - - for i, j, ax in axes: - ax.xaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) - ax.yaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) - al = ax.get_ylim() - ax.set_ylim(min(al[0], lowest), max(al[1], highest)) - ax.set_xlim(left, right) - - if 0 < i < m - 1: - ax.set_xticks([]) - if 0 < j < m - 1: - ax.set_yticks([]) - - if i == 0: - ax.xaxis.tick_top() - if i == m-1: - ax.xaxis.tick_bottom() - - if j == 0: - ax.yaxis.tick_left() - if j == m-1: - ax.yaxis.tick_right() - - _plot_labels(fig, - {'x': 0.5, 'y': 0.025, 's': 'frequency (Hz)', 'horizontalalignment': 'center'}, - {'x': 0.05, 'y': 0.5, 's': 'magnitude', 'horizontalalignment': 'center', 'rotation': 'vertical'}) - - return fig - - -def plot_connectivity_significance(s, fs=2, freq_range=(-np.inf, np.inf), diagonal=0, border=False, fig=None): - """ Plot significance. - - Significance is drawn as a background image where dark vertical stripes indicate freuquencies where a evaluates to - True. - - Parameters - ---------- - a : array, dtype=bool, shape = [n_channels, n_channels, n_fft] - Significance - fs : float - Sampling frequency - freq_range : (float, float) - Frequency range to plot - diagonal : {-1, 0, 1} - If diagonal == -1 nothing is plotted on the diagonal (a[i,i,:] are not plotted), - if diagonal == 0, a is plotted on the diagonal too (all a[i,i,:] are plotted), - if diagonal == 1, a is plotted on the diagonal only (only a[i,i,:] are plotted) - border : bool - If border == true the leftmost column and the topmost row are left blank - fig : Figure object, optional - Figure to plot into. If set to `None`, a new figure is created. - - Returns - ------- - fig : Figure object - The figure into which was plotted. - """ - - a = np.atleast_3d(s) - [_, m, f] = a.shape - freq = np.linspace(0, fs / 2, f) - - left = max(freq_range[0], freq[0]) - right = min(freq_range[1], freq[-1]) - - imext = (freq[0], freq[-1], -1e25, 1e25) - - if fig is None: - fig = new_figure() - - axes = [] - for i in range(m): - if diagonal == 1: - jrange = [i] - elif diagonal == 0: - jrange = range(m) - else: - jrange = [j for j in range(m) if j != i] - for j in jrange: - if border: - ax = fig.add_subplot(m+1, m+1, j + (i+1) * (m+1) + 2) - else: - ax = fig.add_subplot(m, m, j + i * m + 1) - axes.append((i, j, ax)) - ax.imshow(s[i, j, np.newaxis], vmin=0, vmax=2, cmap='binary', aspect='auto', extent=imext, zorder=-999) - - ax.xaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) - ax.yaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) - ax.set_xlim(left, right) - - if 0 < i < m - 1: - ax.set_xticks([]) - if 0 < j < m - 1: - ax.set_yticks([]) - - if j == 0: - ax.yaxis.tick_left() - if j == m-1: - ax.yaxis.tick_right() - - _plot_labels(fig, - {'x': 0.5, 'y': 0.025, 's': 'frequency (Hz)', 'horizontalalignment': 'center'}, - {'x': 0.05, 'y': 0.5, 's': 'magnitude', 'horizontalalignment': 'center', 'rotation': 'vertical'}) - - return fig - - -def plot_connectivity_timespectrum(a, fs=2, crange=None, freq_range=(-np.inf, np.inf), time_range=None, diagonal=0, border=False, fig=None): - """ Draw time/frequency connectivity plots. - - Parameters - ---------- - a : array, shape = [n_channels, n_channels, n_fft, n_timesteps] - Values to draw - fs : float - Sampling frequency - crange : [int, int], optional - Range of values covered by the colormap. - If set to None, [min(a), max(a)] is substituted. - freq_range : (float, float) - Frequency range to plot - time_range : (float, float) - Time range covered by `a` - diagonal : {-1, 0, 1} - If diagonal == -1 nothing is plotted on the diagonal (a[i,i,:] are not plotted), - if diagonal == 0, a is plotted on the diagonal too (all a[i,i,:] are plotted), - if diagonal == 1, a is plotted on the diagonal only (only a[i,i,:] are plotted) - border : bool - If border == true the leftmost column and the topmost row are left blank - fig : Figure object, optional - Figure to plot into. If set to `None`, a new figure is created. - - Returns - ------- - fig : Figure object - The figure into which was plotted. - """ - a = np.asarray(a) - [_, m, _, t] = a.shape - - if crange is None: - crange = [np.min(a), np.max(a)] - - if time_range is None: - t0 = 0 - t1 = t - else: - t0, t1 = time_range - - f0, f1 = fs / 2, 0 - extent = [t0, t1, f0, f1] - - ymin = max(freq_range[0], f1) - ymax = min(freq_range[1], f0) - - if fig is None: - fig = new_figure() - - axes = [] - for i in range(m): - if diagonal == 1: - jrange = [i] - elif diagonal == 0: - jrange = range(m) - else: - jrange = [j for j in range(m) if j != i] - for j in jrange: - if border: - ax = fig.add_subplot(m+1, m+1, j + (i+1) * (m+1) + 2) - else: - ax = fig.add_subplot(m, m, j + i * m + 1) - axes.append(ax) - ax.imshow(a[i, j, :, :], vmin=crange[0], vmax=crange[1], aspect='auto', extent=extent) - ax.invert_yaxis() - - ax.xaxis.set_major_locator(MaxNLocator(max(1, 9 - m))) - ax.yaxis.set_major_locator(MaxNLocator(max(1, 7 - m))) - ax.set_ylim(ymin, ymax) - - if 0 < i < m - 1: - ax.set_xticks([]) - if 0 < j < m - 1: - ax.set_yticks([]) - - if i == 0: - ax.xaxis.tick_top() - if i == m-1: - ax.xaxis.tick_bottom() - - if j == 0: - ax.yaxis.tick_left() - if j == m-1: - ax.yaxis.tick_right() - - _plot_labels(fig, - {'x': 0.5, 'y': 0.025, 's': 'time (s)', 'horizontalalignment': 'center'}, - {'x': 0.05, 'y': 0.5, 's': 'frequency (Hz)', 'horizontalalignment': 'center', 'rotation': 'vertical'}) - - return fig - - -def plot_circular(widths, colors, curviness=0.2, mask=True, topo=None, topomaps=None, axes=None, order=None): - """ Circluar connectivity plot - - Topos are arranged in a circle, with arrows indicating connectivity - - Parameters - ---------- - widths : {float or array, shape = [n_channels, n_channels]} - Width of each arrow. Can be a scalar to assign the same width to all arrows. - colors : array, shape = [n_channels, n_channels, 3] or [3] - RGB color values for each arrow or one RGB color value for all arrows. - curviness : float, optional - Factor that determines how much arrows tend to deviate from a straight line. - mask : array, dtype = bool, shape = [n_channels, n_channels] - Enable or disable individual arrows - topo : :class:`~eegtopo.topoplot.Topoplot` - This object draws the topo plot - topomaps : array, shape = [w_pixels, h_pixels] - Scalp-projected map - axes : axis, optional - Axis to draw into. A new figure is created by default. - order : list of int - Rearrange channels. - - Returns - ------- - axes : Axes object - The axes into which was plotted. - """ - colors = np.asarray(colors) - widths = np.asarray(widths) - mask = np.asarray(mask) - - colors = np.maximum(colors, 0) - colors = np.minimum(colors, 1) - - if len(widths.shape) > 2: - [n, m] = widths.shape - elif len(colors.shape) > 3: - [n, m, c] = widths.shape - elif len(mask.shape) > 2: - [n, m] = mask.shape - else: - n = len(topomaps) - m = n - - if not order: - order = list(range(n)) - - #a = np.asarray(a) - #[n, m] = a.shape - - assert(n == m) - - if axes is None: - fig = new_figure() - axes = fig.add_subplot(111) - axes.set_yticks([]) - axes.set_xticks([]) - axes.set_frame_on(False) - - if len(colors.shape) < 3: - colors = np.tile(colors, (n,n,1)) - - if len(widths.shape) < 2: - widths = np.tile(widths, (n,n)) - - if len(mask.shape) < 2: - mask = np.tile(mask, (n,n)) - np.fill_diagonal(mask, False) - - if topo: - alpha = 1.5 if n < 10 else 1.25 - r = alpha * topo.head_radius / (np.sin(np.pi/n)) - else: - r = 1 - - for i in range(n): - if topo: - o = (r*np.sin(i*2*np.pi/n), r*np.cos(i*2*np.pi/n)) - plot_topo(axes, topo, topomaps[order[i]], offset=o) - - for i in range(n): - for j in range(n): - if not mask[order[i], order[j]]: - continue - a0 = j*2*np.pi/n - a1 = i*2*np.pi/n - - x0, y0 = r*np.sin(a0), r*np.cos(a0) - x1, y1 = r*np.sin(a1), r*np.cos(a1) - - ex = (x0 + x1) / 2 - ey = (y0 + y1) / 2 - en = np.sqrt(ex**2 + ey**2) - - if en < 1e-10: - en = 0 - ex = y0 / r - ey = -x0 / r - w = -r - else: - ex /= en - ey /= en - w = np.sqrt((x1-x0)**2 + (y1-y0)**2) / 2 - - if x0*y1-y0*x1 < 0: - w = -w - - d = en*(1-curviness) - h = en-d - - t = np.linspace(-1, 1, 100) - - dist = (t**2+2*t+1)*w**2 + (t**4-2*t**2+1)*h**2 - - tmask1 = dist >= (1.4*topo.head_radius)**2 - tmask2 = dist >= (1.2*topo.head_radius)**2 - tmask = np.logical_and(tmask1, tmask2[::-1]) - t = t[tmask] - - x = (h*t*t+d)*ex - w*t*ey - y = (h*t*t+d)*ey + w*t*ex - - # Arrow Head - s = np.sqrt((x[-2] - x[-1])**2 + (y[-2] - y[-1])**2) - - width = widths[order[i], order[j]] - - x1 = 0.1*width*(x[-2] - x[-1] + y[-2] - y[-1])/s + x[-1] - y1 = 0.1*width*(y[-2] - y[-1] - x[-2] + x[-1])/s + y[-1] - - x2 = 0.1*width*(x[-2] - x[-1] - y[-2] + y[-1])/s + x[-1] - y2 = 0.1*width*(y[-2] - y[-1] + x[-2] - x[-1])/s + y[-1] - - x = np.concatenate([x, [x1, x[-1], x2]]) - y = np.concatenate([y, [y1, y[-1], y2]]) - axes.plot(x, y, lw=width, color=colors[order[i], order[j]], solid_capstyle='round', solid_joinstyle='round') - - return axes - - -def plot_whiteness(var, h, repeats=1000, axis=None): - """ Draw distribution of the Portmanteu whiteness test. - - Parameters - ---------- - var : :class:`~scot.var.VARBase`-like object - Vector autoregressive model (VAR) object whose residuals are tested for whiteness. - h : int - Maximum lag to include in the test. - repeats : int, optional - Number of surrogate estimates to draw under the null hypothesis. - axis : axis, optional - Axis to draw into. By default draws into :func:`matplotlib.pyplot.gca()`. - - Returns - ------- - pr : float - *p*-value of whiteness under the null hypothesis - """ - pr, q0, q = var.test_whiteness(h, repeats, True) - - if axis is None: - axis = current_axis() - - pdf, _, _ = axis.hist(q0, 30, normed=True, label='surrogate distribution') - axis.plot([q,q], [0,np.max(pdf)], 'r-', label='fitted model') - - #df = m*m*(h-p) - #x = np.linspace(np.min(q0)*0.0, np.max(q0)*2.0, 100) - #y = sp.stats.chi2.pdf(x, df) - #hc = axis.plot(x, y, label='chi-squared distribution (df=%i)' % df) - - axis.set_title('significance: p = %f'%pr) - axis.set_xlabel('Li-McLeod statistic (Q)') - axis.set_ylabel('probability') - - axis.legend() - - return pr - - -def _plot_labels(target, *labels): - for l in labels: - have_label = False - for child in target.get_children(): - try: - if child.get_text() == l['s'] and child.get_position() == (l['x'], l['y']): - have_label = True - break - except AttributeError: - pass - if not have_label: - target.text(**l) diff --git a/mne_sandbox/externals/scot/utils.py b/mne_sandbox/externals/scot/utils.py deleted file mode 100644 index 8e924b0..0000000 --- a/mne_sandbox/externals/scot/utils.py +++ /dev/null @@ -1,203 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Utility functions """ - -from __future__ import division - -import numpy as np - -from functools import partial - - -def cuthill_mckee(matrix): - """ Cuthill-McKee algorithm - - Permute a symmetric binary matrix into a band matrix form with a small bandwidth. - - Parameters - ---------- - matrix : ndarray, dtype=bool, shape = [n, n] - The matrix is internally converted to a symmetric matrix by setting each element [i,j] to True if either - [i,j] or [j,i] evaluates to true. - - Returns - ------- - order : list of int - Permutation intices - - Examples - -------- - >>> A = np.array([[0,0,1,1], [0,0,0,0], [1,0,1,0], [1,0,0,0]]) - >>> p = cuthill_mckee(A) - >>> A - array([[0, 0, 1, 1], - [0, 0, 0, 0], - [1, 0, 1, 0], - [1, 0, 0, 0]]) - >>> A[p,:][:,p] - array([[0, 0, 0, 0], - [0, 0, 1, 0], - [0, 1, 0, 1], - [0, 0, 1, 1]]) - """ - matrix = np.atleast_2d(matrix) - n, m = matrix.shape - assert(n == m) - - # make sure the matrix is really symmetric. This is equivalent to - # converting a directed adjacency matrix into a undirected adjacency matrix. - matrix = np.logical_or(matrix, matrix.T) - - degree = np.sum(matrix, 0) - order = [np.argmin(degree)] - - for i in range(n): - adj = np.nonzero(matrix[order[i]])[0] - adj = [a for a in adj if a not in order] - if not adj: - idx = [i for i in range(n) if i not in order] - order.append(idx[np.argmin(degree[idx])]) - else: - if len(adj) == 1: - order.append(adj[0]) - else: - adj = np.asarray(adj) - i = adj[np.argsort(degree[adj])] - order.extend(i.tolist()) - if len(order) == n: - break - - return order - - -def acm(x, l): - """ Autocovariance matrix at lag l - - This function calculates the autocovariance matrix of `x` at lag `l`. - - Parameters - ---------- - x : ndarray, shape = [n_samples, n_channels, (n_trials)] - Signal data (2D or 3D for multiple trials) - l : int - Lag - - Returns - ------- - c : ndarray, shape = [nchannels, n_channels] - Autocovariance matrix of `x` at lag `l`. - """ - x = np.atleast_3d(x) - - if l > x.shape[0]-1: - raise AttributeError("lag exceeds data length") - - ## subtract mean from each trial - #for t in range(x.shape[2]): - # x[:, :, t] -= np.mean(x[:, :, t], axis=0) - - if l == 0: - a, b = x, x - else: - a = x[l:, :, :] - b = x[0:-l, :, :] - - c = np.zeros((x.shape[1], x.shape[1])) - for t in range(x.shape[2]): - c += a[:, :, t].T.dot(b[:, :, t]) / x.shape[0] - c /= x.shape[2] - - return c - - -#noinspection PyPep8Naming -class memoize(object): - """cache the return value of a method - - This class is meant to be used as a decorator of methods. The return value - from a given method invocation will be cached on the instance whose method - was invoked. All arguments passed to a method decorated with memoize must - be hashable. - - If a memoized method is invoked directly on its class the result will not - be cached. Instead the method will be invoked like a static method: - """ - - def __init__(self, func): - self.func = func - - #noinspection PyUnusedLocal - def __get__(self, obj, objtype=None): - if obj is None: - return self.func - return partial(self, obj) - - def __call__(self, *args, **kw): - obj = args[0] - try: - cache = obj.__cache - except AttributeError: - cache = obj.__cache = {} - key = (self.func, args[1:], frozenset(kw.items())) - try: - res = cache[key] - except KeyError: - res = cache[key] = self.func(*args, **kw) - return res - - -def cartesian(arrays, out=None): - """ - Generate a cartesian product of input arrays. - - Parameters - ---------- - arrays : list of array-like - 1-D arrays to form the cartesian product of. - out : ndarray - Array to place the cartesian product in. - - Returns - ------- - out : ndarray - 2-D array of shape (M, len(arrays)) containing cartesian products - formed of input arrays. - - Examples - -------- - >>> cartesian(([1, 2, 3], [4, 5], [6, 7])) - array([[1, 4, 6], - [1, 4, 7], - [1, 5, 6], - [1, 5, 7], - [2, 4, 6], - [2, 4, 7], - [2, 5, 6], - [2, 5, 7], - [3, 4, 6], - [3, 4, 7], - [3, 5, 6], - [3, 5, 7]]) - - References - ---------- - http://stackoverflow.com/a/1235363/3005167 - - """ - - arrays = [np.asarray(x) for x in arrays] - dtype = arrays[0].dtype - - n = np.prod([x.size for x in arrays]) - if out is None: - out = np.zeros([n, len(arrays)], dtype=dtype) - - m = n // arrays[0].size - out[:, 0] = np.repeat(arrays[0], m) - if arrays[1:]: - cartesian(arrays[1:], out=out[0:m, 1:]) - for j in range(1, arrays[0].size): - out[j * m: (j + 1) * m, 1:] = out[0:m, 1:] - return out \ No newline at end of file diff --git a/mne_sandbox/externals/scot/var.py b/mne_sandbox/externals/scot/var.py deleted file mode 100644 index e15e914..0000000 --- a/mne_sandbox/externals/scot/var.py +++ /dev/null @@ -1,316 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Vector autoregressive (VAR) model implementation -""" - -from __future__ import print_function - -import numpy as np -import scipy as sp -from .varbase import VARBase, _construct_var_eqns -from .datatools import cat_trials -from . import xvschema as xv -from .parallel import parallel_loop - - -class VAR(VARBase): - """ Builtin implementation of VARBase. - - This class provides least squares VAR model fitting with optional ridge - regression. - - Parameters - ---------- - model_order : int - Autoregressive model order - delta : float, optional - Ridge penalty parameter - xvschema : func, optional - Function that creates training and test sets for cross-validation. The - function takes two parameters: the current cross-validation run (int) - and the numer of trials (int). It returns a tuple of two arrays: the - training set and the testing set. - """ - def __init__(self, model_order, delta=0, xvschema=xv.multitrial): - VARBase.__init__(self, model_order) - self.delta = delta - self.xvschema = xvschema - - def fit(self, data): - """ Fit VAR model to data. - - Parameters - ---------- - data : array-like - shape = [n_samples, n_channels, n_trials] or - [n_samples, n_channels] - Continuous or segmented data set. - - Returns - ------- - self : :class:`VAR` - The :class:`VAR` object to facilitate method chaining (see usage - example) - """ - data = sp.atleast_3d(data) - - if self.delta == 0 or self.delta is None: - # ordinary least squares - (x, y) = self._construct_eqns(data) - else: - # regularized least squares (ridge regression) - (x, y) = self._construct_eqns_rls(data) - - (b, res, rank, s) = sp.linalg.lstsq(x, y) - - self.coef = b.transpose() - - self.residuals = data - self.predict(data) - self.rescov = sp.cov(cat_trials(self.residuals[self.p:, :, :]), - rowvar=False) - - return self - - def optimize_order(self, data, min_p=1, max_p=None, n_jobs=1, verbose=0): - """ Determine optimal model order by cross-validating the mean-squared - generalization error. - - Parameters - ---------- - data : array-like - shape = [n_samples, n_channels, n_trials] or - [n_samples, n_channels] - Continuous or segmented data set on which to optimize the model - order. - min_p : int - minimal model order to check - max_p : int - maximum model order to check - n_jobs : int | None - number of jobs to run in parallel. See `joblib.Parallel` for - details. - verbose : int - verbosity level passed to joblib. - """ - data = np.asarray(data) - assert (data.shape[2] > 1) - msge, prange = [], [] - - par, func = parallel_loop(_get_msge_with_gradient, - n_jobs=n_jobs, verbose=verbose) - if not n_jobs: - npar = 1 - elif n_jobs < 0: - npar = 4 # is this a sane default? - else: - npar = n_jobs - - p = min_p - while True: - result = par(func(data, self.delta, self.xvschema, 1, p_) - for p_ in range(p, p + npar)) - j, k = zip(*result) - prange.extend(range(p, p + npar)) - msge.extend(j) - p += npar - if max_p is None: - if len(msge) >= 2 and msge[-1] > msge[-2]: - break - else: - if prange[-1] >= max_p: - break - self.p = prange[np.argmin(msge)] - return zip(prange, msge) - - def optimize_delta_bisection(self, data, skipstep=1): - """ Find optimal ridge penalty with bisection search. - - Parameters - ---------- - data : array-like - shape = [n_samples, n_channels, n_trials] or - [n_samples, n_channels] - Continuous or segmented data set. - skipstep : int, optional - Speed up calculation by skipping samples during cost function - calculation - - Returns - ------- - self : :class:`VAR` - The :class:`VAR` object to facilitate method chaining (see usage - example) - """ - data = sp.atleast_3d(data) - (l, m, t) = data.shape - assert (t > 1) - - maxsteps = 10 - maxdelta = 1e50 - - a = -10 - b = 10 - - trform = lambda x: sp.sqrt(sp.exp(x)) - - msge = _get_msge_with_gradient_func(data.shape, self.p) - - (ja, ka) = msge(data, trform(a), self.xvschema, skipstep, self.p) - (jb, kb) = msge(data, trform(b), self.xvschema, skipstep, self.p) - - # before starting the real bisection, assure the interval contains 0 - while sp.sign(ka) == sp.sign(kb): - print('Bisection initial interval (%f,%f) does not contain zero. ' - 'New interval: (%f,%f)' % (a, b, a * 2, b * 2)) - a *= 2 - b *= 2 - (ja, ka) = msge(data, trform(a), self.xvschema, skipstep, self.p) - (jb, kb) = msge(data, trform(b), self.xvschema, skipstep, self.p) - - if trform(b) >= maxdelta: - print('Bisection: could not find initial interval.') - print(' ********* Delta set to zero! ************ ') - return 0 - - nsteps = 0 - - while nsteps < maxsteps: - - # point where the line between a and b crosses zero - # this is not very stable! - #c = a + (b-a) * np.abs(ka) / np.abs(kb-ka) - c = (a + b) / 2 - (j, k) = msge(data, trform(c), self.xvschema, skipstep, self.p) - if sp.sign(k) == sp.sign(ka): - a, ka = c, k - else: - b, kb = c, k - - nsteps += 1 - tmp = trform([a, b, a + (b - a) * np.abs(ka) / np.abs(kb - ka)]) - print('%d Bisection Interval: %f - %f, (projected: %f)' % - (nsteps, tmp[0], tmp[1], tmp[2])) - - self.delta = trform(a + (b - a) * np.abs(ka) / np.abs(kb - ka)) - print('Final point: %f' % self.delta) - return self - - optimize = optimize_delta_bisection - - def _construct_eqns_rls(self, data): - """Construct VAR equation system with RLS constraint. - """ - return _construct_var_eqns_rls(data, self.p, self.delta) - - -def _msge_with_gradient_underdetermined(data, delta, xvschema, skipstep, p): - """ Calculate the mean squared generalization error and it's gradient for - underdetermined equation system. - """ - (l, m, t) = data.shape - d = None - j, k = 0, 0 - nt = sp.ceil(t / skipstep) - for trainset, testset in xvschema(t, skipstep): - - (a, b) = _construct_var_eqns(sp.atleast_3d(data[:, :, trainset]), p) - (c, d) = _construct_var_eqns(sp.atleast_3d(data[:, :, testset]), p) - - e = sp.linalg.inv(sp.eye(a.shape[0]) * delta ** 2 + - a.dot(a.transpose())) - - cc = c.transpose().dot(c) - - be = b.transpose().dot(e) - bee = be.dot(e) - bea = be.dot(a) - beea = bee.dot(a) - beacc = bea.dot(cc) - dc = d.transpose().dot(c) - - j += sp.sum(beacc * bea - 2 * bea * dc) + sp.sum(d ** 2) - k += sp.sum(beea * dc - beacc * beea) * 4 * delta - - return j / (nt * d.size), k / (nt * d.size) - - -def _msge_with_gradient_overdetermined(data, delta, xvschema, skipstep, p): - """ Calculate the mean squared generalization error and it's gradient for - overdetermined equation system. - """ - (l, m, t) = data.shape - d = None - l, k = 0, 0 - nt = sp.ceil(t / skipstep) - for trainset, testset in xvschema(t, skipstep): - - (a, b) = _construct_var_eqns(sp.atleast_3d(data[:, :, trainset]), p) - (c, d) = _construct_var_eqns(sp.atleast_3d(data[:, :, testset]), p) - - e = sp.linalg.inv(sp.eye(a.shape[1]) * delta ** 2 + - a.transpose().dot(a)) - - ba = b.transpose().dot(a) - dc = d.transpose().dot(c) - bae = ba.dot(e) - baee = bae.dot(e) - baecc = bae.dot(c.transpose().dot(c)) - - l += sp.sum(baecc * bae - 2 * bae * dc) + sp.sum(d ** 2) - k += sp.sum(baee * dc - baecc * baee) * 4 * delta - - return l / (nt * d.size), k / (nt * d.size) - - -def _get_msge_with_gradient_func(shape, p): - """ Select which function to use for MSGE calculation (over- or - underdetermined). - """ - (l, m, t) = shape - - n = (l - p) * t - underdetermined = n < m * p - - if underdetermined: - return _msge_with_gradient_underdetermined - else: - return _msge_with_gradient_overdetermined - - -def _get_msge_with_gradient(data, delta, xvschema, skipstep, p): - """ Calculate the mean squared generalization error and it's gradient, - automatically selecting the best function. - """ - (l, m, t) = data.shape - - n = (l - p) * t - underdetermined = n < m * p - - if underdetermined: - return _msge_with_gradient_underdetermined(data, delta, xvschema, - skipstep, p) - else: - return _msge_with_gradient_overdetermined(data, delta, xvschema, - skipstep, p) - - -def _construct_var_eqns_rls(data, p, delta): - """Construct VAR equation system with RLS constraint. - """ - (l, m, t) = sp.shape(data) - n = (l - p) * t # number of linear relations - # Construct matrix x (predictor variables) - x = sp.zeros((n + m * p, m * p)) - for i in range(m): - for k in range(1, p + 1): - x[:n, i * p + k - 1] = sp.reshape(data[p - k:-k, i, :], n) - sp.fill_diagonal(x[n:, :], delta) - - # Construct vectors yi (response variables for each channel i) - y = sp.zeros((n + m * p, m)) - for i in range(m): - y[:n, i] = sp.reshape(data[p:, i, :], n) - - return x, y \ No newline at end of file diff --git a/mne_sandbox/externals/scot/varbase.py b/mne_sandbox/externals/scot/varbase.py deleted file mode 100644 index f01e236..0000000 --- a/mne_sandbox/externals/scot/varbase.py +++ /dev/null @@ -1,472 +0,0 @@ -# coding=utf-8 - -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013-2014 SCoT Development Team - -""" vector autoregressive (VAR) model """ - -from __future__ import division - -import numbers -from functools import partial - -import numpy as np -import scipy as sp - -from . import datatools -from . import xvschema as xv -from .utils import acm -from .datatools import cat_trials - - -class Defaults: - xvschema = xv.multitrial - - -class VARBase(): - """ Represents a vector autoregressive (VAR) model. - - .. warning:: `VARBase` is an abstract class that defines the interface for - VAR model implementations. Several methods must be implemented by derived - classes. - - Parameters - ---------- - model_order : int - Autoregressive model order - - Notes - ----- - Note on the arrangement of model coefficients: - *b* is of shape [m, m*p], with sub matrices arranged as follows: - - +------+------+------+------+ - | b_00 | b_01 | ... | b_0m | - +------+------+------+------+ - | b_10 | b_11 | ... | b_1m | - +------+------+------+------+ - | ... | ... | ... | ... | - +------+------+------+------+ - | b_m0 | b_m1 | ... | b_mm | - +------+------+------+------+ - - Each sub matrix b_ij is a column vector of length p that contains the - filter coefficients from channel j (source) to channel i (sink). - """ - - def __init__(self, model_order): - self.p = model_order - self.coef = None - self.residuals = None - self.rescov = None - - def copy(self): - """ Create a copy of the VAR model.""" - other = self.__class__(self.p) - other.coef = self.coef.copy() - other.residuals = self.residuals.copy() - other.rescov = self.rescov.copy() - return other - - def fit(self, data): - """ Fit VAR model to data. - - .. warning:: This function must be implemented by derived classes. - - Parameters - ---------- - data : array-like - shape = [n_samples, n_channels, n_trials] or - [n_samples, n_channels] - Continuous or segmented data set. - - Returns - ------- - self : :class:`VAR` - The :class:`VAR` object to facilitate method chaining (see usage - example) - """ - raise NotImplementedError('method fit() is not implemented in ' + - str(self)) - - def optimize(self, data): - """ Optimize model fitting hyperparameters (such as regularization - penalty) - - .. warning:: This function must be implemented by derived classes. - - Parameters - ---------- - data : array-like, shape = [n_samples, n_channels, n_trials] or - [n_samples, n_channels] - Continuous or segmented data set. - """ - raise NotImplementedError('method optimize() is not implemented in ' + - str(self)) - return self - - def from_yw(self, acms): - """ Determine VAR model from autocorrelation matrices by solving the - Yule-Walker equations. - - Parameters - ---------- - acms : array-like, shape = [n_lags, n_channels, n_channels] - acms[l] contains the autocorrelation matrix at lag l. The highest - lag must equal the model order. - - Returns - ------- - self : :class:`VAR` - The :class:`VAR` object to facilitate method chaining (see usage - example) - """ - assert(len(acms) == self.p + 1) - - n_channels = acms[0].shape[0] - - acm = lambda l: acms[l] if l >= 0 else acms[-l].T - - r = np.concatenate(acms[1:], 0) - - rr = np.array([[acm(m-k) for k in range(self.p)] - for m in range(self.p)]) - rr = np.concatenate(np.concatenate(rr, -2), -1) - - c = sp.linalg.solve(rr, r) - - # calculate residual covariance - r = acm(0) - for k in range(self.p): - bs = k * n_channels - r -= np.dot(c[bs:bs + n_channels, :].T, acm(k + 1)) - - self.coef = np.concatenate([c[m::n_channels, :] - for m in range(n_channels)]).T - self.rescov = r - - return self - - def simulate(self, l, noisefunc=None): - """ Simulate vector autoregressive (VAR) model - - This function generates data from the VAR model. - - Parameters - ---------- - l : {int, [int, int]} - Specify number of samples to generate. Can be a tuple or list - where l[0] is the number of samples and l[1] is the number of - trials. - noisefunc : func, optional - This function is used to create the generating noise process. - If set to None Gaussian white noise with zero mean and unit - variance is used. - - Returns - ------- - data : array, shape = [n_samples, n_channels, n_trials] - """ - (m, n) = sp.shape(self.coef) - p = n // m - - try: - (l, t) = l - except TypeError: - t = 1 - - if noisefunc is None: - noisefunc = lambda: sp.random.normal(size=(1, m)) - - n = l + 10 * p - - y = sp.zeros((n, m, t)) - res = sp.zeros((n, m, t)) - - for s in range(t): - for i in range(p): - e = noisefunc() - res[i, :, s] = e - y[i, :, s] = e - for i in range(p, n): - e = noisefunc() - res[i, :, s] = e - y[i, :, s] = e - for k in range(1, p + 1): - y[i, :, s] += self.coef[:, (k - 1)::p].dot(y[i - k, :, s]) - - self.residuals = res[10 * p:, :, :] - self.rescov = sp.cov(cat_trials(self.residuals), rowvar=False) - - return y[10 * p:, :, :] - - def predict(self, data): - """ Predict samples on actual data. - - The result of this function is used for calculating the residuals. - - Parameters - ---------- - data : array-like - shape = [n_samples, n_channels, n_trials] or - [n_samples, n_channels] - Continuous or segmented data set. - - Returns - ------- - predicted : shape = `data`.shape - Data as predicted by the VAR model. - - Notes - ----- - Residuals are obtained by r = x - var.predict(x) - """ - data = sp.atleast_3d(data) - (l, m, t) = data.shape - - p = int(sp.shape(self.coef)[1] / m) - - y = sp.zeros(data.shape) - if t > l-p: # which takes less loop iterations - for k in range(1, p + 1): - bp = self.coef[:, (k - 1)::p] - for n in range(p, l): - y[n, :, :] += bp.dot(data[n - k, :, :]) - else: - for k in range(1, p + 1): - bp = self.coef[:, (k - 1)::p] - for s in range(t): - y[p:, :, s] += data[(p - k):(l - k), :, s].dot(bp.T) - - return y - - def is_stable(self): - """ Test if the VAR model is stable. - - This function tests stability of the VAR model as described in [1]_. - - Returns - ------- - out : bool - True if the model is stable. - - References - ---------- - .. [1] H. Lütkepohl, "New Introduction to Multiple Time Series - Analysis", 2005, Springer, Berlin, Germany - """ - m, mp = self.coef.shape - p = mp // m - assert(mp == m * p) - - top_block = [] - for i in range(p): - top_block.append(self.coef[:, i::p]) - top_block = np.hstack(top_block) - - im = np.eye(m) - eye_block = im - for i in range(p-2): - eye_block = sp.linalg.block_diag(im, eye_block) - eye_block = np.hstack([eye_block, np.zeros((m * (p - 1), m))]) - - tmp = np.vstack([top_block, eye_block]) - - return np.all(np.abs(np.linalg.eig(tmp)[0]) < 1) - - def test_whiteness(self, h, repeats=100, get_q=False): - """ Test if the VAR model residuals are white (uncorrelated up to a lag - of h). - - This function calculates the Li-McLeod as Portmanteau test statistic Q - to test against the null hypothesis H0: "the residuals are white" [1]_. - Surrogate data for H0 is created by sampling from random permutations - of the residuals. - - Usually the returned p-value is compared against a pre-defined type 1 - error level of alpha=0.05 or alpha=0.01. If p<=alpha, the hypothesis of - white residuals is rejected, which indicates that the VAR model does - not properly describe the data. - - Parameters - ---------- - h : int - Maximum lag that is included in the test statistic. - repeats : int, optional - Number of samples to create under the null hypothesis. - get_q : bool, optional - Return Q statistic along with *p*-value - - Returns - ------- - pr : float - Probability of observing a more extreme value of Q under the - assumption that H0 is true. - q0 : list of float, optional (`get_q`) - Individual surrogate estimates that were used for estimating the - distribution of Q under H0. - q : float, optional (`get_q`) - Value of the Q statistic of the residuals - - Notes - ----- - According to [2]_ h must satisfy h = O(n^0.5), where n is the length - (time samples) of the residuals. - - References - ---------- - .. [1] H. Lütkepohl, "New Introduction to Multiple Time Series - Analysis", 2005, Springer, Berlin, Germany - .. [2] J.R.M. Hosking, "The Multivariate Portmanteau Statistic", 1980, - J. Am. Statist. Assoc. - """ - - return test_whiteness(self.residuals, h, self.p, repeats, get_q) - - def _construct_eqns(self, data): - """ Construct VAR equation system - """ - return _construct_var_eqns(data, self.p) - - -############################################################################ - - -def _construct_var_eqns(data, p): - """ Construct VAR equation system - """ - (l, m, t) = np.shape(data) - n = (l - p) * t # number of linear relations - # Construct matrix x (predictor variables) - x = np.zeros((n, m * p)) - for i in range(m): - for k in range(1, p + 1): - x[:, i * p + k - 1] = np.reshape(data[p - k:-k, i, :], n) - - # Construct vectors yi (response variables for each channel i) - y = np.zeros((n, m)) - for i in range(m): - y[:, i] = np.reshape(data[p:, i, :], n) - - return x, y - - -def test_whiteness(data, h, p=0, repeats=100, get_q=False): - """ Test if signals are white (serially uncorrelated up to a lag of h). - - This function calculates the Li-McLeod as Portmanteau test statistic Q to - test against the null hypothesis H0: "the residuals are white" [1]_. - Surrogate data for H0 is created by sampling from random permutations of - the residuals. - - Usually the returned p-value is compared against a pre-defined type 1 error - level of alpha=0.05 or alpha=0.01. If p<=alpha, the hypothesis of white - residuals is rejected, which indicates that the VAR model does not properly - describe the data. - - Parameters - ---------- - signals : array-like - shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - Continuous or segmented data set. - h : int - Maximum lag that is included in the test statistic. - p : int, optional - Model order if the `signals` are the residuals resulting from fitting a - VAR model - repeats : int, optional - Number of samples to create under the null hypothesis. - get_q : bool, optional - Return Q statistic along with *p*-value - - Returns - ------- - pr : float - Probability of observing a more extreme value of Q under the assumption - that H0 is true. - q0 : list of float, optional (`get_q`) - Individual surrogate estimates that were used for estimating the - distribution of Q under H0. - q : float, optional (`get_q`) - Value of the Q statistic of the residuals - - Notes - ----- - According to [2]_ h must satisfy h = O(n^0.5), where n is the length (time - samples) of the residuals. - - References - ---------- - .. [1] H. Lütkepohl, "New Introduction to Multiple Time Series Analysis", - 2005, Springer, Berlin, Germany - .. [2] J.R.M. Hosking, "The Multivariate Portmanteau Statistic", 1980, J. - Am. Statist. Assoc. - """ - res = data[p:, :, :] - (n, m, t) = res.shape - nt = (n - p) * t - - q0 = _calc_q_h0(repeats, res, h, nt)[:, 2, -1] - q = _calc_q_statistic(res, h, nt)[2, -1] - - # probability of observing a result more extreme than q - # under the null-hypothesis - pr = np.sum(q0 >= q) / repeats - - if get_q: - return pr, q0, q - else: - return pr - - -def _calc_q_statistic(x, h, nt): - """ Calculate portmanteau statistics up to a lag of h. - """ - (n, m, t) = x.shape - - # covariance matrix of x - c0 = acm(x, 0) - - # LU factorization of covariance matrix - c0f = sp.linalg.lu_factor(c0, overwrite_a=False, check_finite=True) - - q = np.zeros((3, h + 1)) - for l in range(1, h + 1): - cl = acm(x, l) - - # calculate tr(cl' * c0^-1 * cl * c0^-1) - a = sp.linalg.lu_solve(c0f, cl) - b = sp.linalg.lu_solve(c0f, cl.T) - tmp = a.dot(b).trace() - - # Box-Pierce - q[0, l] = tmp - - # Ljung-Box - q[1, l] = tmp / (nt - l) - - # Li-McLeod - q[2, l] = tmp - - q *= nt - q[1, :] *= (nt+2) - - q = np.cumsum(q, axis=1) - - for l in range(1, h+1): - q[2, l] = q[0, l] + m*m*l * (l + 1) / (2 * nt) - - return q - - -def _calc_q_h0(n, x, h, nt): - """ Calculate q under the null-hypothesis of whiteness. - """ - x = x.copy() - - q = [] - for i in range(n): - np.random.shuffle(x) # shuffle along time axis - q.append(_calc_q_statistic(x, h, nt)) - return np.array(q) diff --git a/mne_sandbox/externals/scot/varica.py b/mne_sandbox/externals/scot/varica.py deleted file mode 100644 index b1633f0..0000000 --- a/mne_sandbox/externals/scot/varica.py +++ /dev/null @@ -1,258 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -import numpy as np - -from . import config -from .datatools import cat_trials, dot_special -from . import xvschema - - -def mvarica(x, var, cl=None, reducedim=0.99, optimize_var=False, backend=None, varfit='ensemble'): - """ Performs joint VAR model fitting and ICA source separation. - - This function implements the MVARICA procedure [1]_. - - Parameters - ---------- - x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - data set - var : :class:`~scot.var.VARBase`-like object - Vector autoregressive model (VAR) object that is used for model fitting. - cl : list of valid dict keys, optional - Class labels associated with each trial. - reducedim : {int, float, 'no_pca'}, optional - A number of less than 1 in interpreted as the fraction of variance that should remain in the data. All - components that describe in total less than `1-reducedim` of the variance are removed by the PCA step. - An integer numer of 1 or greater is interpreted as the number of components to keep after applying the PCA. - If set to 'no_pca' the PCA step is skipped. - optimize_var : bool, optional - Whether to call automatic optimization of the VAR fitting routine. - backend : dict-like, optional - Specify backend to use. When set to None the backend configured in config.backend is used. - varfit : string - Determines how to calculate the residuals for source decomposition. - 'ensemble' (default) fits one model to the whole data set, - 'class' fits a new model for each class, and - 'trial' fits a new model for each individual trial. - - Returns - ------- - result : class - A class with the following attributes is returned: - - +---------------+----------------------------------------------------------+ - | mixing | Source mixing matrix | - +---------------+----------------------------------------------------------+ - | unmixing | Source unmixing matrix | - +---------------+----------------------------------------------------------+ - | residuals | Residuals of the VAR model(s) in source space | - +---------------+----------------------------------------------------------+ - | var_residuals | Residuals of the VAR model(s) in EEG space (before ICA) | - +---------------+----------------------------------------------------------+ - | c | Noise covariance of the VAR model(s) in source space | - +---------------+----------------------------------------------------------+ - | b | VAR model coefficients (source space) | - +---------------+----------------------------------------------------------+ - | a | VAR model coefficients (EEG space) | - +---------------+----------------------------------------------------------+ - - Notes - ----- - MVARICA is performed with the following steps: - 1. Optional dimensionality reduction with PCA - 2. Fitting a VAR model tho the data - 3. Decomposing the VAR model residuals with ICA - 4. Correcting the VAR coefficients - - References - ---------- - .. [1] G. Gomez-Herrero et al. "Measuring directional coupling between EEG sources", NeuroImage, 2008 - """ - - x = np.atleast_3d(x) - l, m, t = np.shape(x) - - if backend is None: - backend = config.backend - - # pre-transform the data with PCA - if reducedim == 'no pca': - c = np.eye(m) - d = np.eye(m) - xpca = x - else: - c, d, xpca = backend['pca'](x, reducedim) - - if optimize_var: - var.optimize(xpca) - - if varfit == 'trial': - r = np.zeros(xpca.shape) - for i in range(t): - # fit MVAR model - a = var.fit(xpca[:, :, i]) - # residuals - r[:, :, i] = xpca[:, :, i] - var.predict(xpca[:, :, i])[:, :, 0] - elif varfit == 'class': - r = np.zeros(xpca.shape) - for i in np.unique(cl): - mask = cl == i - a = var.fit(xpca[:, :, mask]) - r[:, :, mask] = xpca[:, :, mask] - var.predict(xpca[:, :, mask]) - elif varfit == 'ensemble': - # fit MVAR model - a = var.fit(xpca) - # residuals - r = xpca - var.predict(xpca) - else: - raise ValueError('unknown VAR fitting mode: {}'.format(varfit)) - - # run on residuals ICA to estimate volume conduction - mx, ux = backend['ica'](cat_trials(r)) - - # driving process - e = dot_special(r, ux) - - # correct AR coefficients - b = a.copy() - for k in range(0, a.p): - b.coef[:, k::a.p] = mx.dot(a.coef[:, k::a.p].transpose()).dot(ux).transpose() - - # correct (un)mixing matrix estimatees - mx = mx.dot(d) - ux = c.dot(ux) - - class Result: - unmixing = ux - mixing = mx - residuals = e - var_residuals = r - c = np.cov(cat_trials(e), rowvar=False) - - Result.b = b - Result.a = a - Result.xpca = xpca - - return Result - - -def cspvarica(x, var, cl, reducedim=np.inf, optimize_var=False, backend=None, varfit='ensemble'): - """ Performs joint VAR model fitting and ICA source separation. - - This function implements the CSPVARICA procedure [1]_. - - Parameters - ---------- - x : array-like, shape = [n_samples, n_channels, n_trials] or [n_samples, n_channels] - data set - var : :class:`~scot.var.VARBase`-like object - Vector autoregressive model (VAR) object that is used for model fitting. - cl : list of valid dict keys - Class labels associated with each trial. - reducedim : {int}, optional - Number of (most discriminative) components to keep after applying the CSP. - optimize_var : bool, optional - Whether to call automatic optimization of the VAR fitting routine. - backend : dict-like, optional - Specify backend to use. When set to None the backend configured in config.backend is used. - varfit : string - Determines how to calculate the residuals for source decomposition. - 'ensemble' (default) fits one model to the whole data set, - 'class' fits a new model for each class, and - 'trial' fits a new model for each individual trial. - - Returns - ------- - Result : class - A class with the following attributes is returned: - - +---------------+----------------------------------------------------------+ - | mixing | Source mixing matrix | - +---------------+----------------------------------------------------------+ - | unmixing | Source unmixing matrix | - +---------------+----------------------------------------------------------+ - | residuals | Residuals of the VAR model(s) in source space | - +---------------+----------------------------------------------------------+ - | var_residuals | Residuals of the VAR model(s) in EEG space (before ICA) | - +---------------+----------------------------------------------------------+ - | c | Noise covariance of the VAR model(s) in source space | - +---------------+----------------------------------------------------------+ - | b | VAR model coefficients (source space) | - +---------------+----------------------------------------------------------+ - | a | VAR model coefficients (EEG space) | - +---------------+----------------------------------------------------------+ - - Notes - ----- - CSPVARICA is performed with the following steps: - 1. Dimensionality reduction with CSP - 2. Fitting a VAR model tho the data - 3. Decomposing the VAR model residuals with ICA - 4. Correcting the VAR coefficients - - References - ---------- - .. [1] M. Billinger et al. "SCoT: A Python Toolbox for EEG Source Connectivity", Frontiers in Neuroinformatics, 2014 - """ - - x = np.atleast_3d(x) - l, m, t = np.shape(x) - - if backend is None: - backend = config.backend - - # pre-transform the data with CSP - c, d, xcsp = backend['csp'](x, cl, reducedim) - - if optimize_var: - var.optimize(xcsp) - - if varfit == 'trial': - r = np.zeros(xcsp.shape) - for i in range(t): - # fit MVAR model - a = var.fit(xcsp[:, :, i]) - # residuals - r[:, :, i] = xcsp[:, :, i] - var.predict(xcsp[:, :, i])[:, :, 0] - elif varfit == 'class': - r = np.zeros(xcsp.shape) - for i in np.unique(cl): - mask = cl == i - a = var.fit(xcsp[:, :, mask]) - r[:, :, mask] = xcsp[:, :, mask] - var.predict(xcsp[:, :, mask]) - elif varfit == 'ensemble': - # fit MVAR model - a = var.fit(xcsp) - # residuals - r = xcsp - var.predict(xcsp) - else: - raise ValueError('unknown VAR fitting mode: {}'.format(varfit)) - - # run on residuals ICA to estimate volume conduction - mx, ux = backend['ica'](cat_trials(r)) - - # driving process - e = dot_special(r, ux) - - # correct AR coefficients - b = a.copy() - for k in range(0, a.p): - b.coef[:, k::a.p] = mx.dot(a.coef[:, k::a.p].transpose()).dot(ux).transpose() - - # correct (un)mixing matrix estimatees - mx = mx.dot(d) - ux = c.dot(ux) - - class Result: - unmixing = ux - mixing = mx - residuals = e - var_residuals = r - c = np.cov(cat_trials(e), rowvar=False) - Result.b = b - Result.a = a - Result.xcsp = xcsp - - return Result diff --git a/mne_sandbox/externals/scot/xvschema.py b/mne_sandbox/externals/scot/xvschema.py deleted file mode 100644 index 4e3a293..0000000 --- a/mne_sandbox/externals/scot/xvschema.py +++ /dev/null @@ -1,115 +0,0 @@ -# Released under The MIT License (MIT) -# http://opensource.org/licenses/MIT -# Copyright (c) 2013 SCoT Development Team - -""" Cross-validation schemas """ - -from __future__ import division - -import numpy as np -from numpy import sort -from functools import partial - - -def singletrial(num_trials, skipstep): - """ Single-trial cross-validation schema - - Use one trial for training, all others for testing. - - Parameters - ---------- - num_trials : int - Total number of trials - skipstep : int - only use every `skipstep` trial for training - - Returns - ------- - gen : generator object - the generator returns tuples (trainset, testset) - """ - for t in range(0, num_trials, skipstep): - trainset = [t] - testset = [i for i in range(trainset[0])] + \ - [i for i in range(trainset[-1] + 1, num_trials)] - testset = sort([t % num_trials for t in testset]) - yield trainset, testset - - -def multitrial(num_trials, skipstep): - """ Multi-trial cross-validation schema - - Use one trial for testing, all others for training. - - Parameters - ---------- - num_trials : int - Total number of trials - skipstep : int - only use every `skipstep` trial for testing - - Returns - ------- - gen : generator object - the generator returns tuples (trainset, testset) - """ - for t in range(0, num_trials, skipstep): - testset = [t] - trainset = [i for i in range(testset[0])] + \ - [i for i in range(testset[-1] + 1, num_trials)] - trainset = sort([t % num_trials for t in trainset]) - yield trainset, testset - - -def splitset(num_trials, skipstep): - """ Split-set cross validation - - Use half the trials for training, and the other half for testing. Then - repeat the other way round. - - Parameters - ---------- - num_trials : int - Total number of trials - skipstep : int - unused - - Returns - ------- - gen : generator object - the generator returns tuples (trainset, testset) - """ - split = num_trials // 2 - - a = list(range(0, split)) - b = list(range(split, num_trials)) - yield a, b - yield b, a - - -def make_nfold(n): - """ n-fold cross validation - - Use each of n blocks for testing once. - - Parameters - ---------- - n : int - number of blocks - - Returns - ------- - gengen : func - a function that returns the generator - """ - return partial(_nfold, n=n) - - -def _nfold(num_trials, skipstep, n): - blocksize = int(np.ceil(num_trials / n)) - for i in range(0, num_trials, blocksize): - testset = [k for k in (i + np.arange(blocksize)) if k < num_trials] - trainset = [i for i in range(testset[0])] + \ - [i for i in range(testset[-1] + 1, num_trials)] - trainset = sort([t % num_trials for t in trainset]) - yield trainset, testset diff --git a/setup.py b/setup.py index c1b08f2..3198edb 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,6 @@ packages=[ 'mne_sandbox', 'mne_sandbox.connectivity', - 'mne_sandbox.externals.scot', 'mne_sandbox.preprocessing', 'mne_sandbox.viz' ], From ef82549b89ee73ba792930110e9df05fc6410c1c Mon Sep 17 00:00:00 2001 From: mbillinger Date: Thu, 21 Apr 2016 15:01:17 +0200 Subject: [PATCH 04/12] Added test for surrogate connectivtiy --- mne_sandbox/connectivity/tests/test_mvar.py | 35 ++++++++++----------- mne_sandbox/viz/connectivity.py | 1 + 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py index 92072e8..30d55a7 100644 --- a/mne_sandbox/connectivity/tests/test_mvar.py +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -1,28 +1,10 @@ import numpy as np from numpy.testing import assert_array_almost_equal -from nose.tools import assert_raises, assert_equal - -from mne import SourceEstimate +from nose.tools import assert_raises, assert_equal, assert_less, assert_greater from mne_sandbox.connectivity import mvar_connectivity -def _stc_gen(data, sfreq, tmin, combo=False): - """Simulate a SourceEstimate generator""" - vertices = [np.arange(data.shape[1]), np.empty(0)] - for d in data: - if not combo: - stc = SourceEstimate(data=d, vertices=vertices, - tmin=tmin, tstep=1 / float(sfreq)) - yield stc - else: - # simulate a combination of array and source estimate - arr = d[0] - stc = SourceEstimate(data=d[1:], vertices=vertices, - tmin=tmin, tstep=1 / float(sfreq)) - yield (arr, stc) - - def _make_data(var_coef, n_samples, n_epochs): var_order = var_coef.shape[0] n_signals = var_coef.shape[1] @@ -132,3 +114,18 @@ def test_mvar_connectivity(): assert_array_almost_equal(con['PDC'][:, :, 0], h / np.sum(h, 0, keepdims=True), decimal=2) assert_array_almost_equal(con['GPDC'], con['PDC'], decimal=2) + + # generate data with some directed connectivity + var_coef = np.zeros((1, n_sigs, n_sigs)) + var_coef[:, 1, 0] = 1 + var_coef[:, 2, 1] = 1 + data = _make_data(var_coef, n_samples, n_epochs) + + con, freqs, p, p_vals = mvar_connectivity(data, 'PDC', order=(1, None), + n_surrogates=10) + for i in range(n_sigs): + for j in range(n_sigs): + if var_coef[0, i, j] > 0: + assert_less(p_vals[0][i, j, 0], 0.05) + else: + assert_greater(p_vals[0][i, j, 0], 0.05) diff --git a/mne_sandbox/viz/connectivity.py b/mne_sandbox/viz/connectivity.py index 8d257b4..1fcf494 100644 --- a/mne_sandbox/viz/connectivity.py +++ b/mne_sandbox/viz/connectivity.py @@ -18,6 +18,7 @@ from mne.viz.circle import _plot_connectivity_circle_onpick +# copied from mne.viz.circle to add optional `plot_names` argument def plot_connectivity_circle(con, node_names, indices=None, n_lines=None, node_angles=None, node_width=None, node_colors=None, facecolor='black', From a43df0759730b71be4a3cd6861d346868959455e Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Mon, 25 Apr 2016 11:28:42 +0200 Subject: [PATCH 05/12] No longer use assert_less, which is not available in Python 2.6 --- mne_sandbox/connectivity/tests/test_mvar.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py index 30d55a7..0aa33e2 100644 --- a/mne_sandbox/connectivity/tests/test_mvar.py +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -1,6 +1,6 @@ import numpy as np -from numpy.testing import assert_array_almost_equal -from nose.tools import assert_raises, assert_equal, assert_less, assert_greater +from numpy.testing import assert_array_almost_equal, assert_array_less +from nose.tools import assert_raises, assert_equal from mne_sandbox.connectivity import mvar_connectivity @@ -116,6 +116,8 @@ def test_mvar_connectivity(): assert_array_almost_equal(con['GPDC'], con['PDC'], decimal=2) # generate data with some directed connectivity + # check if statistics report only significant connectivity where the + # original coefficients were non-zero var_coef = np.zeros((1, n_sigs, n_sigs)) var_coef[:, 1, 0] = 1 var_coef[:, 2, 1] = 1 @@ -126,6 +128,6 @@ def test_mvar_connectivity(): for i in range(n_sigs): for j in range(n_sigs): if var_coef[0, i, j] > 0: - assert_less(p_vals[0][i, j, 0], 0.05) + assert_array_less(p_vals[0][i, j, 0], 0.05) else: - assert_greater(p_vals[0][i, j, 0], 0.05) + assert_array_less(0.05, p_vals[0][i, j, 0]) From 9a535926fdc1aba85c08004370720b3cfd22999d Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Mon, 25 Apr 2016 11:56:11 +0200 Subject: [PATCH 06/12] Added tests for viz.connectivity --- mne_sandbox/connectivity/tests/test_mvar.py | 4 ++ mne_sandbox/viz/connectivity.py | 1 + mne_sandbox/viz/tests/test_connecitvity.py | 56 +++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 mne_sandbox/viz/tests/test_connecitvity.py diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py index 0aa33e2..f41a7e6 100644 --- a/mne_sandbox/connectivity/tests/test_mvar.py +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -1,3 +1,7 @@ +# Authors: Martin Billinger +# +# License: BSD (3-clause) + import numpy as np from numpy.testing import assert_array_almost_equal, assert_array_less from nose.tools import assert_raises, assert_equal diff --git a/mne_sandbox/viz/connectivity.py b/mne_sandbox/viz/connectivity.py index 1fcf494..0881d28 100644 --- a/mne_sandbox/viz/connectivity.py +++ b/mne_sandbox/viz/connectivity.py @@ -421,6 +421,7 @@ def plot_connectivity_matrix(con, node_names, indices=None, raise ValueError('con has to be 1D or a square matrix') # remove diagonal (do not show node's self-connectivity) + con = con.copy() np.fill_diagonal(con, np.nan) # get the colormap diff --git a/mne_sandbox/viz/tests/test_connecitvity.py b/mne_sandbox/viz/tests/test_connecitvity.py new file mode 100644 index 0000000..c620e85 --- /dev/null +++ b/mne_sandbox/viz/tests/test_connecitvity.py @@ -0,0 +1,56 @@ +# Authors: Martin Billinger +# +# License: Simplified BSD + +import numpy as np +from numpy.testing import assert_array_equal + +from mne_sandbox.viz import (plot_connectivity_circle, + plot_connectivity_matrix, + plot_connectivity_inoutcircles) + +# Set our plotters to test mode +import matplotlib +matplotlib.use('Agg') # for testing don't use X server + + +def test_plot_connectivity_circle(): + """Test plotting connecitvity circle + """ + label_names = ['bankssts-lh', 'bankssts-rh', 'caudalanteriorcingulate-lh', + 'caudalanteriorcingulate-rh', 'caudalmiddlefrontal-lh'] + + con = np.random.RandomState(42).rand(5, 5) + + plot_connectivity_circle(con, label_names, plot_names=True) + plot_connectivity_circle(con, label_names, plot_names=False) + + +def test_plot_connectivity_matrix(): + """Test plotting connecitvity matrix + """ + label_names = ['bankssts-lh', 'bankssts-rh', 'caudalanteriorcingulate-lh', + 'caudalanteriorcingulate-rh', 'caudalmiddlefrontal-lh'] + + con = np.random.RandomState(42).rand(5, 5) + con0 = con.copy() + + plot_connectivity_matrix(con, label_names) + + # check that function does not change arguments + assert_array_equal(con, con0) + + +def test_plot_connectivity_inoutcircles(): + """Test plotting directional connecitvity circles + """ + label_names = ['bankssts-lh', 'bankssts-rh', 'caudalanteriorcingulate-lh', + 'caudalanteriorcingulate-rh', 'caudalmiddlefrontal-lh'] + + con = np.random.RandomState(42).rand(5, 5) + con0 = con.copy() + + plot_connectivity_inoutcircles(con, 'bankssts-rh', label_names) + + # check that function does not change arguments + assert_array_equal(con, con0) From 188aa7e1cdbf6c28b38a1b0adfe7c5086339eb38 Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Mon, 25 Apr 2016 13:34:20 +0200 Subject: [PATCH 07/12] Upgraded to recent SCoT --- .travis.yml | 2 +- mne_sandbox/connectivity/mvar.py | 4 ++-- mne_sandbox/connectivity/tests/test_mvar.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index b84fadd..9660f4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ install: - source ${MNE_ROOT}/bin/mne_setup_sh; - conda install --yes --quiet $ENSURE_PACKAGES pandas$PANDAS scikit-learn$SKLEARN patsy h5py pillow; - pip install -q joblib nibabel; - - pip install -q scot==0.1.0 + - pip install -q scot==0.2.1 - if [ "${PYTHON}" == "3.5" ]; then conda install --yes --quiet $ENSURE_PACKAGES ipython; else diff --git a/mne_sandbox/connectivity/mvar.py b/mne_sandbox/connectivity/mvar.py index bfb9e8e..da99c0a 100644 --- a/mne_sandbox/connectivity/mvar.py +++ b/mne_sandbox/connectivity/mvar.py @@ -48,8 +48,8 @@ def _fit_mvar_lsq(data, pmin, pmax, delta, n_jobs, verbose): if pmin != pmax: logger.info('MVAR order selection...') var.optimize_order(data, pmin, pmax, n_jobs=n_jobs, verbose=verbose) - # todo: only convert if data is a generator - data = np.asarray(list(data)).transpose([2, 1, 0]) + # todo: only convert to list if data is a generator + data = np.asarray(list(data)) var.fit(data) return var diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py index f41a7e6..7f37065 100644 --- a/mne_sandbox/connectivity/tests/test_mvar.py +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -128,7 +128,8 @@ def test_mvar_connectivity(): data = _make_data(var_coef, n_samples, n_epochs) con, freqs, p, p_vals = mvar_connectivity(data, 'PDC', order=(1, None), - n_surrogates=10) + n_surrogates=20) + for i in range(n_sigs): for j in range(n_sigs): if var_coef[0, i, j] > 0: From a95ef0888075a21ed9c0eba0cc5fe104f078b062 Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Tue, 26 Apr 2016 10:08:12 +0200 Subject: [PATCH 08/12] Fixed Yule-Walker fitting and added tests --- mne_sandbox/connectivity/mvar.py | 2 +- mne_sandbox/connectivity/tests/test_mvar.py | 29 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mne_sandbox/connectivity/mvar.py b/mne_sandbox/connectivity/mvar.py index da99c0a..4caaf40 100644 --- a/mne_sandbox/connectivity/mvar.py +++ b/mne_sandbox/connectivity/mvar.py @@ -25,7 +25,7 @@ def _acm(x, l): a = x[:, l:] b = x[:, 0:-l] - return np.dot(a[:, :], b[:, :].T) / a.shape[1] + return np.dot(a[:, :], b[:, :].T).T / a.shape[1] def _epoch_autocorrelations(epoch, max_lag): diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py index 7f37065..81470d4 100644 --- a/mne_sandbox/connectivity/tests/test_mvar.py +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -3,10 +3,13 @@ # License: BSD (3-clause) import numpy as np -from numpy.testing import assert_array_almost_equal, assert_array_less +from numpy.testing import (assert_array_equal, assert_array_almost_equal, + assert_array_less) from nose.tools import assert_raises, assert_equal +from copy import deepcopy from mne_sandbox.connectivity import mvar_connectivity +from mne_sandbox.connectivity.mvar import _fit_mvar_lsq, _fit_mvar_yw def _make_data(var_coef, n_samples, n_epochs): @@ -136,3 +139,27 @@ def test_mvar_connectivity(): assert_array_less(p_vals[0][i, j, 0], 0.05) else: assert_array_less(0.05, p_vals[0][i, j, 0]) + + +def test_fit_mvar(): + """Test MVAR model fitting""" + np.random.seed(0) + + n_sigs = 3 + n_epochs = 50 + n_samples = 200 + + var_coef = np.zeros((1, n_sigs, n_sigs)) + var_coef[0, :, :] = [[0.9, 0, 0], + [1, 0.5, 0], + [2, 0, -0.5]] + data = _make_data(var_coef, n_samples, n_epochs) + data0 = deepcopy(data) + + var = _fit_mvar_lsq(data, pmin=1, pmax=1, delta=0, n_jobs=1, verbose=0) + assert_array_equal(data, data0) + assert_array_almost_equal(var_coef[0], var.coef, decimal=2) + + var = _fit_mvar_yw(data, pmin=1, pmax=1, n_jobs=1, verbose=0) + assert_array_equal(data, data0) + assert_array_almost_equal(var_coef[0], var.coef, decimal=2) From 91c4d7603c93e8afd29affc061108121ba04c527 Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Thu, 28 Apr 2016 10:30:50 +0200 Subject: [PATCH 09/12] Added tests for improved coverage of viz --- mne_sandbox/viz/tests/test_connecitvity.py | 29 ++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/mne_sandbox/viz/tests/test_connecitvity.py b/mne_sandbox/viz/tests/test_connecitvity.py index c620e85..66c7775 100644 --- a/mne_sandbox/viz/tests/test_connecitvity.py +++ b/mne_sandbox/viz/tests/test_connecitvity.py @@ -4,10 +4,12 @@ import numpy as np from numpy.testing import assert_array_equal +from nose.tools import assert_raises, assert_equal from mne_sandbox.viz import (plot_connectivity_circle, plot_connectivity_matrix, plot_connectivity_inoutcircles) +from mne_sandbox.viz.connectivity import _plot_connectivity_matrix_nodename # Set our plotters to test mode import matplotlib @@ -35,11 +37,28 @@ def test_plot_connectivity_matrix(): con = np.random.RandomState(42).rand(5, 5) con0 = con.copy() - plot_connectivity_matrix(con, label_names) + assert_raises(ValueError, plot_connectivity_matrix, + con=np.empty((2, 2, 2)), node_names=label_names) + assert_raises(ValueError, plot_connectivity_matrix, + con=np.empty((1, 2)), node_names=label_names) + + plot_connectivity_matrix(con, label_names, colormap='jet', title='Test') # check that function does not change arguments assert_array_equal(con, con0) + # test status bar text + labels = ['a', 'b', 'c', 'd'] + con = np.empty((8, 8)) + con[:] = np.nan + con[2:-2, 2:-2] = np.arange(16).reshape(4, 4) + + str1 = _plot_connectivity_matrix_nodename(1, 1, con, labels) + str2 = _plot_connectivity_matrix_nodename(2, 3, con, labels) + + assert_equal(str1, '') + assert_equal(str2, 'a --> b: 4') + def test_plot_connectivity_inoutcircles(): """Test plotting directional connecitvity circles @@ -50,7 +69,13 @@ def test_plot_connectivity_inoutcircles(): con = np.random.RandomState(42).rand(5, 5) con0 = con.copy() - plot_connectivity_inoutcircles(con, 'bankssts-rh', label_names) + assert_raises(ValueError, plot_connectivity_inoutcircles, con, 'n/a', + label_names) + assert_raises(ValueError, plot_connectivity_inoutcircles, con, 99, + label_names) + + plot_connectivity_inoutcircles(con, 'bankssts-rh', label_names, + title='Test') # check that function does not change arguments assert_array_equal(con, con0) From 5cb322d4de2b7dcb8aba31286154d677942e81eb Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Thu, 28 Apr 2016 12:37:29 +0200 Subject: [PATCH 10/12] Optimized autocorrelation computation. --- mne_sandbox/connectivity/mvar.py | 71 +++++++++++++-------- mne_sandbox/connectivity/tests/test_mvar.py | 19 +++++- 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/mne_sandbox/connectivity/mvar.py b/mne_sandbox/connectivity/mvar.py index 4caaf40..c7e3a2b 100644 --- a/mne_sandbox/connectivity/mvar.py +++ b/mne_sandbox/connectivity/mvar.py @@ -11,25 +11,21 @@ from scot.connectivity import connectivity from scot.connectivity_statistics import surrogate_connectivity from scot.xvschema import make_nfold +from scot.utils import acm from mne.parallel import parallel_func from mne.utils import logger, verbose -def _acm(x, l): - """Calculates autocorrelation matrix of x at lag l. - """ - if l == 0: - a, b = x, x - else: - a = x[:, l:] - b = x[:, 0:-l] - - return np.dot(a[:, :], b[:, :].T).T / a.shape[1] +# todo: I'm sure we can import generator from somewhere... +def _generator(): + yield None +generator = type(_generator()) +del _generator -def _epoch_autocorrelations(epoch, max_lag): - return [_acm(epoch, l) for l in range(max_lag + 1)] +def _autocorrelations(epochs, max_lag): + return [acm(epochs, l) for l in range(max_lag + 1)] def _get_n_epochs(epochs, n): @@ -40,7 +36,20 @@ def _get_n_epochs(epochs, n): if len(epochs_out) >= n: yield epochs_out epochs_out = [] - yield epochs_out + if len(epochs_out) > 0: + yield epochs_out + + +def _get_n_epochblocks(epochs, n_blocks, blocksize): + """Generator that returns lists with at most n_blocks of epochs""" + blocks_out = [] + for block in _get_n_epochs(epochs, blocksize): + blocks_out.append(block) + if len(blocks_out) >= n_blocks: + yield blocks_out + blocks_out = [] + if len(blocks_out) > 0: + yield blocks_out def _fit_mvar_lsq(data, pmin, pmax, delta, n_jobs, verbose): @@ -54,26 +63,28 @@ def _fit_mvar_lsq(data, pmin, pmax, delta, n_jobs, verbose): return var -def _fit_mvar_yw(data, pmin, pmax, n_jobs=1, verbose=None): +def _fit_mvar_yw(data, pmin, pmax, n_jobs, blocksize, verbose=None): if pmin != pmax: raise NotImplementedError('Yule-Walker fitting does not support ' 'automatic model order selection.') order = pmin - parallel, my_epoch_autocorrelations, _ = \ - parallel_func(_epoch_autocorrelations, n_jobs, + if not isinstance(data, generator): + blocksize = int(np.ceil(len(data) / n_jobs)) + + parallel, block_autocorrelations, _ = \ + parallel_func(_autocorrelations, n_jobs, verbose=verbose) - n_epochs = 0 - logger.info('Accumulating autocovariance matrices...') - for epoch_block in _get_n_epochs(data, n_jobs): - out = parallel(my_epoch_autocorrelations(epoch, order) - for epoch in epoch_block) - if n_epochs == 0: - acm_estimates = np.sum(out, 0) + n_blocks = 0 + for blocks in _get_n_epochblocks(data, n_jobs, blocksize): + acms = parallel(block_autocorrelations(block, order) + for block in blocks) + if n_blocks == 0: + acm_estimates = np.sum(acms, 0) else: - acm_estimates += np.sum(out, 0) - n_epochs += len(epoch_block) - acm_estimates /= n_epochs + acm_estimates += np.sum(acms, 0) + n_blocks += len(blocks) + acm_estimates /= n_blocks var = VARBase(order) var.from_yw(acm_estimates) @@ -84,7 +95,7 @@ def _fit_mvar_yw(data, pmin, pmax, n_jobs=1, verbose=None): @verbose def mvar_connectivity(data, method, order=(1, None), fitting_mode='lsq', ridge=0, sfreq=2 * np.pi, fmin=0, fmax=np.inf, n_fft=64, - n_surrogates=None, buffer_size=8, n_jobs=1, + n_surrogates=None, buffer_size=8, n_jobs=1, blocksize=10, verbose=None): """Estimate connectivity from multivariate autoregressive (MVAR) models. @@ -149,6 +160,9 @@ def mvar_connectivity(data, method, order=(1, None), fitting_mode='lsq', n_jobs : int Number of jobs to run in parallel. This is used for model order selection and statistics calculations. + blocksize : int + Epochs are prozessed in batches of size blocksize. For best performance + set blocksize so that `n_epochs == n_jobs * blocksize`. verbose : bool, str, int, or None If not None, override default verbose level (see mne.verbose). @@ -218,7 +232,8 @@ def mvar_connectivity(data, method, order=(1, None), fitting_mode='lsq', logger.info('MVAR fitting...') if fitting_mode == 'yw': - var = _fit_mvar_yw(data, pmin, pmax) + var = _fit_mvar_yw(data, pmin, pmax, n_jobs=n_jobs, + blocksize=blocksize, verbose=verbose) elif fitting_mode == 'lsq': var = _fit_mvar_lsq(data, pmin, pmax, ridge, n_jobs=n_jobs, verbose=scot_verbosity) diff --git a/mne_sandbox/connectivity/tests/test_mvar.py b/mne_sandbox/connectivity/tests/test_mvar.py index 81470d4..0dd1b86 100644 --- a/mne_sandbox/connectivity/tests/test_mvar.py +++ b/mne_sandbox/connectivity/tests/test_mvar.py @@ -27,6 +27,11 @@ def _make_data(var_coef, n_samples, n_epochs): return [x[:, i + win] for i in range(0, n_epochs * n_samples, n_samples)] +def _data_generator(data): + for d in data: + yield d + + def test_mvar_connectivity(): """Test MVAR connectivity estimation""" # Use a case known to have no spurious correlations (it would bad if @@ -143,10 +148,10 @@ def test_mvar_connectivity(): def test_fit_mvar(): """Test MVAR model fitting""" - np.random.seed(0) + np.random.seed(42) n_sigs = 3 - n_epochs = 50 + n_epochs = 65 n_samples = 200 var_coef = np.zeros((1, n_sigs, n_sigs)) @@ -160,6 +165,14 @@ def test_fit_mvar(): assert_array_equal(data, data0) assert_array_almost_equal(var_coef[0], var.coef, decimal=2) - var = _fit_mvar_yw(data, pmin=1, pmax=1, n_jobs=1, verbose=0) + var = _fit_mvar_yw(data, pmin=1, pmax=1, n_jobs=1, blocksize=7, verbose=0) assert_array_equal(data, data0) assert_array_almost_equal(var_coef[0], var.coef, decimal=2) + + data = _data_generator(data0) + var = _fit_mvar_lsq(data, pmin=1, pmax=1, delta=0, n_jobs=2, verbose=0) + assert_array_almost_equal(var_coef[0], var.coef, decimal=2) + + data = _data_generator(data0) + var = _fit_mvar_yw(data, pmin=1, pmax=1, n_jobs=2, blocksize=9, verbose=0) + assert_array_almost_equal(var_coef[0], var.coef, decimal=2) From cc7582b5d34fd6777cf6736daddcdc8dd9a49c75 Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Sat, 30 Apr 2016 12:44:50 +0200 Subject: [PATCH 11/12] Fixed Python 2.6 string formatting --- mne_sandbox/viz/connectivity.py | 4 ++-- mne_sandbox/viz/tests/test_connecitvity.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne_sandbox/viz/connectivity.py b/mne_sandbox/viz/connectivity.py index 0881d28..537df41 100644 --- a/mne_sandbox/viz/connectivity.py +++ b/mne_sandbox/viz/connectivity.py @@ -321,8 +321,8 @@ def _plot_connectivity_matrix_nodename(x, y, con, node_names): y = int(round(y) - 2) if x < 0 or y < 0 or x >= len(node_names) or y >= len(node_names): return '' - return '{} --> {}: {:.3g}'.format(node_names[x], node_names[y], - con[y + 2, x + 2]) + return '%s --> %s: %.2f' % (node_names[x], node_names[y], + con[y + 2, x + 2]) def plot_connectivity_matrix(con, node_names, indices=None, diff --git a/mne_sandbox/viz/tests/test_connecitvity.py b/mne_sandbox/viz/tests/test_connecitvity.py index 66c7775..6449fc3 100644 --- a/mne_sandbox/viz/tests/test_connecitvity.py +++ b/mne_sandbox/viz/tests/test_connecitvity.py @@ -57,7 +57,7 @@ def test_plot_connectivity_matrix(): str2 = _plot_connectivity_matrix_nodename(2, 3, con, labels) assert_equal(str1, '') - assert_equal(str2, 'a --> b: 4') + assert_equal(str2, 'a --> b: 4.00') def test_plot_connectivity_inoutcircles(): From a038730aecd281cd93a9e2c2e18247dc87cb794a Mon Sep 17 00:00:00 2001 From: Martin Billinger Date: Thu, 5 May 2016 08:48:31 +0200 Subject: [PATCH 12/12] Import GeneratorType --- mne_sandbox/connectivity/mvar.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/mne_sandbox/connectivity/mvar.py b/mne_sandbox/connectivity/mvar.py index c7e3a2b..88e2af1 100644 --- a/mne_sandbox/connectivity/mvar.py +++ b/mne_sandbox/connectivity/mvar.py @@ -5,6 +5,7 @@ from __future__ import division import numpy as np import logging +from types import GeneratorType from scot.varbase import VARBase from scot.var import VAR @@ -17,13 +18,6 @@ from mne.utils import logger, verbose -# todo: I'm sure we can import generator from somewhere... -def _generator(): - yield None -generator = type(_generator()) -del _generator - - def _autocorrelations(epochs, max_lag): return [acm(epochs, l) for l in range(max_lag + 1)] @@ -69,7 +63,7 @@ def _fit_mvar_yw(data, pmin, pmax, n_jobs, blocksize, verbose=None): 'automatic model order selection.') order = pmin - if not isinstance(data, generator): + if not isinstance(data, GeneratorType): blocksize = int(np.ceil(len(data) / n_jobs)) parallel, block_autocorrelations, _ = \