Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nipy / nitime / 159

9 Dec 2018 - 14:20 coverage: 79.708% (-0.2%) from 79.871%
159

Pull #165

travis-ci

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
Eliminate ciruclar imports.
Pull Request #165: DOC: Addresses #164

70 of 74 new or added lines in 2 files covered. (94.59%)

1 existing line in 1 file now uncovered.

4690 of 5884 relevant lines covered (79.71%)

1.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.82
/nitime/utils.py
1
"""Miscellaneous utilities for time series analysis.
2

3
"""
4
from __future__ import print_function
2×
5
import warnings
2×
6
import numpy as np
2×
7
import scipy.ndimage as ndimage
2×
8

9
from nitime.lazy import scipy_linalg as linalg
2×
10
from nitime.lazy import scipy_signal as sig
2×
11
from nitime.lazy import scipy_fftpack as fftpack
2×
12
from nitime.lazy import scipy_signal_signaltools as signaltools
2×
13
from nitime.lazy import scipy_stats_distributions as dists
2×
14
from nitime.lazy import scipy_interpolate as interpolate
2×
15

16

17
#-----------------------------------------------------------------------------
18
# Spectral estimation testing utilities
19
#-----------------------------------------------------------------------------
20
def square_window_spectrum(N, Fs):
2×
21
    r"""
22
    Calculate the analytical spectrum of a square window
23

24
    Parameters
25
    ----------
26
    N : int
27
       the size of the window
28

29
    Fs : float
30
       The sampling rate
31

32
    Returns
33
    -------
34
    float array - the frequency bands, given N and FS
35
    complex array: the power in the spectrum of the square window in the
36
    frequency bands
37

38
    Notes
39
    -----
40
    This is equation 21c in Harris (1978):
41

42
    .. math::
43

44
      W(\theta) = exp(-j \frac{N-1}{2} \theta) \frac{sin \frac{N\theta}{2}} {sin\frac{\theta}{2}}
45

46
    F.J. Harris (1978). On the use of windows for harmonic analysis with the
47
    discrete Fourier transform. Proceedings of the IEEE, 66:51-83
48
    """
49
    f = get_freqs(Fs, N - 1)
!
50
    j = 0 + 1j
!
51
    a = -j * (N - 1) * f / 2
!
52
    b = np.sin(N * f / 2.0)
!
53
    c = np.sin(f / 2.0)
!
54
    make = np.exp(a) * b / c
!
55

56
    return f,  make[1:] / make[1]
!
57

58

59
def hanning_window_spectrum(N, Fs):
2×
60
    r"""
61
    Calculate the analytical spectrum of a Hanning window
62

63
    Parameters
64
    ----------
65
    N : int
66
       The size of the window
67

68
    Fs : float
69
       The sampling rate
70

71
    Returns
72
    -------
73
    float array - the frequency bands, given N and FS
74
    complex array: the power in the spectrum of the square window in the
75
    frequency bands
76

77
    Notes
78
    -----
79
    This is equation 28b in Harris (1978):
80

81
    .. math::
82

83
      W(\theta) = 0.5 D(\theta) + 0.25 (D(\theta - \frac{2\pi}{N}) +
84
                D(\theta + \frac{2\pi}{N}) ),
85

86
    where:
87

88
    .. math::
89

90
      D(\theta) = exp(j\frac{\theta}{2})
91
                  \frac{sin\frac{N\theta}{2}}{sin\frac{\theta}{2}}
92

93
    F.J. Harris (1978). On the use of windows for harmonic analysis with the
94
    discrete Fourier transform. Proceedings of the IEEE, 66:51-83
95
    """
96
    #A helper function
97
    D = lambda theta, n: (
!
98
        np.exp((0 + 1j) * theta / 2) * ((np.sin(n * theta / 2)) / (theta / 2)))
99

100
    f = get_freqs(Fs, N)
!
101

102
    make = 0.5 * D(f, N) + 0.25 * (D((f - (2 * np.pi / N)), N) +
!
103
                                   D((f + (2 * np.pi / N)), N))
104
    return f, make[1:] / make[1]
!
105

106

107
def ar_generator(N=512, sigma=1., coefs=None, drop_transients=0, v=None):
2×
108
    """
109
    This generates a signal u(n) = a1*u(n-1) + a2*u(n-2) + ... + v(n)
110
    where v(n) is a stationary stochastic process with zero mean
111
    and variance = sigma. XXX: confusing variance notation
112

113
    Parameters
114
    ----------
115

116
    N : float
117
       The number of points in the AR process generated. Default: 512
118
    sigma : float
119
       The variance of the noise in the AR process. Default: 1
120
    coefs : list or array of floats
121
       The AR model coefficients. Default: [2.7607, -3.8106, 2.6535, -0.9238],
122
       which is a sequence shown to be well-estimated by an order 8 AR system.
123
    drop_transients : float
124
       How many samples to drop from the beginning of the sequence (the
125
       transient phases of the process), so that the process can be considered
126
       stationary.
127
    v : float array
128
       Optionally, input a specific sequence of noise samples (this over-rides
129
       the sigma parameter). Default: None
130

131
    Returns
132
    -------
133

134
    u : ndarray
135
       the AR sequence
136
    v : ndarray
137
       the unit-variance innovations sequence
138
    coefs : ndarray
139
       feedback coefficients from k=1,len(coefs)
140

141
    The form of the feedback coefficients is a little different than
142
    the normal linear constant-coefficient difference equation. Therefore
143
    the transfer function implemented in this method is
144

145
    H(z) = sigma**0.5 / ( 1 - sum_k coefs(k)z**(-k) )    1 <= k <= P
146

147
    Examples
148
    --------
149

150
    >>> import nitime.algorithms as alg
151
    >>> ar_seq, nz, alpha = ar_generator()
152
    >>> fgrid, hz = alg.freq_response(1.0, a=np.r_[1, -alpha])
153
    >>> sdf_ar = (hz * hz.conj()).real
154

155
    """
156
    if coefs is None:
2×
157
        # this sequence is shown to be estimated well by an order 8 AR system
158
        coefs = np.array([2.7607, -3.8106, 2.6535, -0.9238])
2×
159
    else:
160
        coefs = np.asarray(coefs)
2×
161

162
    # The number of terms we generate must include the dropped transients, and
163
    # then at the end we cut those out of the returned array.
164
    N += drop_transients
2×
165

166
    # Typically uses just pass sigma in, but optionally they can provide their
167
    # own noise vector, case in which we use it
168
    if v is None:
2×
169
        v = np.random.normal(size=N)
2×
170
        v -= v[drop_transients:].mean()
2×
171

172
    b = [sigma ** 0.5]
2×
173
    a = np.r_[1, -coefs]
2×
174
    u = sig.lfilter(b, a, v)
2×
175

176
    # Only return the data after the drop_transients terms
177
    return u[drop_transients:], v[drop_transients:], coefs
2×
178

179

180
def circularize(x, bottom=0, top=2 * np.pi, deg=False):
2×
181
    """Maps the input into the continuous interval (bottom, top) where
182
    bottom defaults to 0 and top defaults to 2*pi
183

184
    Parameters
185
    ----------
186

187
    x : ndarray - the input array
188

189
    bottom : float, optional (defaults to 0).
190
        If you want to set the bottom of the interval into which you
191
        modulu to something else than 0.
192

193
    top : float, optional (defaults to 2*pi).
194
        If you want to set the top of the interval into which you
195
        modulu to something else than 2*pi
196

197
    Returns
198
    -------
199
    The input array, mapped into the interval (bottom,top)
200

201
    """
202
    x = np.asarray([x])
!
203

204
    if  (np.all(x[np.isfinite(x)] >= bottom) and
!
205
         np.all(x[np.isfinite(x)] <= top)):
206
        return np.squeeze(x)
!
207
    else:
208
        x[np.where(x < 0)] += top
!
209
        x[np.where(x > top)] -= top
!
210

211
    return np.squeeze(circularize(x, bottom=bottom, top=top))
!
212

213

214
def dB(x, power=True):
2×
215
    """Convert the values in x to decibels.
216
    If the values in x are in 'power'-like units, then set the power
217
    flag accordingly
218

219
    1) dB(x) = 10log10(x)                     (if power==True)
220
    2) dB(x) = 10log10(|x|^2) = 20log10(|x|)  (if power==False)
221
    """
222
    if not power:
!
223
        return 20 * np.log10(np.abs(x))
!
224
    return 10 * np.log10(np.abs(x))
!
225

226

227
#-----------------------------------------------------------------------------
228
# Stats utils
229
#-----------------------------------------------------------------------------
230

231
def normalize_coherence(x, dof, copy=True):
2×
232
    """
233
    The generally accepted choice to transform coherence measures into
234
    a more normal distribution
235

236
    Parameters
237
    ----------
238
    x : ndarray, real
239
       square-root of magnitude-square coherence measures
240
    dof : int
241
       number of degrees of freedom in the multitaper model
242
    copy : bool
243
        Copy or return inplace modified x.
244

245
    Returns
246
    -------
247
    y : ndarray, real
248
        The transformed array.
249
    """
250
    if copy:
2×
251
        x = x.copy()
2×
252
    np.arctanh(x, x)
2×
253
    x *= np.sqrt(dof)
2×
254
    return x
2×
255

256

257
def normal_coherence_to_unit(y, dof, out=None):
2×
258
    """
259
    The inverse transform of the above normalization
260
    """
261
    if out is None:
2×
262
        x = y / np.sqrt(dof)
!
263
    else:
264
        y /= np.sqrt(dof)
2×
265
        x = y
2×
266
    np.tanh(x, x)
2×
267
    return x
2×
268

269

270
def expected_jk_variance(K):
2×
271
    """Compute the expected value of the jackknife variance estimate
272
    over K windows below. This expected value formula is based on the
273
    asymptotic expansion of the trigamma function derived in
274
    [Thompson_1994]
275

276
    Parameters
277
    ---------
278

279
    K : int
280
      Number of tapers used in the multitaper method
281

282
    Returns
283
    -------
284

285
    evar : float
286
      Expected value of the jackknife variance estimator
287

288
    """
289

290
    kf = float(K)
!
291
    return ((1 / kf) * (kf - 1) / (kf - 0.5) *
!
292
            ((kf - 1) / (kf - 2)) ** 2 * (kf - 3) / (kf - 2))
293

294

295
def jackknifed_sdf_variance(yk, eigvals, sides='onesided', adaptive=True):
2×
296
    r"""
297
    Returns the variance of the log-sdf estimated through jack-knifing
298
    a group of independent sdf estimates.
299

300
    Parameters
301
    ----------
302

303
    yk : ndarray (K, L)
304
       The K DFTs of the tapered sequences
305
    eigvals : ndarray (K,)
306
       The eigenvalues corresponding to the K DPSS tapers
307
    sides : str, optional
308
       Compute the jackknife pseudovalues over as one-sided or
309
       two-sided spectra
310
    adpative : bool, optional
311
       Compute the adaptive weighting for each jackknife pseudovalue
312

313
    Returns
314
    -------
315

316
    var : The estimate for log-sdf variance
317

318
    Notes
319
    -----
320

321
    The jackknifed mean estimate is distributed about the true mean as
322
    a Student's t-distribution with (K-1) degrees of freedom, and
323
    standard error equal to sqrt(var). However, Thompson and Chave [1]
324
    point out that this variance better describes the sample mean.
325

326

327
    [1] Thomson D J, Chave A D (1991) Advances in Spectrum Analysis and Array
328
    Processing (Prentice-Hall, Englewood Cliffs, NJ), 1, pp 58-113.
329
    """
330
    K = yk.shape[0]
2×
331

332
    from nitime.algorithms import mtm_cross_spectrum
2×
333

334
    # the samples {S_k} are defined, with or without weights, as
335
    # S_k = | x_k |**2
336
    # | x_k |**2 = | y_k * d_k |**2          (with adaptive weights)
337
    # | x_k |**2 = | y_k * sqrt(eig_k) |**2  (without adaptive weights)
338

339
    all_orders = set(range(K))
2×
340
    jk_sdf = []
2×
341
    # get the leave-one-out estimates -- ideally, weights are recomputed
342
    # for each leave-one-out. This is now the case.
343
    for i in range(K):
2×
344
        items = list(all_orders.difference([i]))
2×
345
        spectra_i = np.take(yk, items, axis=0)
2×
346
        eigs_i = np.take(eigvals, items)
2×
347
        if adaptive:
2×
348
            # compute the weights
349
            weights, _ = adaptive_weights(spectra_i, eigs_i, sides=sides)
2×
350
        else:
351
            weights = eigs_i[:, None]
2×
352
        # this is the leave-one-out estimate of the sdf
353
        jk_sdf.append(
2×
354
            mtm_cross_spectrum(
355
                spectra_i, spectra_i, weights, sides=sides
356
                )
357
            )
358
    # log-transform the leave-one-out estimates and the mean of estimates
359
    jk_sdf = np.log(jk_sdf)
2×
360
    # jk_avg should be the mean of the log(jk_sdf(i))
361
    jk_avg = jk_sdf.mean(axis=0)
2×
362

363
    K = float(K)
2×
364

365
    jk_var = (jk_sdf - jk_avg)
2×
366
    np.power(jk_var, 2, jk_var)
2×
367
    jk_var = jk_var.sum(axis=0)
2×
368

369
    # Thompson's recommended factor, eq 18
370
    # Jackknifing Multitaper Spectrum Estimates
371
    # IEEE SIGNAL PROCESSING MAGAZINE [20] JULY 2007
372
    f = (K - 1) ** 2 / K / (K - 0.5)
2×
373
    jk_var *= f
2×
374
    return jk_var
2×
375

376

377
def jackknifed_coh_variance(tx, ty, eigvals, adaptive=True):
2×
378
    """
379
    Returns the variance of the coherency between x and y, estimated
380
    through jack-knifing the tapered samples in {tx, ty}.
381

382
    Parameters
383
    ----------
384

385
    tx : ndarray, (K, L)
386
       The K complex spectra of tapered timeseries x
387
    ty : ndarray, (K, L)
388
       The K complex spectra of tapered timeseries y
389
    eigvals : ndarray (K,)
390
       The eigenvalues associated with the K DPSS tapers
391

392
    Returns
393
    -------
394

395
    jk_var : ndarray
396
       The variance computed in the transformed domain (see
397
       normalize_coherence)
398
    """
399

400
    K = tx.shape[0]
2×
401

402
    # calculate leave-one-out estimates of MSC (magnitude squared coherence)
403
    jk_coh = []
2×
404
    # coherence is symmetric (right??)
405
    sides = 'onesided'
2×
406
    all_orders = set(range(K))
2×
407

408
    import nitime.algorithms as alg
2×
409

410
    # get the leave-one-out estimates
411
    for i in range(K):
2×
412
        items = list(all_orders.difference([i]))
2×
413
        tx_i = np.take(tx, items, axis=0)
2×
414
        ty_i = np.take(ty, items, axis=0)
2×
415
        eigs_i = np.take(eigvals, items)
2×
416
        if adaptive:
2×
417
            wx, _ = adaptive_weights(tx_i, eigs_i, sides=sides)
2×
418
            wy, _ = adaptive_weights(ty_i, eigs_i, sides=sides)
2×
419
        else:
420
            wx = wy = eigs_i[:, None]
2×
421
        # The CSD
422
        sxy_i = alg.mtm_cross_spectrum(tx_i, ty_i, (wx, wy), sides=sides)
2×
423
        # The PSDs
424
        sxx_i = alg.mtm_cross_spectrum(tx_i, tx_i, wx, sides=sides)
2×
425
        syy_i = alg.mtm_cross_spectrum(ty_i, ty_i, wy, sides=sides)
2×
426
        # these are the | c_i | samples
427
        msc = np.abs(sxy_i)
2×
428
        msc /= np.sqrt(sxx_i * syy_i)
2×
429
        jk_coh.append(msc)
2×
430

431
    jk_coh = np.array(jk_coh)
2×
432
    # now normalize the coherence estimates and take the mean
433
    normalize_coherence(jk_coh, 2 * K - 2, copy=False)  # inplace
2×
434
    jk_avg = np.mean(jk_coh, axis=0)
2×
435

436
    jk_var = (jk_coh - jk_avg)
2×
437
    np.power(jk_var, 2, jk_var)
2×
438
    jk_var = jk_var.sum(axis=0)
2×
439

440
    # Do/Don't use the alternative scaling here??
441
    f = float(K - 1) / K
2×
442

443
    jk_var *= f
2×
444

445
    return jk_var
2×
446

447

448
#-----------------------------------------------------------------------------
449
# Multitaper utils
450
#-----------------------------------------------------------------------------
451
def adaptive_weights(yk, eigvals, sides='onesided', max_iter=150):
2×
452
    r"""
453
    Perform an iterative procedure to find the optimal weights for K
454
    direct spectral estimators of DPSS tapered signals.
455

456
    Parameters
457
    ----------
458

459
    yk : ndarray (K, N)
460
       The K DFTs of the tapered sequences
461
    eigvals : ndarray, length-K
462
       The eigenvalues of the DPSS tapers
463
    sides : str
464
       Whether to compute weights on a one-sided or two-sided spectrum
465
    max_iter : int
466
       Maximum number of iterations for weight computation
467

468
    Returns
469
    -------
470

471
    weights, nu
472

473
       The weights (array like sdfs), and the
474
       "equivalent degrees of freedom" (array length-L)
475

476
    Notes
477
    -----
478

479
    The weights to use for making the multitaper estimate, such that
480
    :math:`S_{mt} = \sum_{k} |w_k|^2S_k^{mt} / \sum_{k} |w_k|^2`
481

482
    If there are less than 3 tapers, then the adaptive weights are not
483
    found. The square root of the eigenvalues are returned as weights,
484
    and the degrees of freedom are 2*K
485

486
    """
487
    from nitime.algorithms import mtm_cross_spectrum
2×
488
    K = len(eigvals)
2×
489
    if len(eigvals) < 3:
2×
490
        print("""
!
491
        Warning--not adaptively combining the spectral estimators
492
        due to a low number of tapers.
493
        """)
494
        # we'll hope this is a correct length for L
495
        N = yk.shape[-1]
!
496
        L = N / 2 + 1 if sides == 'onesided' else N
!
497
        return (np.multiply.outer(np.sqrt(eigvals), np.ones(L)), 2 * K)
!
498
    rt_eig = np.sqrt(eigvals)
2×
499

500
    # combine the SDFs in the traditional way in order to estimate
501
    # the variance of the timeseries
502
    N = yk.shape[1]
2×
503
    sdf = mtm_cross_spectrum(yk, yk, eigvals[:, None], sides=sides)
2×
504
    L = sdf.shape[-1]
2×
505
    var_est = np.sum(sdf, axis=-1) / N
2×
506
    bband_sup = (1-eigvals)*var_est
2×
507

508
    # The process is to iteratively switch solving for the following
509
    # two expressions:
510
    # (1) Adaptive Multitaper SDF:
511
    # S^{mt}(f) = [ sum |d_k(f)|^2 S_k(f) ]/ sum |d_k(f)|^2
512
    #
513
    # (2) Weights
514
    # d_k(f) = [sqrt(lam_k) S^{mt}(f)] / [lam_k S^{mt}(f) + E{B_k(f)}]
515
    #
516
    # Where lam_k are the eigenvalues corresponding to the DPSS tapers,
517
    # and the expected value of the broadband bias function
518
    # E{B_k(f)} is replaced by its full-band integration
519
    # (1/2pi) int_{-pi}^{pi} E{B_k(f)} = sig^2(1-lam_k)
520

521
    # start with an estimate from incomplete data--the first 2 tapers
522
    sdf_iter = mtm_cross_spectrum(yk[:2], yk[:2], eigvals[:2, None],
2×
523
                                  sides=sides)
524
    err = np.zeros((K, L))
2×
525
    # for numerical considerations, don't bother doing adaptive
526
    # weighting after 150 dB down
527
    min_pwr = sdf_iter.max() * 10 ** (-150/20.)
2×
528
    default_weights = np.where(sdf_iter < min_pwr)[0]
2×
529
    adaptiv_weights = np.where(sdf_iter >= min_pwr)[0]
2×
530

531
    w_def = rt_eig[:,None] * sdf_iter[default_weights]
2×
532
    w_def /= eigvals[:, None] * sdf_iter[default_weights] + bband_sup[:,None]
2×
533

534
    d_sdfs = np.abs(yk[:,adaptiv_weights])**2
2×
535
    if L < N:
2×
536
        d_sdfs *= 2
2×
537
    sdf_iter = sdf_iter[adaptiv_weights]
2×
538
    yk = yk[:,adaptiv_weights]
2×
539
    for n in range(max_iter):
2×
540
        d_k = rt_eig[:,None] * sdf_iter[None, :]
2×
541
        d_k /= eigvals[:, None]*sdf_iter[None, :] + bband_sup[:,None]
2×
542
        # Test for convergence -- this is overly conservative, since
543
        # iteration only stops when all frequencies have converged.
544
        # A better approach is to iterate separately for each freq, but
545
        # that is a nonvectorized algorithm.
546
        #sdf_iter = mtm_cross_spectrum(yk, yk, d_k, sides=sides)
547
        sdf_iter = np.sum( d_k**2 * d_sdfs, axis=0 )
2×
548
        sdf_iter /= np.sum( d_k**2, axis=0 )
2×
549
        # Compute the cost function from eq 5.4 in Thomson 1982
550
        cfn = eigvals[:,None] * (sdf_iter[None,:] - d_sdfs)
2×
551
        cfn /= (eigvals[:,None] * sdf_iter[None,:] + bband_sup[:,None])**2
2×
552
        cfn = np.sum(cfn, axis=0)
2×
553
        # there seem to be some pathological freqs sometimes ..
554
        # this should be a good heuristic
555
        if np.percentile(cfn**2, 95) < 1e-12:
2×
556
            break
2×
557
    else:  # If you have reached maximum number of iterations
558
        # Issue a warning and return non-converged weights:
559
        e_s = 'Breaking due to iterative meltdown in '
!
560
        e_s += 'nitime.utils.adaptive_weights.'
!
561
        warnings.warn(e_s, RuntimeWarning)
!
562
    weights = np.zeros( (K,L) )
2×
563
    weights[:,adaptiv_weights] = d_k
2×
564
    weights[:,default_weights] = w_def
2×
565
    nu = 2 * (weights ** 2).sum(axis=-2)
2×
566
    return weights, nu
2×
567

568

569
def dpss_windows(N, NW, Kmax, interp_from=None, interp_kind='linear'):
2×
570
    """
571
    Returns the Discrete Prolate Spheroidal Sequences of orders [0,Kmax-1]
572
    for a given frequency-spacing multiple NW and sequence length N.
573

574
    Parameters
575
    ----------
576
    N : int
577
        sequence length
578
    NW : float, unitless
579
        standardized half bandwidth corresponding to 2NW = BW/f0 = BW*N*dt
580
        but with dt taken as 1
581
    Kmax : int
582
        number of DPSS windows to return is Kmax (orders 0 through Kmax-1)
583
    interp_from : int (optional)
584
        The dpss can be calculated using interpolation from a set of dpss
585
        with the same NW and Kmax, but shorter N. This is the length of this
586
        shorter set of dpss windows.
587
    interp_kind : str (optional)
588
        This input variable is passed to scipy.interpolate.interp1d and
589
        specifies the kind of interpolation as a string ('linear', 'nearest',
590
        'zero', 'slinear', 'quadratic, 'cubic') or as an integer specifying the
591
        order of the spline interpolator to use.
592

593

594
    Returns
595
    -------
596
    v, e : tuple,
597
        v is an array of DPSS windows shaped (Kmax, N)
598
        e are the eigenvalues
599

600
    Notes
601
    -----
602
    Tridiagonal form of DPSS calculation from:
603

604
    Slepian, D. Prolate spheroidal wave functions, Fourier analysis, and
605
    uncertainty V: The discrete case. Bell System Technical Journal,
606
    Volume 57 (1978), 1371430
607
    """
608
    Kmax = int(Kmax)
2×
609
    W = float(NW) / N
2×
610
    nidx = np.arange(N, dtype='d')
2×
611

612
    # In this case, we create the dpss windows of the smaller size
613
    # (interp_from) and then interpolate to the larger size (N)
614
    if interp_from is not None:
2×
615
        if interp_from > N:
2×
NEW
616
            e_s = 'In dpss_windows, interp_from is: %s ' % interp_from
!
NEW
617
            e_s += 'and N is: %s. ' % N
!
NEW
618
            e_s += 'Please enter interp_from smaller than N.'
!
NEW
619
            raise ValueError(e_s)
!
620
        dpss = []
2×
621
        d, e = dpss_windows(interp_from, NW, Kmax)
2×
622
        for this_d in d:
2×
623
            x = np.arange(this_d.shape[-1])
2×
624
            I = interpolate.interp1d(x, this_d, kind=interp_kind)
2×
625
            d_temp = I(np.linspace(0, this_d.shape[-1] - 1, N, endpoint=False))
2×
626

627
            # Rescale:
628
            d_temp = d_temp / np.sqrt(np.sum(d_temp ** 2))
2×
629

630
            dpss.append(d_temp)
2×
631

632
        dpss = np.array(dpss)
2×
633

634
    else:
635
        # here we want to set up an optimization problem to find a sequence
636
        # whose energy is maximally concentrated within band [-W,W].
637
        # Thus, the measure lambda(T,W) is the ratio between the energy within
638
        # that band, and the total energy. This leads to the eigen-system
639
        # (A - (l1)I)v = 0, where the eigenvector corresponding to the largest
640
        # eigenvalue is the sequence with maximally concentrated energy. The
641
        # collection of eigenvectors of this system are called Slepian
642
        # sequences, or discrete prolate spheroidal sequences (DPSS). Only the
643
        # first K, K = 2NW/dt orders of DPSS will exhibit good spectral
644
        # concentration
645
        # [see http://en.wikipedia.org/wiki/Spectral_concentration_problem]
646

647
        # Here I set up an alternative symmetric tri-diagonal eigenvalue
648
        # problem such that
649
        # (B - (l2)I)v = 0, and v are our DPSS (but eigenvalues l2 != l1)
650
        # the main diagonal = ([N-1-2*t]/2)**2 cos(2PIW), t=[0,1,2,...,N-1]
651
        # and the first off-diagonal = t(N-t)/2, t=[1,2,...,N-1]
652
        # [see Percival and Walden, 1993]
653
        diagonal = ((N - 1 - 2 * nidx) / 2.) ** 2 * np.cos(2 * np.pi * W)
2×
654
        off_diag = np.zeros_like(nidx)
2×
655
        off_diag[:-1] = nidx[1:] * (N - nidx[1:]) / 2.
2×
656
        # put the diagonals in LAPACK "packed" storage
657
        ab = np.zeros((2, N), 'd')
2×
658
        ab[1] = diagonal
2×
659
        ab[0, 1:] = off_diag[:-1]
2×
660
        # only calculate the highest Kmax eigenvalues
661
        w = linalg.eigvals_banded(ab, select='i',
2×
662
                                  select_range=(N - Kmax, N - 1))
663
        w = w[::-1]
2×
664

665
        # find the corresponding eigenvectors via inverse iteration
666
        t = np.linspace(0, np.pi, N)
2×
667
        dpss = np.zeros((Kmax, N), 'd')
2×
668
        for k in range(Kmax):
2×
669
            dpss[k] = tridi_inverse_iteration(
2×
670
                diagonal, off_diag, w[k], x0=np.sin((k + 1) * t)
671
                )
672

673
    # By convention (Percival and Walden, 1993 pg 379)
674
    # * symmetric tapers (k=0,2,4,...) should have a positive average.
675
    # * antisymmetric tapers should begin with a positive lobe
676
    fix_symmetric = (dpss[0::2].sum(axis=1) < 0)
2×
677
    for i, f in enumerate(fix_symmetric):
2×
678
        if f:
2×
679
            dpss[2 * i] *= -1
2×
680
    # rather than test the sign of one point, test the sign of the
681
    # linear slope up to the first (largest) peak
682
    pk = np.argmax(np.abs(dpss[1::2, :N//2]), axis=1)
2×
683
    for i, p in enumerate(pk):
2×
684
        if np.sum(dpss[2 * i + 1, :p]) < 0:
2×
685
            dpss[2 * i + 1] *= -1
2×
686

687
    # Now find the eigenvalues of the original spectral concentration problem
688
    # Use the autocorr sequence technique from Percival and Walden, 1993 pg 390
689
    dpss_rxx = autocorr(dpss) * N
2×
690
    r = 4 * W * np.sinc(2 * W * nidx)
2×
691
    r[0] = 2 * W
2×
692
    eigvals = np.dot(dpss_rxx, r)
2×
693

694
    return dpss, eigvals
2×
695

696

697
def tapered_spectra(s, tapers, NFFT=None, low_bias=True):
2×
698
    """
699
    Compute the tapered spectra of the rows of s.
700

701
    Parameters
702
    ----------
703

704
    s : ndarray, (n_arr, n_pts)
705
        An array whose rows are timeseries.
706

707
    tapers : ndarray or container
708
        Either the precomputed DPSS tapers, or the pair of parameters
709
        (NW, K) needed to compute K tapers of length n_pts.
710

711
    NFFT : int
712
        Number of FFT bins to compute
713

714
    low_bias : Boolean
715
        If compute DPSS, automatically select tapers corresponding to
716
        > 90% energy concentration.
717

718
    Returns
719
    -------
720

721
    t_spectra : ndarray, shaped (n_arr, K, NFFT)
722
      The FFT of the tapered sequences in s. First dimension is squeezed
723
      out if n_arr is 1.
724
    eigvals : ndarray
725
      The eigenvalues are also returned if DPSS are calculated here.
726

727
    """
728
    N = s.shape[-1]
2×
729
    # XXX: don't allow NFFT < N -- not every implementation is so restrictive!
730
    if NFFT is None or NFFT < N:
2×
731
        NFFT = N
2×
732
    rest_of_dims = s.shape[:-1]
2×
733
    M = int(np.product(rest_of_dims))
2×
734

735
    s = s.reshape(int(np.product(rest_of_dims)), N)
2×
736
    # de-mean this sucker
737
    s = remove_bias(s, axis=-1)
2×
738

739
    if not isinstance(tapers, np.ndarray):
2×
740
        # then tapers is (NW, K)
741
        args = (N,) + tuple(tapers)
2×
742
        dpss, eigvals = dpss_windows(*args)
2×
743
        if low_bias:
2×
744
            keepers = (eigvals > 0.9)
2×
745
            dpss = dpss[keepers]
2×
746
            eigvals = eigvals[keepers]
2×
747
        tapers = dpss
2×
748
    else:
749
        eigvals = None
2×
750
    K = tapers.shape[0]
2×
751
    sig_sl = [slice(None)] * len(s.shape)
2×
752
    sig_sl.insert(len(s.shape) - 1, np.newaxis)
2×
753

754
    # tapered.shape is (M, Kmax, N)
755
    tapered = s[tuple(sig_sl)] * tapers
2×
756

757
    # compute the y_{i,k}(f) -- full FFT takes ~1.5x longer, but unpacking
758
    # results of real-valued FFT eats up memory
759
    t_spectra = fftpack.fft(tapered, n=NFFT, axis=-1)
2×
760
    t_spectra.shape = rest_of_dims + (K, NFFT)
2×
761
    if eigvals is None:
2×
762
        return t_spectra
2×
763
    return t_spectra, eigvals
2×
764

765

766

767
def detect_lines(s, tapers, p=None, **taper_kws):
2×
768
    """
769
    Detect the presence of line spectra in s using the F-test
770
    described in "Spectrum estimation and harmonic analysis" (Thompson 81).
771
    Strategies for detecting harmonics in low SNR include increasing the
772
    number of FFT points (NFFT keyword arg) and/or increasing the stability
773
    of the spectral estimate by using more tapers (higher NW parameter).
774

775
    s : ndarray
776
        The sequence(s) to test. If s.ndim > 1, then test sequences in
777
        the last axis in parallel
778

779
    tapers : ndarray or container
780
        Either the precomputed DPSS tapers, or the pair of parameters
781
        (NW, K) needed to compute K tapers of length n_pts.
782

783
    p : float
784
        The confidence threshold: under the null hypothesis of
785
        a locally white spectrum, there is a threshold such that
786
        there is a (1-p)% chance of a line amplitude being larger
787
        than that threshold. Only detect lines with amplitude greater
788
        than this threshold. The default is 1/NFFT, to control for false
789
        positives.
790

791
    taper_kws
792
        Options for the tapered_spectra method, if no DPSS are provided.
793

794
    Returns
795
    -------
796

797
    (freq, beta) : sequence
798
        The frequencies (normalized in [0, .5]) and coefficients of the
799
        complex exponentials detected in the spectrum. A pair is returned
800
        for each sequence tested.
801

802
        One can reconstruct the line components as such:
803

804
        sn = 2*(beta[:,None]*np.exp(i*2*np.pi*np.arange(N)*freq[:,None])).real
805
        sn = sn.sum(axis=0)
806

807
    """
808
    N = s.shape[-1]
2×
809
    # Some boiler-plate --
810
    # 1) set up tapers
811
    # 2) perform FFT on all windowed series
812
    if not isinstance(tapers, np.ndarray):
2×
813
        # then tapers is (NW, K)
814
        args = (N,) + tuple(tapers)
2×
815
        dpss, eigvals = dpss_windows(*args)
2×
816
        if taper_kws.pop('low_bias', False):
2×
817
            keepers = (eigvals > 0.9)
2×
818
            dpss = dpss[keepers]
2×
819
        tapers = dpss
2×
820
    # spectra is (n_arr, K, nfft)
821
    spectra = tapered_spectra(s, tapers, **taper_kws)
2×
822
    nfft = spectra.shape[-1]
2×
823
    spectra = spectra[..., :nfft//2 + 1]
2×
824

825
    # Set up some data for the following calculations --
826
    #   get the DC component of the taper spectra
827
    K = tapers.shape[0]
2×
828
    U0 = tapers.sum(axis=1)
2×
829
    U_sq = np.sum(U0**2)
2×
830
    #  first order linear regression for mu to explain spectra
831
    mu = np.sum( U0[:,None] * spectra, axis=-2 ) / U_sq
2×
832

833
    # numerator of F-stat -- strength of regression
834
    numr = 0.5 * np.abs(mu)**2 * U_sq
2×
835
    numr[...,0] = 1; # don't care about DC
2×
836
    # denominator -- strength of residual
837
    spectra = np.rollaxis(spectra, -2, 0)
2×
838
    U0.shape = (K,) + (1,) * (spectra.ndim-1)
2×
839
    denomr = spectra - U0*mu
2×
840
    denomr = np.sum(np.abs(denomr)**2, axis=0) / (2*K-2)
2×
841
    denomr[...,0] = 1;
2×
842
    f_stat = numr / denomr
2×
843

844
    # look for lines in each F-spectrum
845
    if not p:
2×
846
        # the number of simultaneous tests are nfft/2, so this puts
847
        # the expected value for false detection somewhere less than 1
848
        p = 1.0/nfft
2×
849
    #thresh = dists.f.isf(p, 2, 2*K-2)
850
    thresh = dists.f.isf(p, 2, K-1)
2×
851
    f_stat = np.atleast_2d(f_stat)
2×
852
    mu = np.atleast_2d(mu)
2×
853
    lines = ()
2×
854
    for fs, m in zip(f_stat, mu):
2×
855
        detected = np.where(fs > thresh)[0]
2×
856
        # do a quick pass through the detected lines to reject multiple
857
        # hits within the 2NW resolution of the MT analysis -- approximate
858
        # 2NW by K
859
        ddiff = np.diff(detected)
2×
860
        flagged_groups, last_group = ndimage.label( (ddiff < K) )
2×
861
        for g in range(1,last_group+1):
2×
862
            idx = np.where(flagged_groups==g)[0]
!
863
            idx = np.r_[idx, idx[-1]+1]
!
864
            # keep the super-threshold point with largest amplitude
865
            mx = np.argmax(np.abs(m[ detected[idx] ]))
!
866
            i_sv = detected[idx[mx]]
!
867
            detected[idx] = -1
!
868
            detected[idx[mx]] = i_sv
!
869
        detected = detected[detected>0]
2×
870
        if len(detected):
2×
871
            lines = lines + ( (detected/float(nfft), m[detected]), )
2×
872
        else:
873
            lines = lines + ( (), )
!
874
    if len(lines) == 1:
2×
875
        lines = lines[0]
2×
876
    return lines
2×
877

878

879
#-----------------------------------------------------------------------------
880
# Eigensystem utils
881
#-----------------------------------------------------------------------------
882

883
# If we can get it, we want the cythonized version
884
try:
2×
885
    from _utils import tridisolve
2×
886

887
# If that doesn't work, we define it here:
888
except ImportError:
1×
889
    def tridisolve(d, e, b, overwrite_b=True):
1×
890
        """
891
        Symmetric tridiagonal system solver,
892
        from Golub and Van Loan, Matrix Computations pg 157
893

894
        Parameters
895
        ----------
896

897
        d : ndarray
898
          main diagonal stored in d[:]
899
        e : ndarray
900
          superdiagonal stored in e[:-1]
901
        b : ndarray
902
          RHS vector
903

904
        Returns
905
        -------
906

907
        x : ndarray
908
          Solution to Ax = b (if overwrite_b is False). Otherwise solution is
909
          stored in previous RHS vector b
910

911
        """
912
        N = len(b)
1×
913
        # work vectors
914
        dw = d.copy()
1×
915
        ew = e.copy()
1×
916
        if overwrite_b:
1×
917
            x = b
1×
918
        else:
919
            x = b.copy()
!
920
        for k in range(1, N):
1×
921
            # e^(k-1) = e(k-1) / d(k-1)
922
            # d(k) = d(k) - e^(k-1)e(k-1) / d(k-1)
923
            t = ew[k - 1]
1×
924
            ew[k - 1] = t / dw[k - 1]
1×
925
            dw[k] = dw[k] - t * ew[k - 1]
1×
926
        for k in range(1, N):
1×
927
            x[k] = x[k] - ew[k - 1] * x[k - 1]
1×
928
        x[N - 1] = x[N - 1] / dw[N - 1]
1×
929
        for k in range(N - 2, -1, -1):
1×
930
            x[k] = x[k] / dw[k] - ew[k] * x[k + 1]
1×
931

932
        if not overwrite_b:
1×
933
            return x
!
934

935

936
def tridi_inverse_iteration(d, e, w, x0=None, rtol=1e-8):
2×
937
    """Perform an inverse iteration to find the eigenvector corresponding
938
    to the given eigenvalue in a symmetric tridiagonal system.
939

940
    Parameters
941
    ----------
942

943
    d : ndarray
944
      main diagonal of the tridiagonal system
945
    e : ndarray
946
      offdiagonal stored in e[:-1]
947
    w : float
948
      eigenvalue of the eigenvector
949
    x0 : ndarray
950
      initial point to start the iteration
951
    rtol : float
952
      tolerance for the norm of the difference of iterates
953

954
    Returns
955
    -------
956

957
    e : ndarray
958
      The converged eigenvector
959

960
    """
961
    eig_diag = d - w
2×
962
    if x0 is None:
2×
963
        x0 = np.random.randn(len(d))
!
964
    x_prev = np.zeros_like(x0)
2×
965
    norm_x = np.linalg.norm(x0)
2×
966
    # the eigenvector is unique up to sign change, so iterate
967
    # until || |x^(n)| - |x^(n-1)| ||^2 < rtol
968
    x0 /= norm_x
2×
969
    while np.linalg.norm(np.abs(x0) - np.abs(x_prev)) > rtol:
2×
970
        x_prev = x0.copy()
2×
971
        tridisolve(eig_diag, e, x0)
2×
972
        norm_x = np.linalg.norm(x0)
2×
973
        x0 /= norm_x
2×
974
    return x0
2×
975

976
#-----------------------------------------------------------------------------
977
# Correlation/Covariance utils
978
#-----------------------------------------------------------------------------
979

980

981
def remove_bias(x, axis):
2×
982
    "Subtracts an estimate of the mean from signal x at axis"
983
    padded_slice = [slice(d) for d in x.shape]
2×
984
    padded_slice[axis] = np.newaxis
2×
985
    mn = np.mean(x, axis=axis)
2×
986
    return x - mn[tuple(padded_slice)]
2×
987

988

989
def crosscov(x, y, axis=-1, all_lags=False, debias=True, normalize=True):
2×
990
    r"""Returns the crosscovariance sequence between two ndarrays.
991
    This is performed by calling fftconvolve on x, y[::-1]
992

993
    Parameters
994
    ----------
995

996
    x : ndarray
997
    y : ndarray
998
    axis : time axis
999
    all_lags : {True/False}
1000
       whether to return all nonzero lags, or to clip the length of s_xy
1001
       to be the length of x and y. If False, then the zero lag covariance
1002
       is at index 0. Otherwise, it is found at (len(x) + len(y) - 1)/2
1003
    debias : {True/False}
1004
       Always removes an estimate of the mean along the axis, unless
1005
       told not to (eg X and Y are known zero-mean)
1006

1007
    Returns
1008
    -------
1009

1010
    cxy : ndarray
1011
       The crosscovariance function
1012

1013
    Notes
1014
    -----
1015

1016
    cross covariance of processes x and y is defined as
1017

1018
    .. math::
1019

1020
    C_{xy}[k]=E\{(X(n+k)-E\{X\})(Y(n)-E\{Y\})^{*}\}
1021

1022
    where X and Y are discrete, stationary (or ergodic) random processes
1023

1024
    Also note that this routine is the workhorse for all auto/cross/cov/corr
1025
    functions.
1026
    """
1027

1028
    if x.shape[axis] != y.shape[axis]:
2×
1029
        raise ValueError(
!
1030
            'crosscov() only works on same-length sequences for now'
1031
            )
1032
    if debias:
2×
1033
        x = remove_bias(x, axis)
2×
1034
        y = remove_bias(y, axis)
2×
1035
    slicing = [slice(d) for d in x.shape]
2×
1036
    slicing[axis] = slice(None, None, -1)
2×
1037
    cxy = fftconvolve(x, y[tuple(slicing)].conj(), axis=axis, mode='full')
2×
1038
    N = x.shape[axis]
2×
1039
    if normalize:
2×
1040
        cxy /= N
2×
1041
    if all_lags:
2×
1042
        return cxy
2×
1043
    slicing[axis] = slice(N - 1, 2 * N - 1)
2×
1044
    return cxy[tuple(slicing)]
2×
1045

1046

1047
def crosscorr(x, y, **kwargs):
2×
1048
    r"""
1049
    Returns the crosscorrelation sequence between two ndarrays.
1050
    This is performed by calling fftconvolve on x, y[::-1]
1051

1052
    Parameters
1053
    ----------
1054

1055
    x : ndarray
1056
    y : ndarray
1057
    axis : time axis
1058
    all_lags : {True/False}
1059
       whether to return all nonzero lags, or to clip the length of r_xy
1060
       to be the length of x and y. If False, then the zero lag correlation
1061
       is at index 0. Otherwise, it is found at (len(x) + len(y) - 1)/2
1062

1063
    Returns
1064
    -------
1065

1066
    rxy : ndarray
1067
       The crosscorrelation function
1068

1069
    Notes
1070
    -----
1071

1072
    cross correlation is defined as
1073

1074
    .. math::
1075

1076
    R_{xy}[k]=E\{X[n+k]Y^{*}[n]\}
1077

1078
    where X and Y are discrete, stationary (ergodic) random processes
1079
    """
1080
    # just make the same computation as the crosscovariance,
1081
    # but without subtracting the mean
1082
    kwargs['debias'] = False
!
1083
    rxy = crosscov(x, y, **kwargs)
!
1084
    return rxy
!
1085

1086

1087
def autocov(x, **kwargs):
2×
1088
    r"""Returns the autocovariance of signal s at all lags.
1089

1090
    Parameters
1091
    ----------
1092

1093
    x : ndarray
1094
    axis : time axis
1095
    all_lags : {True/False}
1096
       whether to return all nonzero lags, or to clip the length of r_xy
1097
       to be the length of x and y. If False, then the zero lag correlation
1098
       is at index 0. Otherwise, it is found at (len(x) + len(y) - 1)/2
1099

1100
    Returns
1101
    -------
1102

1103
    cxx : ndarray
1104
       The autocovariance function
1105

1106
    Notes
1107
    -----
1108

1109
    Adheres to the definition
1110

1111
    .. math::
1112

1113
    C_{xx}[k]=E\{(X[n+k]-E\{X\})(X[n]-E\{X\})^{*}\}
1114

1115
    where X is a discrete, stationary (ergodic) random process
1116
    """
1117
    # only remove the mean once, if needed
1118
    debias = kwargs.pop('debias', True)
2×
1119
    axis = kwargs.get('axis', -1)
2×
1120
    if debias:
2×
1121
        x = remove_bias(x, axis)
2×
1122
    kwargs['debias'] = False
2×
1123
    return crosscov(x, x, **kwargs)
2×
1124

1125

1126
def autocorr(x, **kwargs):
2×
1127
    r"""Returns the autocorrelation of signal s at all lags.
1128

1129
    Parameters
1130
    ----------
1131

1132
    x : ndarray
1133
    axis : time axis
1134
    all_lags : {True/False}
1135
       whether to return all nonzero lags, or to clip the length of r_xy
1136
       to be the length of x and y. If False, then the zero lag correlation
1137
       is at index 0. Otherwise, it is found at (len(x) + len(y) - 1)/2
1138

1139
    Notes
1140
    -----
1141

1142
    Adheres to the definition
1143

1144
    .. math::
1145

1146
    R_{xx}[k]=E\{X[n+k]X^{*}[n]\}
1147

1148
    where X is a discrete, stationary (ergodic) random process
1149

1150
    """
1151
    # do same computation as autocovariance,
1152
    # but without subtracting the mean
1153
    kwargs['debias'] = False
2×
1154
    return autocov(x, **kwargs)
2×
1155

1156

1157
def fftconvolve(in1, in2, mode="full", axis=None):
2×
1158
    """ Convolve two N-dimensional arrays using FFT. See convolve.
1159

1160
    This is a fix of scipy.signal.fftconvolve, adding an axis argument.
1161
    """
1162
    s1 = np.array(in1.shape)
2×
1163
    s2 = np.array(in2.shape)
2×
1164
    complex_result = (np.issubdtype(in1.dtype, np.complex) or
2×
1165
                      np.issubdtype(in2.dtype, np.complex))
1166

1167
    if axis is None:
2×
1168
        size = s1 + s2 - 1
!
1169
        fslice = tuple([slice(0, int(sz)) for sz in size])
!
1170
    else:
1171
        equal_shapes = s1 == s2
2×
1172
        # allow equal_shapes[axis] to be False
1173
        equal_shapes[axis] = True
2×
1174
        assert equal_shapes.all(), 'Shape mismatch on non-convolving axes'
2×
1175
        size = s1[axis] + s2[axis] - 1
2×
1176
        fslice = [slice(l) for l in s1]
2×
1177
        fslice[axis] = slice(0, int(size))
2×
1178
        fslice = tuple(fslice)
2×
1179

1180
    # Always use 2**n-sized FFT
1181
    fsize = 2 ** int(np.ceil(np.log2(size)))
2×
1182
    if axis is None:
2×
1183
        IN1 = fftpack.fftn(in1, fsize)
!
1184
        IN1 *= fftpack.fftn(in2, fsize)
!
1185
        ret = fftpack.ifftn(IN1)[fslice].copy()
!
1186
    else:
1187
        IN1 = fftpack.fft(in1, fsize, axis=axis)
2×
1188
        IN1 *= fftpack.fft(in2, fsize, axis=axis)
2×
1189
        ret = fftpack.ifft(IN1, axis=axis)[fslice].copy()
2×
1190
    del IN1
2×
1191
    if not complex_result:
2×
1192
        ret = ret.real
2×
1193
    if mode == "full":
2×
1194
        return ret
2×
1195
    elif mode == "same":
!
1196
        if np.product(s1, axis=0) > np.product(s2, axis=0):
!
1197
            osize = s1
!
1198
        else:
1199
            osize = s2
!
1200
        return signaltools._centered(ret, osize)
!
1201
    elif mode == "valid":
!
1202
        return signaltools._centered(ret, abs(s2 - s1) + 1)
!
1203

1204

1205
#-----------------------------------------------------------------------------
1206
# 'get' utils
1207
#-----------------------------------------------------------------------------
1208
def get_freqs(Fs, n):
2×
1209
    """Returns the center frequencies of the frequency decomposotion of a time
1210
    series of length n, sampled at Fs Hz"""
1211

1212
    return np.linspace(0, float(Fs) / 2, float(n) / 2 + 1)
2×
1213

1214

1215
def circle_to_hz(omega, Fsamp):
2×
1216
    """For a frequency grid spaced on the unit circle of an imaginary plane,
1217
    return the corresponding frequency grid in Hz.
1218
    """
1219
    return Fsamp * omega / (2 * np.pi)
2×
1220

1221

1222
def get_bounds(f, lb=0, ub=None):
2×
1223
    """ Find the indices of the lower and upper bounds within an array f
1224

1225
    Parameters
1226
    ----------
1227
    f, array
1228

1229
    lb,ub, float
1230

1231
    Returns
1232
    -------
1233

1234
    lb_idx, ub_idx: the indices into 'f' which correspond to values bounded
1235
    between ub and lb in that array
1236
    """
1237
    lb_idx = np.searchsorted(f, lb, 'left')
2×
1238
    if ub == None:
2×
1239
        ub_idx = len(f)
2×
1240
    else:
1241
        ub_idx = np.searchsorted(f, ub, 'right')
2×
1242

1243
    return lb_idx, ub_idx
2×
1244

1245

1246
def unwrap_phases(a):
2×
1247
    """
1248
    Changes consecutive jumps larger than pi to their 2*pi complement.
1249
    """
1250
    pi = np.pi
2×
1251

1252
    diffs = np.diff(a)
2×
1253
    mod_diffs = np.mod(diffs + pi, 2 * pi) - pi
2×
1254
    neg_pi_idx = np.where(mod_diffs == -1 * np.pi)
2×
1255
    pos_idx = np.where(diffs > 0)
2×
1256
    this_idx = np.intersect1d(neg_pi_idx[0], pos_idx[0])
2×
1257
    mod_diffs[this_idx] = pi
2×
1258
    correction = mod_diffs - diffs
2×
1259
    correction[np.where(np.abs(diffs) < pi)] = 0
2×
1260
    a[1:] += np.cumsum(correction)
2×
1261

1262
    return a
2×
1263

1264

1265
def multi_intersect(input):
2×
1266
    """ A function for finding the intersection of several different arrays
1267

1268
    Parameters
1269
    ----------
1270
    input is a tuple of arrays, with all the different arrays
1271

1272
    Returns
1273
    -------
1274
    array - the intersection of the inputs
1275

1276
    Notes
1277
    -----
1278
    Simply runs intersect1d iteratively on the inputs
1279
    """
1280
    arr  = input[0].ravel()
2×
1281
    for this in input[1:]:
2×
1282
        arr = np.intersect1d(arr, this.ravel())
2×
1283

1284
    return arr
2×
1285

1286
def zero_pad(time_series, NFFT):
2×
1287
    """
1288
    Pad a time-series with zeros on either side, depending on its length
1289

1290
    Parameters
1291
    ----------
1292
    time_series : n-d array
1293
       Time-series data with time as the last dimension
1294

1295
    NFFT : int
1296
       The length to pad the data up to.
1297

1298
    """
1299

1300
    n_dims = len(time_series.shape)
2×
1301
    n_time_points = time_series.shape[-1]
2×
1302

1303
    if n_dims>1:
2×
1304
        n_channels = time_series.shape[:-1]
2×
1305
        shape_out = n_channels + (NFFT,)
2×
1306
    else:
1307
        shape_out = NFFT
2×
1308
    # zero pad if time_series is too short
1309
    if n_time_points < NFFT:
2×
1310
        tmp = time_series
2×
1311
        time_series = np.zeros(shape_out, time_series.dtype)
2×
1312
        time_series[..., :n_time_points] = tmp
2×
1313
        del tmp
2×
1314

1315
    return time_series
2×
1316

1317

1318
#-----------------------------------------------------------------------------
1319
# Numpy utilities - Note: these have been sent into numpy itself, so eventually
1320
# we'll be able to get rid of them here.
1321
#-----------------------------------------------------------------------------
1322
def fill_diagonal(a, val):
2×
1323
    """Fill the main diagonal of the given array of any dimensionality.
1324

1325
    For an array with ndim > 2, the diagonal is the list of locations with
1326
    indices a[i,i,...,i], all identical.
1327

1328
    This function modifies the input array in-place, it does not return a
1329
    value.
1330

1331
    This functionality can be obtained via diag_indices(), but internally this
1332
    version uses a much faster implementation that never constructs the indices
1333
    and uses simple slicing.
1334

1335
    Parameters
1336
    ----------
1337
    a : array, at least 2-dimensional.
1338
      Array whose diagonal is to be filled, it gets modified in-place.
1339

1340
    val : scalar
1341
      Value to be written on the diagonal, its type must be compatible with
1342
      that of the array a.
1343

1344
    Examples
1345
    --------
1346
    >>> a = np.zeros((3,3),int)
1347
    >>> fill_diagonal(a,5)
1348
    >>> a
1349
    array([[5, 0, 0],
1350
           [0, 5, 0],
1351
           [0, 0, 5]])
1352

1353
    The same function can operate on a 4-d array:
1354
    >>> a = np.zeros((3,3,3,3),int)
1355
    >>> fill_diagonal(a,4)
1356

1357
    We only show a few blocks for clarity:
1358
    >>> a[0,0]
1359
    array([[4, 0, 0],
1360
           [0, 0, 0],
1361
           [0, 0, 0]])
1362
    >>> a[1,1]
1363
    array([[0, 0, 0],
1364
           [0, 4, 0],
1365
           [0, 0, 0]])
1366
    >>> a[2,2]
1367
    array([[0, 0, 0],
1368
           [0, 0, 0],
1369
           [0, 0, 4]])
1370

1371
    See also
1372
    --------
1373
    - diag_indices: indices to access diagonals given shape information.
1374
    - diag_indices_from: indices to access diagonals given an array.
1375
    """
1376
    if a.ndim < 2:
!
1377
        raise ValueError("array must be at least 2-d")
!
1378
    if a.ndim == 2:
!
1379
        # Explicit, fast formula for the common case.  For 2-d arrays, we
1380
        # accept rectangular ones.
1381
        step = a.shape[1] + 1
!
1382
    else:
1383
        # For more than d=2, the strided formula is only valid for arrays with
1384
        # all dimensions equal, so we check first.
1385
        if not np.alltrue(np.diff(a.shape) == 0):
!
1386
            raise ValueError("All dimensions of input must be of equal length")
!
1387
        step = np.cumprod((1,) + a.shape[:-1]).sum()
!
1388

1389
    # Write the value out into the diagonal.
1390
    a.flat[::step] = val
!
1391

1392

1393
def diag_indices(n, ndim=2):
2×
1394
    """Return the indices to access the main diagonal of an array.
1395

1396
    This returns a tuple of indices that can be used to access the main
1397
    diagonal of an array with ndim (>=2) dimensions and shape (n,n,...,n).  For
1398
    ndim=2 this is the usual diagonal, for ndim>2 this is the set of indices
1399
    to access A[i,i,...,i] for i=[0..n-1].
1400

1401
    Parameters
1402
    ----------
1403
    n : int
1404
      The size, along each dimension, of the arrays for which the returned
1405
      indices can be used.
1406

1407
    ndim : int, optional
1408
      The number of dimensions
1409

1410
    Examples
1411
    --------
1412
    Create a set of indices to access the diagonal of a (4,4) array:
1413
    >>> di = diag_indices(4)
1414

1415
    >>> a = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
1416
    >>> a
1417
    array([[ 1,  2,  3,  4],
1418
           [ 5,  6,  7,  8],
1419
           [ 9, 10, 11, 12],
1420
           [13, 14, 15, 16]])
1421
    >>> a[di] = 100
1422
    >>> a
1423
    array([[100,   2,   3,   4],
1424
           [  5, 100,   7,   8],
1425
           [  9,  10, 100,  12],
1426
           [ 13,  14,  15, 100]])
1427

1428
    Now, we create indices to manipulate a 3-d array:
1429
    >>> d3 = diag_indices(2,3)
1430

1431
    And use it to set the diagonal of a zeros array to 1:
1432
    >>> a = np.zeros((2,2,2),int)
1433
    >>> a[d3] = 1
1434
    >>> a
1435
    array([[[1, 0],
1436
            [0, 0]],
1437
    <BLANKLINE>
1438
           [[0, 0],
1439
            [0, 1]]])
1440

1441
    See also
1442
    --------
1443
    - diag_indices_from: create the indices based on the shape of an existing
1444
    array.
1445
    """
1446
    idx = np.arange(n)
!
1447
    return (idx,) * ndim
!
1448

1449

1450
def diag_indices_from(arr):
2×
1451
    """Return the indices to access the main diagonal of an n-dimensional
1452
    array.
1453

1454
    See diag_indices() for full details.
1455

1456
    Parameters
1457
    ----------
1458
    arr : array, at least 2-d
1459
    """
1460
    if not arr.ndim >= 2:
!
1461
        raise ValueError("input array must be at least 2-d")
!
1462
    # For more than d=2, the strided formula is only valid for arrays with
1463
    # all dimensions equal, so we check first.
1464
    if not np.alltrue(np.diff(arr.shape) == 0):
!
1465
        raise ValueError("All dimensions of input must be of equal length")
!
1466

1467
    return diag_indices(arr.shape[0], arr.ndim)
!
1468

1469

1470
def mask_indices(n, mask_func, k=0):
2×
1471
    """Return the indices to access (n,n) arrays, given a masking function.
1472

1473
    Assume mask_func() is a function that, for a square array a of size (n,n)
1474
    with a possible offset argument k, when called as mask_func(a,k) returns a
1475
    new array with zeros in certain locations (functions like triu() or tril()
1476
    do precisely this).  Then this function returns the indices where the
1477
    non-zero values would be located.
1478

1479
    Parameters
1480
    ----------
1481
    n : int
1482
      The returned indices will be valid to access arrays of shape (n,n).
1483

1484
    mask_func : callable
1485
      A function whose api is similar to that of numpy.tri{u,l}.  That is,
1486
      mask_func(x,k) returns a boolean array, shaped like x.  k is an optional
1487
      argument to the function.
1488

1489
    k : scalar
1490
      An optional argument which is passed through to mask_func().  Functions
1491
      like tri{u,l} take a second argument that is interpreted as an offset.
1492

1493
    Returns
1494
    -------
1495
    indices : an n-tuple of index arrays.
1496
      The indices corresponding to the locations where mask_func(ones((n,n)),k)
1497
      is True.
1498

1499
    Examples
1500
    --------
1501
    These are the indices that would allow you to access the upper triangular
1502
    part of any 3x3 array:
1503
    >>> iu = mask_indices(3,np.triu)
1504

1505
    For example, if `a` is a 3x3 array:
1506
    >>> a = np.arange(9).reshape(3,3)
1507
    >>> a
1508
    array([[0, 1, 2],
1509
           [3, 4, 5],
1510
           [6, 7, 8]])
1511

1512
    Then:
1513
    >>> a[iu]
1514
    array([0, 1, 2, 4, 5, 8])
1515

1516
    An offset can be passed also to the masking function.  This gets us the
1517
    indices starting on the first diagonal right of the main one:
1518
    >>> iu1 = mask_indices(3,np.triu,1)
1519

1520
    with which we now extract only three elements:
1521
    >>> a[iu1]
1522
    array([1, 2, 5])
1523
    """
1524
    m = np.ones((n, n), int)
!
1525
    a = mask_func(m, k)
!
1526
    return np.where(a != 0)
!
1527

1528

1529
def tril_indices(n, k=0):
2×
1530
    """Return the indices for the lower-triangle of an (n,n) array.
1531

1532
    Parameters
1533
    ----------
1534
    n : int
1535
      Sets the size of the arrays for which the returned indices will be valid.
1536

1537
    k : int, optional
1538
      Diagonal offset (see tril() for details).
1539

1540
    Examples
1541
    --------
1542
    Commpute two different sets of indices to access 4x4 arrays, one for the
1543
    lower triangular part starting at the main diagonal, and one starting two
1544
    diagonals further right:
1545

1546
    >>> il1 = tril_indices(4)
1547
    >>> il2 = tril_indices(4,2)
1548

1549
    Here is how they can be used with a sample array:
1550
    >>> a = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
1551
    >>> a
1552
    array([[ 1,  2,  3,  4],
1553
           [ 5,  6,  7,  8],
1554
           [ 9, 10, 11, 12],
1555
           [13, 14, 15, 16]])
1556

1557
    Both for indexing:
1558
    >>> a[il1]
1559
    array([ 1,  5,  6,  9, 10, 11, 13, 14, 15, 16])
1560

1561
    And for assigning values:
1562
    >>> a[il1] = -1
1563
    >>> a
1564
    array([[-1,  2,  3,  4],
1565
           [-1, -1,  7,  8],
1566
           [-1, -1, -1, 12],
1567
           [-1, -1, -1, -1]])
1568

1569
    These cover almost the whole array (two diagonals right of the main one):
1570
    >>> a[il2] = -10
1571
    >>> a
1572
    array([[-10, -10, -10,   4],
1573
           [-10, -10, -10, -10],
1574
           [-10, -10, -10, -10],
1575
           [-10, -10, -10, -10]])
1576

1577
    See also
1578
    --------
1579
    - triu_indices : similar function, for upper-triangular.
1580
    - mask_indices : generic function accepting an arbitrary mask function.
1581
    """
1582
    return mask_indices(n, np.tril, k)
!
1583

1584

1585
def tril_indices_from(arr, k=0):
2×
1586
    """Return the indices for the lower-triangle of an (n,n) array.
1587

1588
    See tril_indices() for full details.
1589

1590
    Parameters
1591
    ----------
1592
    n : int
1593
      Sets the size of the arrays for which the returned indices will be valid.
1594

1595
    k : int, optional
1596
      Diagonal offset (see tril() for details).
1597

1598
    """
1599
    if not arr.ndim == 2 and arr.shape[0] == arr.shape[1]:
!
1600
        raise ValueError("input array must be 2-d and square")
!
1601
    return tril_indices(arr.shape[0], k)
!
1602

1603

1604
def triu_indices(n, k=0):
2×
1605
    """Return the indices for the upper-triangle of an (n,n) array.
1606

1607
    Parameters
1608
    ----------
1609
    n : int
1610
      Sets the size of the arrays for which the returned indices will be valid.
1611

1612
    k : int, optional
1613
      Diagonal offset (see triu() for details).
1614

1615
    Examples
1616
    --------
1617
    Commpute two different sets of indices to access 4x4 arrays, one for the
1618
    upper triangular part starting at the main diagonal, and one starting two
1619
    diagonals further right:
1620

1621
    >>> iu1 = triu_indices(4)
1622
    >>> iu2 = triu_indices(4,2)
1623

1624
    Here is how they can be used with a sample array:
1625
    >>> a = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
1626
    >>> a
1627
    array([[ 1,  2,  3,  4],
1628
           [ 5,  6,  7,  8],
1629
           [ 9, 10, 11, 12],
1630
           [13, 14, 15, 16]])
1631

1632
    Both for indexing:
1633
    >>> a[iu1]
1634
    array([ 1,  2,  3,  4,  6,  7,  8, 11, 12, 16])
1635

1636
    And for assigning values:
1637
    >>> a[iu1] = -1
1638
    >>> a
1639
    array([[-1, -1, -1, -1],
1640
           [ 5, -1, -1, -1],
1641
           [ 9, 10, -1, -1],
1642
           [13, 14, 15, -1]])
1643

1644
    These cover almost the whole array (two diagonals right of the main one):
1645
    >>> a[iu2] = -10
1646
    >>> a
1647
    array([[ -1,  -1, -10, -10],
1648
           [  5,  -1,  -1, -10],
1649
           [  9,  10,  -1,  -1],
1650
           [ 13,  14,  15,  -1]])
1651

1652
    See also
1653
    --------
1654
    - tril_indices : similar function, for lower-triangular.
1655
    - mask_indices : generic function accepting an arbitrary mask function.
1656
    """
1657
    return mask_indices(n, np.triu, k)
!
1658

1659

1660
def triu_indices_from(arr, k=0):
2×
1661
    """Return the indices for the lower-triangle of an (n,n) array.
1662

1663
    See triu_indices() for full details.
1664

1665
    Parameters
1666
    ----------
1667
    n : int
1668
      Sets the size of the arrays for which the returned indices will be valid.
1669

1670
    k : int, optional
1671
      Diagonal offset (see triu() for details).
1672

1673
    """
1674
    if not arr.ndim == 2 and arr.shape[0] == arr.shape[1]:
!
1675
        raise ValueError("input array must be 2-d and square")
!
1676
    return triu_indices(arr.shape[0], k)
!
1677

1678

1679
def structured_rand_arr(size, sample_func=np.random.random,
2×
1680
                        ltfac=None, utfac=None, fill_diag=None):
1681
    """Make a structured random 2-d array of shape (size,size).
1682

1683
    If no optional arguments are given, a symmetric array is returned.
1684

1685
    Parameters
1686
    ----------
1687
    size : int
1688
      Determines the shape of the output array: (size,size).
1689

1690
    sample_func : function, optional.
1691
      Must be a function which when called with a 2-tuple of ints, returns a
1692
      2-d array of that shape.  By default, np.random.random is used, but any
1693
      other sampling function can be used as long as matches this API.
1694

1695
    utfac : float, optional
1696
      Multiplicative factor for the upper triangular part of the matrix.
1697

1698
    ltfac : float, optional
1699
      Multiplicative factor for the lower triangular part of the matrix.
1700

1701
    fill_diag : float, optional
1702
      If given, use this value to fill in the diagonal.  Otherwise the diagonal
1703
      will contain random elements.
1704

1705
    Examples
1706
    --------
1707
    >>> np.random.seed(0)  # for doctesting
1708
    >>> np.set_printoptions(precision=4)  # for doctesting
1709
    >>> structured_rand_arr(4)
1710
    array([[ 0.5488,  0.7152,  0.6028,  0.5449],
1711
           [ 0.7152,  0.6459,  0.4376,  0.8918],
1712
           [ 0.6028,  0.4376,  0.7917,  0.5289],
1713
           [ 0.5449,  0.8918,  0.5289,  0.0871]])
1714
    >>> structured_rand_arr(4,ltfac=-10,utfac=10,fill_diag=0.5)
1715
    array([[ 0.5   ,  8.3262,  7.7816,  8.7001],
1716
           [-8.3262,  0.5   ,  4.6148,  7.8053],
1717
           [-7.7816, -4.6148,  0.5   ,  9.4467],
1718
           [-8.7001, -7.8053, -9.4467,  0.5   ]])
1719
    """
1720
    # Make a random array from the given sampling function
1721
    rmat = sample_func((size, size))
!
1722
    # And the empty one we'll then fill in to return
1723
    out = np.empty_like(rmat)
!
1724
    # Extract indices for upper-triangle, lower-triangle and diagonal
1725
    uidx = triu_indices(size, 1)
!
1726
    lidx = tril_indices(size, -1)
!
1727
    didx = diag_indices(size)
!
1728
    # Extract each part from the original and copy it to the output, possibly
1729
    # applying multiplicative factors.  We check the factors instead of
1730
    # defaulting to 1.0 to avoid unnecessary floating point multiplications
1731
    # which could be noticeable for very large sizes.
1732
    if utfac:
!
1733
        out[uidx] = utfac * rmat[uidx]
!
1734
    else:
1735
        out[uidx] = rmat[uidx]
!
1736
    if ltfac:
!
1737
        out[lidx] = ltfac * rmat.T[lidx]
!
1738
    else:
1739
        out[lidx] = rmat.T[lidx]
!
1740
    # If fill_diag was provided, use it; otherwise take the values in the
1741
    # diagonal from the original random array.
1742
    if fill_diag is not None:
!
1743
        out[didx] = fill_diag
!
1744
    else:
1745
        out[didx] = rmat[didx]
!
1746

1747
    return out
!
1748

1749

1750
def symm_rand_arr(size, sample_func=np.random.random, fill_diag=None):
2×
1751
    """Make a symmetric random 2-d array of shape (size,size).
1752

1753
    Parameters
1754
    ----------
1755
    n : int
1756
      Size of the output array.
1757

1758
    sample_func : function, optional.
1759
      Must be a function which when called with a 2-tuple of ints, returns a
1760
      2-d array of that shape.  By default, np.random.random is used, but any
1761
      other sampling function can be used as long as matches this API.
1762

1763
    fill_diag : float, optional
1764
      If given, use this value to fill in the diagonal.  Useful for
1765

1766
    Examples
1767
    --------
1768
    >>> np.random.seed(0)  # for doctesting
1769
    >>> np.set_printoptions(precision=4)  # for doctesting
1770
    >>> symm_rand_arr(4)
1771
    array([[ 0.5488,  0.7152,  0.6028,  0.5449],
1772
           [ 0.7152,  0.6459,  0.4376,  0.8918],
1773
           [ 0.6028,  0.4376,  0.7917,  0.5289],
1774
           [ 0.5449,  0.8918,  0.5289,  0.0871]])
1775
    >>> symm_rand_arr(4,fill_diag=4)
1776
    array([[ 4.    ,  0.8326,  0.7782,  0.87  ],
1777
           [ 0.8326,  4.    ,  0.4615,  0.7805],
1778
           [ 0.7782,  0.4615,  4.    ,  0.9447],
1779
           [ 0.87  ,  0.7805,  0.9447,  4.    ]])
1780
      """
1781
    return structured_rand_arr(size, sample_func, fill_diag=fill_diag)
!
1782

1783

1784
def antisymm_rand_arr(size, sample_func=np.random.random):
2×
1785
    """Make an anti-symmetric random 2-d array of shape (size,size).
1786

1787
    Parameters
1788
    ----------
1789

1790
    n : int
1791
      Size of the output array.
1792

1793
    sample_func : function, optional.
1794
      Must be a function which when called with a 2-tuple of ints, returns a
1795
      2-d array of that shape.  By default, np.random.random is used, but any
1796
      other sampling function can be used as long as matches this API.
1797

1798
    Examples
1799
    --------
1800
    >>> np.random.seed(0)  # for doctesting
1801
    >>> np.set_printoptions(precision=4)  # for doctesting
1802
    >>> antisymm_rand_arr(4)
1803
    array([[ 0.    ,  0.7152,  0.6028,  0.5449],
1804
           [-0.7152,  0.    ,  0.4376,  0.8918],
1805
           [-0.6028, -0.4376,  0.    ,  0.5289],
1806
           [-0.5449, -0.8918, -0.5289,  0.    ]])
1807
      """
1808
    return structured_rand_arr(size, sample_func, ltfac=-1.0, fill_diag=0)
!
1809

1810

1811
# --------brainx utils------------------------------------------------------
1812
# These utils were copied over from brainx - needed for viz
1813

1814

1815
def threshold_arr(cmat, threshold=0.0, threshold2=None):
2×
1816
    """Threshold values from the input array.
1817

1818
    Parameters
1819
    ----------
1820
    cmat : array
1821

1822
    threshold : float, optional.
1823
      First threshold.
1824

1825
    threshold2 : float, optional.
1826
      Second threshold.
1827

1828
    Returns
1829
    -------
1830
    indices, values: a tuple with ndim+1
1831

1832
    Examples
1833
    --------
1834
    >>> np.set_printoptions(precision=4)  # For doctesting
1835
    >>> a = np.linspace(0,0.2,5)
1836
    >>> a
1837
    array([ 0.  ,  0.05,  0.1 ,  0.15,  0.2 ])
1838
    >>> threshold_arr(a,0.1)
1839
    (array([3, 4]), array([ 0.15,  0.2 ]))
1840

1841
    With two thresholds:
1842
    >>> threshold_arr(a,0.1,0.2)
1843
    (array([0, 1]), array([ 0.  ,  0.05]))
1844
    """
1845
    # Select thresholds
1846
    if threshold2 is None:
!
1847
        th_low = -np.inf
!
1848
        th_hi = threshold
!
1849
    else:
1850
        th_low = threshold
!
1851
        th_hi = threshold2
!
1852

1853
    # Mask out the values we are actually going to use
1854
    idx = np.where((cmat < th_low) | (cmat > th_hi))
!
1855
    vals = cmat[idx]
!
1856

1857
    return idx + (vals,)
!
1858

1859

1860
def thresholded_arr(arr, threshold=0.0, threshold2=None, fill_val=np.nan):
2×
1861
    """Threshold values from the input matrix and return a new matrix.
1862

1863
    Parameters
1864
    ----------
1865
    arr : array
1866

1867
    threshold : float
1868
      First threshold.
1869

1870
    threshold2 : float, optional.
1871
      Second threshold.
1872

1873
    Returns
1874
    -------
1875
    An array shaped like the input, with the values outside the threshold
1876
    replaced with fill_val.
1877

1878
    Examples
1879
    --------
1880
    """
1881
    a2 = np.empty_like(arr)
!
1882
    a2.fill(fill_val)
!
1883
    mth = threshold_arr(arr, threshold, threshold2)
!
1884
    idx, vals = mth[:-1], mth[-1]
!
1885
    a2[idx] = vals
!
1886

1887
    return a2
!
1888

1889

1890
def rescale_arr(arr, amin, amax):
2×
1891
    """Rescale an array to a new range.
1892

1893
    Return a new array whose range of values is (amin,amax).
1894

1895
    Parameters
1896
    ----------
1897
    arr : array-like
1898

1899
    amin : float
1900
      new minimum value
1901

1902
    amax : float
1903
      new maximum value
1904

1905
    Examples
1906
    --------
1907
    >>> a = np.arange(5)
1908

1909
    >>> rescale_arr(a,3,6)
1910
    array([ 3.  ,  3.75,  4.5 ,  5.25,  6.  ])
1911
    """
1912

1913
    # old bounds
1914
    m = arr.min()
!
1915
    M = arr.max()
!
1916
    # scale/offset
1917
    s = float(amax - amin) / (M - m)
!
1918
    d = amin - s * m
!
1919

1920
    # Apply clip before returning to cut off possible overflows outside the
1921
    # intended range due to roundoff error, so that we can absolutely guarantee
1922
    # that on output, there are no values > amax or < amin.
1923
    return np.clip(s * arr + d, amin, amax)
!
1924

1925

1926
def minmax_norm(arr, mode='direct', folding_edges=None):
2×
1927
    """Minmax_norm an array to [0,1] range.
1928

1929
    By default, this simply rescales the input array to [0,1].  But it has a
1930
    special 'folding' mode that allows for the normalization of an array with
1931
    negative and positive values by mapping the negative values to their
1932
    flipped sign
1933

1934
    Parameters
1935
    ----------
1936
    arr : 1d array
1937

1938
    mode : string, one of ['direct','folding']
1939

1940
    folding_edges : (float,float)
1941
      Only needed for folding mode, ignored in 'direct' mode.
1942

1943
    Examples
1944
    --------
1945
    >>> np.set_printoptions(precision=4)  # for doctesting
1946
    >>> a = np.linspace(0.3,0.8,4)
1947
    >>> minmax_norm(a)
1948
    array([ 0.    ,  0.3333,  0.6667,  1.    ])
1949
    >>> b = np.concatenate([np.linspace(-0.7,-0.3,3),
1950
    ...                             np.linspace(0.3,0.8,3)])
1951
    >>> b
1952
    array([-0.7 , -0.5 , -0.3 ,  0.3 ,  0.55,  0.8 ])
1953
    >>> minmax_norm(b,'folding',[-0.3,0.3])
1954
    array([ 0.8,  0.4,  0. ,  0. ,  0.5,  1. ])
1955
    """
1956
    if mode == 'direct':
!
1957
        return rescale_arr(arr, 0, 1)
!
1958
    else:
1959
        fa, fb = folding_edges
!
1960
        amin, amax = arr.min(), arr.max()
!
1961
        ra, rb = float(fa - amin), float(amax - fb)  # in case inputs are ints
!
1962
        if ra < 0 or rb < 0:
!
1963
            raise ValueError("folding edges must be within array range")
!
1964
        greater = arr >= fb
!
1965
        upper_idx = greater.nonzero()
!
1966
        lower_idx = (~greater).nonzero()
!
1967
        # Two folding scenarios, we map the thresholds to zero but the upper
1968
        # ranges must retain comparability.
1969
        if ra > rb:
!
1970
            lower = 1.0 - rescale_arr(arr[lower_idx], 0, 1.0)
!
1971
            upper = rescale_arr(arr[upper_idx], 0, float(rb) / ra)
!
1972
        else:
1973
            upper = rescale_arr(arr[upper_idx], 0, 1)
!
1974
            # The lower range is trickier: we need to rescale it and then flip
1975
            # it, so the edge goes to 0.
1976
            resc_a = float(ra) / rb
!
1977
            lower = rescale_arr(arr[lower_idx], 0, resc_a)
!
1978
            lower = resc_a - lower
!
1979
        # Now, make output array
1980
        out = np.empty_like(arr)
!
1981
        out[lower_idx] = lower
!
1982
        out[upper_idx] = upper
!
1983
        return out
!
1984

1985

1986
#---------- intersect coords ----------------------------------------------
1987
def intersect_coords(coords1, coords2):
2×
1988
    """For two sets of coordinates, find the coordinates that are common to
1989
    both, where the dimensionality is the coords1.shape[0]"""
1990
    # Find the longer one
1991
    if coords1.shape[-1] > coords2.shape[-1]:
!
1992
        coords_long = coords1
!
1993
        coords_short = coords2
!
1994
    else:
1995
        coords_long = coords2
!
1996
        coords_short = coords1
!
1997

1998
    ans = np.array([[], [], []], dtype='int')  # Initialize as a 3 row variable
!
1999
    # Loop over the longer of the coordinate sets
2000
    for i in range(coords_long.shape[-1]):
!
2001
        # For each coordinate:
2002
        this_coords = coords_long[:, i]
!
2003
        # Find the matches in the other set of coordinates:
2004
        x = np.where(coords_short[0, :] == this_coords[0])[0]
!
2005
        y = np.where(coords_short[1, :] == this_coords[1])[0]
!
2006
        z = np.where(coords_short[2, :] == this_coords[2])[0]
!
2007

2008
        # Use intersect1d, such that there can be more than one match (and the
2009
        # size of idx will reflect how many such matches exist):
2010
        idx = np.intersect1d(np.intersect1d(x, y), z)
!
2011
        # Append the places where there are matches in all three dimensions:
2012
        if len(idx):
!
2013
            ans = np.hstack([ans, coords_short[:, idx]])
!
2014

2015
    return ans
!
2016

2017

2018
#---------- Time Series Stats ----------------------------------------
2019
def zscore(time_series, axis=-1):
2×
2020
    """Returns the z-score of each point of the time series
2021
    along a given axis of the array time_series.
2022

2023
    Parameters
2024
    ----------
2025
    time_series : ndarray
2026
        an array of time series
2027
    axis : int, optional
2028
        the axis of time_series along which to compute means and stdevs
2029

2030
    Returns
2031
    _______
2032
    zt : ndarray
2033
        the renormalized time series array
2034
    """
2035
    time_series = np.asarray(time_series)
2×
2036
    et = time_series.mean(axis=axis)
2×
2037
    st = time_series.std(axis=axis)
2×
2038
    sl = [slice(None)] * len(time_series.shape)
2×
2039
    sl[axis] = np.newaxis
2×
2040
    zt = time_series - et[sl]
2×
2041
    zt /= st[sl]
2×
2042
    return zt
2×
2043

2044

2045
def percent_change(ts, ax=-1):
2×
2046
    """Returns the % signal change of each point of the times series
2047
    along a given axis of the array time_series
2048

2049
    Parameters
2050
    ----------
2051

2052
    ts : ndarray
2053
        an array of time series
2054

2055
    ax : int, optional (default to -1)
2056
        the axis of time_series along which to compute means and stdevs
2057

2058
    Returns
2059
    -------
2060

2061
    ndarray
2062
        the renormalized time series array (in units of %)
2063

2064
    Examples
2065
    --------
2066

2067
    >>> ts = np.arange(4*5).reshape(4,5)
2068
    >>> ax = 0
2069
    >>> percent_change(ts,ax)
2070
    array([[-100.    ,  -88.2353,  -78.9474,  -71.4286,  -65.2174],
2071
           [ -33.3333,  -29.4118,  -26.3158,  -23.8095,  -21.7391],
2072
           [  33.3333,   29.4118,   26.3158,   23.8095,   21.7391],
2073
           [ 100.    ,   88.2353,   78.9474,   71.4286,   65.2174]])
2074
    >>> ax = 1
2075
    >>> percent_change(ts,ax)
2076
    array([[-100.    ,  -50.    ,    0.    ,   50.    ,  100.    ],
2077
           [ -28.5714,  -14.2857,    0.    ,   14.2857,   28.5714],
2078
           [ -16.6667,   -8.3333,    0.    ,    8.3333,   16.6667],
2079
           [ -11.7647,   -5.8824,    0.    ,    5.8824,   11.7647]])
2080
"""
2081
    ts = np.asarray(ts)
2×
2082

2083
    return (ts / np.expand_dims(np.mean(ts, ax), ax) - 1) * 100
2×
2084

2085

2086
#----------Event-related analysis utils ----------------------------------
2087
def fir_design_matrix(events, len_hrf):
2×
2088
    """Create a FIR event matrix from a time-series of events.
2089

2090
    Parameters
2091
    ----------
2092

2093
    events : 1-d int array
2094
       Integers denoting different kinds of events, occurring at the time
2095
       corresponding to the bin represented by each slot in the array. In
2096
       time-bins in which no event occurred, a 0 should be entered. If negative
2097
       event values are entered, they will be used as "negative" events, as in
2098
       events that should be contrasted with the postitive events (typically -1
2099
       and 1 can be used for a simple contrast of two conditions)
2100

2101
    len_hrf : int
2102
       The expected length of the HRF (in the same time-units as the events are
2103
       represented (presumably TR). The size of the block dedicated in the
2104
       fir_matrix to each type of event
2105

2106
    Returns
2107
    -------
2108

2109
    fir_matrix : matrix
2110

2111
       The design matrix for FIR estimation
2112
    """
2113
    event_types = np.unique(events)[np.unique(events) != 0]
2×
2114
    fir_matrix = np.zeros((events.shape[0], len_hrf * event_types.shape[0]))
2×
2115

2116
    for t in event_types:
2×
2117
        idx_h_a = (np.array(np.where(event_types == t)[0]) * len_hrf)[0]
2×
2118
        idx_h_b = idx_h_a + len_hrf
2×
2119
        idx_v = np.where(events == t)[0]
2×
2120
        for idx_v_a in idx_v:
2×
2121
            idx_v_b = idx_v_a + len_hrf
2×
2122
            fir_matrix[idx_v_a:idx_v_b, idx_h_a:idx_h_b] += (np.eye(len_hrf) *
2×
2123
                                                             np.sign(t))
2124

2125
    return fir_matrix
2×
2126

2127

2128
#---------- MAR utilities ----------------------------------------
2129

2130
# These utilities are used in the computation of multivariate autoregressive
2131
# models (used in computing Granger causality):
2132

2133
def crosscov_vector(x, y, nlags=None):
2×
2134
    r"""
2135
    This method computes the following function
2136

2137
    .. math::
2138

2139
        R_{xy}(k) = E{ x(t)y^{*}(t-k) } = E{ x(t+k)y^{*}(t) }
2140
        k \in {0, 1, ..., nlags-1}
2141

2142
    (* := conjugate transpose)
2143

2144
    Note: This is related to the other commonly used definition
2145
    for vector crosscovariance
2146

2147
    .. math::
2148

2149
        R_{xy}^{(2)}(k) = E{ x(t-k)y^{*}(t) } = R_{xy}^(-k) = R_{yx}^{*}(k)
2150

2151
    Parameters
2152
    ----------
2153

2154
    x, y : ndarray (nc, N)
2155

2156
    nlags : int, optional
2157
       compute lags for k in {0, ..., nlags-1}
2158

2159
    Returns
2160
    -------
2161

2162
    rxy : ndarray (nc, nc, nlags)
2163

2164
    """
2165
    N = x.shape[1]
2×
2166
    if nlags is None:
2×
2167
        nlags = N
!
2168
    nc = x.shape[0]
2×
2169

2170
    rxy = np.empty((nc, nc, nlags))
2×
2171

2172
    # rxy(k) = E{ x(t)y*(t-k) } ( * = conj transpose )
2173
    # Take the expectation over an outer-product
2174
    # between x(t) and conj{y(t-k)} for each t
2175

2176
    for k in range(nlags):
2×
2177
        # rxy(k) = E{ x(t)y*(t-k) }
2178
        prod = x[:, None, k:] * y[None, :, :N - k].conj()
2×
2179
##         # rxy(k) = E{ x(t)y*(t+k) }
2180
##         prod = x[:,None,:N-k] * y[None,:,k:].conj()
2181
        # Do a sample mean of N-k pts? or sum and divide by N?
2182
        rxy[..., k] = prod.mean(axis=-1)
2×
2183
    return rxy
2×
2184

2185

2186
def autocov_vector(x, nlags=None):
2×
2187
    r"""
2188
    This method computes the following function
2189

2190
    .. math::
2191

2192
    R_{xx}(k) = E{ x(t)x^{*}(t-k) } = E{ x(t+k)x^{*}(t) }
2193
    k \in {0, 1, ..., nlags-1}
2194

2195
    (* := conjugate transpose)
2196

2197
    Note: this is related to
2198
    the other commonly used definition for vector autocovariance
2199

2200
    .. math::
2201

2202
    R_{xx}^{(2)}(k) = E{ x(t-k)x^{*}(t) } = R_{xx}(-k) = R_{xx}^{*}(k)
2203

2204
    Parameters
2205
    ----------
2206

2207
    x : ndarray (nc, N)
2208

2209
    nlags : int, optional
2210
       compute lags for k in {0, ..., nlags-1}
2211

2212
    Returns
2213
    -------
2214

2215
    rxx : ndarray (nc, nc, nlags)
2216

2217
    """
2218
    return crosscov_vector(x, x, nlags=nlags)
2×
2219

2220

2221
def generate_mar(a, cov, N):
2×
2222
    """
2223
    Generates a multivariate autoregressive dataset given the formula:
2224

2225
    X(t) + sum_{i=1}^{P} a(i)X(t-i) = E(t)
2226

2227
    Where E(t) is a vector of samples from possibly covarying noise processes.
2228

2229
    Parameters
2230
    ----------
2231

2232
    a : ndarray (n_order, n_c, n_c)
2233
       An order n_order set of coefficient matrices, each shaped (n_c, n_c) for
2234
       n_channel data
2235
    cov : ndarray (n_c, n_c)
2236
       The innovations process covariance
2237
    N : int
2238
       how many samples to generate
2239

2240
    Returns
2241
    -------
2242

2243
    mar, nz
2244

2245
    mar and noise process shaped (n_c, N)
2246
    """
2247
    n_c = cov.shape[0]
2×
2248
    n_order = a.shape[0]
2×
2249

2250
    nz = np.random.multivariate_normal(
2×
2251
        np.zeros(n_c), cov, size=(N,)
2252
        )
2253

2254
    # nz is a (N x n_seq) array
2255

2256
    mar = nz.copy()  # np.zeros((N, n_seq), 'd')
2×
2257

2258
    # this looks like a redundant loop that can be rolled into a matrix-matrix
2259
    # multiplication at each coef matrix a(i)
2260

2261
    # this rearranges the equation to read:
2262
    # X(i) = E(i) - sum_{j=1}^{P} a(j)X(i-j)
2263
    # where X(n) n < 0 is taken to be 0
2264
    # In terms of the code: X is mar and E is nz, P is n_order
2265
    for i in range(N):
2×
2266
        for j in range(min(i, n_order)):  # j logically in set {1, 2, ..., P}
2×
2267
            mar[i, :] -= np.dot(a[j], mar[i - j - 1, :])
2×
2268

2269
    return mar.transpose(), nz.transpose()
2×
2270

2271

2272
#----------goodness of fit utilities ----------------------------------------
2273

2274
def akaike_information_criterion(ecov, p, m, Ntotal, corrected=False):
2×
2275

2276
    """
2277

2278
    A measure of the goodness of fit of an auto-regressive model based on the
2279
    model order and the error covariance.
2280

2281
    Parameters
2282
    ----------
2283

2284
    ecov : float array
2285
        The error covariance of the system
2286
    p
2287
        the number of channels
2288
    m : int
2289
        the model order
2290
    Ntotal
2291
        the number of total time-points (across channels)
2292
    corrected : boolean (optional)
2293
        Whether to correct for small sample size
2294

2295
    Returns
2296
    -------
2297

2298
    AIC : float
2299
        The value of the AIC
2300

2301

2302
    Notes
2303
    -----
2304
    This is an implementation of equation (50) in Ding et al. (2006):
2305

2306
    M Ding and Y Chen and S Bressler (2006) Granger Causality: Basic Theory and
2307
    Application to Neuroscience. http://arxiv.org/abs/q-bio/0608035v1
2308

2309

2310
    Correction for small sample size is taken from:
2311
    http://en.wikipedia.org/wiki/Akaike_information_criterion.
2312

2313
    """
2314

2315
    AIC = (2 * (np.log(linalg.det(ecov))) +
2×
2316
           ((2 * (p ** 2) * m) / (Ntotal)))
2317

2318
    if corrected:
2×
2319
        return AIC + (2 * m * (m + 1)) / (Ntotal - m - 1)
2×
2320
    else:
2321
        return AIC
2×
2322

2323

2324
def bayesian_information_criterion(ecov, p, m, Ntotal):
2×
2325
    r"""The Bayesian Information Criterion, also known as the Schwarz criterion
2326
     is a measure of goodness of fit of a statistical model, based on the
2327
     number of model parameters and the likelihood of the model
2328

2329
    Parameters
2330
    ----------
2331
    ecov : float array
2332
        The error covariance of the system
2333

2334
    p : int
2335
        the system size (how many variables).
2336

2337
    m : int
2338
        the model order.
2339

2340
    corrected : boolean (optional)
2341
        Whether to correct for small sample size
2342

2343

2344
    Returns
2345
    -------
2346

2347
    BIC : float
2348
        The value of the BIC
2349
    a
2350
        the resulting autocovariance vector
2351

2352
    Notes
2353
    -----
2354
    This is an implementation of equation (51) in Ding et al. (2006):
2355

2356
    .. math ::
2357

2358
    BIC(m) = 2 log(|\Sigma|) + \frac{2p^2 m log(N_{total})}{N_{total}},
2359

2360
    where $\Sigma$ is the noise covariance matrix. In auto-regressive model
2361
    estimation, this matrix will contain in $\Sigma_{i,j}$ the residual
2362
    variance in estimating time-series $i$ from $j$, $p$ is the dimensionality
2363
    of the data, $m$ is the number of parameters in the model and $N_{total}$
2364
    is the number of time-points.
2365

2366
    M Ding and Y Chen and S Bressler (2006) Granger Causality: Basic Theory and
2367
    Application to Neuroscience. http://arxiv.org/abs/q-bio/0608035v1
2368

2369

2370
    See http://en.wikipedia.org/wiki/Schwarz_criterion
2371

2372
    """
2373

2374
    BIC = (2 * (np.log(linalg.det(ecov))) +
2×
2375
            ((2 * (p ** 2) * m * np.log(Ntotal)) / (Ntotal)))
2376

2377
    return BIC
2×
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc