```python
"""
Tutorial: A reference implementation of orbital optimized second-order Moller-Plesset perturbation theory.
"""

__authors__    = "Boyi Zhang"
__credits__   = ["Boyi Zhang", "Justin M. Turney"]

__copyright_amp__ = "(c) 2014-2018, The Psi4NumPy Developers"
__license__   = "BSD-3-Clause"
__date__      = "2017-08-08"
```

# Orbital-Optimized Second-Order Moller Plesset Perturbation Theory (OMP2)

In this tutorial, we will implement the configuration interaction singles method in the spin orbital notation. The groundwork for working in the spin orbital notation has been laid out in "Introduction to the Spin Orbital Formulation of Post-HF methods" [tutorial](../08_CEPA0_and_CCD/8a_Intro_to_spin_orbital_postHF.ipynb). It is highly recommended to work through that introduction before starting this tutorial.

## I. Theoretical Overview

### The general orbital optimization procedure

In orbital optimization methods, the energy is minimized with respect to(w.r.t) an orbital rotation parameter $\textbf{X}$ and can be expanded to second-order as:

\begin{equation}
E(\textbf{X}) = E(\textbf{X}) + \textbf{X}^\dagger \textbf{w} + \frac{1}{2}\textbf{X}^\dagger\textbf{A}\textbf{X}
\end{equation}

Here, $\textbf{w}$ is the orbital gradient (derivative of E w.r.t. $\textbf{X}^\dagger$ evaluated at zero and $\textbf{A}$ is the orbital Hessian matrix (second derivative of E w.r.t. $\textbf{X}^\dagger\textbf{X}$ evaluated at zero).

It can be shown that $\textbf{X} = -\textbf{A}^{-1}\textbf{w}$, which gives us the equation used in the Newton-Raphson step of the orbital optimization.

We define the unitary rotation matrix to be $\textbf{U} = exp(\textbf{X}-\textbf{X}^\dagger)$ and use this to rotate the orbitals (using the cofficient matrix).

We then transform the 1 and 2-electron integrals using the new cofficient matrix and evaluate the energy.

This process is repeated until the energy convergence satisfies a specified convergence parameter.

A detailed algorithm for OMP2 is provided in the implementation section.

### A note on the MP2 amplitude equation

The MP2 amplitude equation can be explicitly written as

\begin{equation}
 t_{ab}^{ij} = (\mathcal{E}_{ab}^{ij})^{-1} \left(
     \bar{g}_{ab}^{ij} + P_{(a/b)}f'{}_{a}^{c} t_{cb}^{ij} -
     P^{(i/j)}f'{}_k^it_{ab}^{kj} \right)
\end{equation}

where f' is the off-digonal Fock matrix.

Indices p, q, r... are used to indicate arbitrary orbitals, indices a, b, c... are used to indicate virtual orbitals, and indices i, j, k... are used to indicate occupied orbitals.

In conventional MP2, the use canonical orbitals result in a diagonal Fock matrix and the last two terms of the t amplitude equation goes to zero. In OMP2, however, the orbitals are no longer canonical due to orbital rotation, and we have to include these terms in the equation.

## II. Implementation

As with previous tutorials, let's begin by importing Psi4 and NumPy and setting memory and output file options.
Note that we will also be importing SciPy, which is another library that builds on NumPy and has additional capabilities that we will use.

```python
# ==> Import Psi4, NumPy, & SciPy <==
import psi4
import numpy as np
import scipy.linalg as la

# ==> Set Basic Psi4 Options <==

# Memory specifications
psi4.set_memory(int(2e9))
numpy_memory = 2

# Output options
psi4.core.set_output_file('output.dat', False)
```

We now define the molecule and set Psi4 options:

```python
mol = psi4.geometry("""
0 1
O
H 1 1.1
H 1 1.1 2 104
symmetry c1
""")

psi4.set_options({'basis':        '6-31g',
                  'scf_type':     'pk',
                  'reference':    'rhf',
                  'mp2_type':     'conv',
                  'e_convergence': 1e-8,
                  'd_convergence': 1e-8})

```

We use Psi4 to compute the RHF energy and wavefunction and store them in variables `scf_e` and `scf_wfn`. We also check the memory requirements for computation:

```python
# Get the SCF wavefunction & energies
scf_e, scf_wfn = psi4.energy('scf', return_wfn=True)

# ==> Nuclear Repulsion Energy <==
E_nuc = mol.nuclear_repulsion_energy()

# Check memory requirements
nmo = scf_wfn.nmo()
I_size = (nmo**4) * 8e-9
print('\nSize of the ERI tensor will be %4.2f GB.\n' % I_size)
memory_footprint = I_size * 1.5
if I_size > numpy_memory:
    psi4.core.clean()
    raise Exception("Estimated memory utilization (%4.2f GB) exceeds allotted \
                     memory limit of %4.2f GB." % (memory_footprint, numpy_memory))
```

    Size of the ERI tensor will be 0.00 GB.

We need to set the maximum number of iterations for the OMP2 code as well as the energy convergence criteria:

```python
# ==> Set default program options <==
# Maximum OMP2 iterations
MAXITER = 40
# Energy convergence criterion
E_conv = 1.0e-8
```

We first obtain orbital information from our wavefunction. We also create an instance of MintsHelper to help build our molecular integrals:

```python
# Create instance of MintsHelper class
mints = psi4.core.MintsHelper(scf_wfn.basisset())

# Get basis and orbital information
nbf = mints.nbf()          # Number of basis functions
nalpha = scf_wfn.nalpha()  # Number of alpha electrons
nbeta = scf_wfn.nbeta()    # Number of beta electrons
nocc = nalpha + nbeta      # Total number of electrons
nso = 2 * nbf              # Total number of spin orbitals
nvirt = nso - nocc         # Number of virtual orbitals
```

We now build our 2-electron integral, a 4D tensor, in the spin orbital formulation. We also convert it into physicist's notation and antisymmetrize for easier manipulation of the tensor later on.

```python
def spin_block_tei(I):
    '''
    Spin blocks 2-electron integrals
    Using np.kron, we project I and I tranpose into the space of the 2x2 ide
    The result is our 2-electron integral tensor in spin orbital notation
    '''
    identity = np.eye(2)
    I = np.kron(identity, I)
    return np.kron(identity, I.T)

I = np.asarray(mints.ao_eri())
I_spinblock = spin_block_tei(I)

# Convert chemist's notation to physicist's notation, and antisymmetrize
# (pq | rs) ---> <pr | qs>
# <pr||qs> = <pr | qs> - <pr | sq>
gao = I_spinblock.transpose(0, 2, 1, 3) - I_spinblock.transpose(0, 2, 3, 1)

```

We get the core Hamiltonian from the reference wavefunction and build it in the spin orbital formulation. The NumPy function `np.kron` is used to project the core Hamiltonian into the space of a 2x2 identity matrix. Note that `np.kron` was also used for spin-blocking the 2-electron integral. In the current case, np.kron is only called once because the core Hamltonian is a 2D matrix.

```python
# ==> core Hamiltoniam <==

h = np.asarray(scf_wfn.H())

# Using np.kron, we project h into the space of the 2x2 identity
# The result is the core Hamiltonian in the spin orbital formulation
hao = np.kron(np.eye(2), h)
```

We get the orbital energies from alpha and beta electrons and append them together. We spin-block the coefficients obtained from the reference wavefunction and convert them into NumPy arrays. There is a set corresponding to coefficients from alpha electrons and a set of coefficients from beta electrons. We then sort them according to the order of the orbital energies using argsort():

```python
# Get orbital energies, cast into NumPy array, and extend eigenvalues
eps_a = np.asarray(scf_wfn.epsilon_a())
eps_b = np.asarray(scf_wfn.epsilon_b())
eps = np.append(eps_a, eps_b)

# Get coefficients, block, and sort
Ca = np.asarray(scf_wfn.Ca())
Cb = np.asarray(scf_wfn.Cb())
C = np.block([
             [      Ca,         np.zeros_like(Cb)],
             [np.zeros_like(Ca),          Cb     ]])

# Sort the columns of C according to the order of orbital energies
C = C[:, eps.argsort()]

```

We now define two functions that will transform the core Hamiltonian and the 2-electron integral from the AO basis into the MO basis using the coefficients:

\begin{align}
h_p^q &= \sum_{\mu \nu} C_{\mu p}^* h_{\mu \nu} C_{\nu q} \\
\bar{g}_{pq}^{rs} &= \sum_{\mu \nu \rho \sigma}
                C_{\mu p}^* C_{\nu q}^* \langle \mu \nu || \rho \sigma \rangle C_{\rho r}C_{\sigma s}
\end{align}

Note that we transform the core Hamiltonian twice because it has two dimensions. We use these functions to transform the `hao` and `gao` previously defined:

```python
# ==> AO to MO transformation functions <==

def ao_to_mo(hao, C):
    '''
    Transform hao, which is the core Hamiltonian in the spin orbital basis,
    into the MO basis using MO coefficients
    '''

    return np.einsum('pQ, pP -> PQ',
           np.einsum('pq, qQ -> pQ', hao, C), C)

def ao_to_mo_tei(gao, C):
    '''
    Transform gao, which is the spin-blocked 4d array of physicist's notation,
    antisymmetric two-electron integrals, into the MO basis using MO coefficients
    '''

    return np.einsum('pQRS, pP -> PQRS',
           np.einsum('pqRS, qQ -> pQRS',
           np.einsum('pqrS, rR -> pqRS',
           np.einsum('pqrs, sS -> pqrS', gao, C), C), C), C)

# Transform gao and hao into MO basis
hmo = ao_to_mo(hao, C)
gmo = ao_to_mo_tei(gao, C)

```

Here we define slices corresponding to the number and position of occupied and virtual indices. We will use these later in the code to access occupied and virtual blocks of relevant arrays. For example, to get $\bar{g}_{ab}^{ij}$, we call:
~~~python
gmo[v, v, o, o]
~~~

```python
# Make slices
o = slice(None, nocc)
v = slice(nocc, None)
x = np.newaxis
```

**OMP2 iteration algorithm:**

1. Build the fock matrix

   \begin{equation}
   f_p^q = h_p^q +\bar{g}_{pi}^{qi}
   \end{equation}

2. Build the off-diagonal Fock matrix and the orbital energies, where off-diagonal Fock matrix(`fprime`) is just the Fock matrix with its diagonal elements set to zero, and the orbital energies (`eps`) are just the diagonal elements of the Fock matrix

    \begin{equation}
    \epsilon_p = f_p^p
    \end{equation}

    \begin{equation}
    f'{}_p^q =(1 - \delta_p^q)f_p^q
    \end{equation}

3. Update the amplitudes (`t_amp`)

    \begin{equation}
     t_{ab}^{ij} = (\mathcal{E}_{ab}^{ij})^{-1} \left(
     \bar{g}_{ab}^{ij} + P_{(a/b)}f'{}_{a}^{c} t_{cb}^{ij} -
     P^{(i/j)}f'{}_k^it_{ab}^{kj} \right)
    \end{equation}

   Here, P is a permutation operator that permutes the indices indicated. For example, $P_{(a/b)}$ would give all    possible permutations of a and b. Thus,

    \begin{equation}
    P_{(a/b)}f'{}_{a}^{c} t_{cb}^{ij} = f'{}_{a}^{c} t_{cb}^{ij} - f'{}_{b}^{c} t_{ca}^{ij}
    \end{equation}

   where the minus sign arises as a result of antisymmetric properties due to the interchange of the two indices
   The amplitudes terms in the code are assigned as `t1`, `t2`, and `t3`, respectively.

   To take in account the permutation terms, we evaluate the term and then transpose the relevant indices.
   For example, for the second term in the amplitude equation we first evaluate it as it:
   ~~~python
   t2 = np.einsum('ac,cbij -> abij', fprime[v, v], t_amp)
   ~~~
   Then, to account for the permutation, we transpose the two dimensions corresponding to the permuted indices. Since    a and b are in the first two dimensions of `t2`, we switch 0 and 1:
   ~~~python
   t2 = t2 - t2.transpose((1, 0, 2, 3))
   ~~~
4. Build the one-particle density matrix (`opdm`)

    \begin{equation}
    \gamma_q^p = \tilde{\gamma}_q^p + \mathring{\gamma}_q^p
    \end{equation}

   The one-particle density matrix(opdm) is a sum of the reference opdm ($\mathring{\gamma}_q^p$) and a correlation opdm ($\tilde{\gamma}_q^p$).

    $\mathring{\gamma}_q^p$ is assigned as the variable `odm_ref` and defined as:
     \begin{align}
     & \, \delta^i_j \, \text{for $p=i$, $q=j$}, \\
     & 0 \,  \text{otherwise}
    \end{align}

    The virtual block of $\tilde{\gamma}_q^p$ (assigned as `odm_corr`) is defined as:
    \begin{equation}
    \tilde{\gamma}_b^a  = \frac{1}{2} t_{ij}^{ab*}t_{bc}^{ij}
    \end{equation}

     The occupied block of $\tilde{\gamma}_q^p$ is defined as:
    \begin{equation}
    \tilde{\gamma}_j^i  = -\frac{1}{2} t_{jk}^{ab*}t_{ab}^{ik}
    \end{equation}

    As seen before, we used our defined slices to pick out these specific blocks:
    ~~~python
     opdm_corr[v, v] = (1/2)*np.einsum('ijac,bcij -> ba', t_amp.T, t_amp)
     opdm_corr[o, o] = -(1/2)*np.einsum('jkab,abik -> ji', t_amp.T, t_amp)
    ~~~

5. Build the two-particle density matrix (`tpdm`)

    \begin{equation}
    \Gamma_{rs}^{pq} = \tilde{\Gamma}_{rs}^{pq} + P_{(r/s)}^{(p/q)}\tilde{\gamma}_r^p\mathring{\gamma}_s^q
    +P_{(r/s)}\mathring{\gamma}_r^p\mathring{\gamma}_s^q
    \end{equation}

     where as before, P is the permutation operator

 $\tilde{\Gamma}_{rs}^{pq}$ (`tdm_corr`) can be separated into two components:

 \begin{align}
 \tilde{\Gamma}_{ij}^{ab} = & t_{ij}^{ab*}\\
 \tilde{\Gamma}_{ab}^{ij} = & t_{ab}^{ij}
 \end{align}

6. Compute the Newton-Raphson step

   First, form a generalized-Fock matrix using the one and two particle density matrices. This will be used to form the MO gradient matrix needed for the rotation matrix:

   \begin{equation}
   (\textbf{F})_p^q \equiv h_p^r \gamma_r^q + \frac{1}{2} \bar{g}_{pr}^{st}\Gamma_{st}^{qr}
   \end{equation}

   We have seen in the theoretical overview that the X matrix while paramtetrizes the orbital rotations can be expressed in terms of the orbital gradient matrix and orbital Hessian matrix. It can be shown that the individual elements of X can be computed by:

   \begin{equation}
    x_a^i = \frac{(\textbf{F} - \textbf{F}^\dagger)_a^i}{\epsilon_i - \epsilon_a}
    \end{equation}

     Here we only consider rotations between the occupied and virtual orbitals, since rotations within each block are redudant since energy is invariant to rotations within those spaces.

     Rather than computing individual elements we can compute the whole virtual-occupied block:

    \begin{equation}
    \textbf{X}_v^o = (\textbf{F} - \textbf{F}^\dagger)_v^o (\mathcal{E}_v^o)^{-1}
    \end{equation}
    Translating this to code, this becomes:
    ~~~python
    X[v, o] = ((F - F.T)[v, o])/(-eps[v, x] + eps[x, o])
    ~~~
7. We can now build the Newton-Raphson orbital rotation matrix from $\textbf{X}$:

    \begin{equation}
    \textbf{U} = exp(\textbf{X} - \textbf{X}^\dagger)
    \end{equation}

8. Use the rotation matrix to rotate the MO coefficients
   \begin{equation}
   \textbf{C} \leftarrow \textbf{CU}
   \end{equation}

9. Transform the 1-electron (`hmo`) and 2-electron (`gmo`) integrals to the MO basis using the new coefficient matrix. We can use our previously defined transformation functions for this step.

    \begin{align}
    h_p^q &= \sum_{\mu \nu} C_{\mu p}^* h_{\mu \nu} C_{\nu q} \\
    \bar{g}_{pq}^{rs} &= \sum_{\mu \nu \rho \sigma}
    C_{\mu p}^* C_{\nu q}^* \langle \mu \nu || \rho \sigma \rangle C_{\rho r}C_{\sigma s}
    \end{align}
10. Evaluate the energy (`E_OMP2`)
    \begin{equation}
    E = h_p^q \gamma_q^p + \frac{1}{4} \bar{g}_{pq}^{rs}\Gamma_{rs}^{pq}
    \end{equation}

11. If the energy is converged according to the convergence criterion defined above, quit. Otherwise, loop over the algorithm again.

Before beginning the iterations, we initialize OMP2 energy and the t amplitudes $t_{ab}^{ij}$ (`t_amp`) to be zero. We also initialize the correlation and reference one-particle density matrix and the correlation two-particle density matrix. Finally we intialize `X`, which is the parameter used to optimize our orbitals in the Newton-Raphson step.

```python
# Intialize t amplitude and energy
t_amp = np.zeros((nvirt, nvirt, nocc, nocc))
E_OMP2_old = 0.0

# Initialize the correlation one particle density matrix
opdm_corr = np.zeros((nso, nso))

# Build the reference one particle density matrix
opdm_ref = np.zeros((nso, nso))
opdm_ref[o, o] = np.identity(nocc)

# Initialize two particle density matrix
tpdm_corr = np.zeros((nso, nso, nso, nso))

# Initialize the rotation matrix parameter
X = np.zeros((nso, nso))

for iteration in range(MAXITER):

    # Build the Fock matrix
    f = hmo + np.einsum('piqi -> pq', gmo[:, o, :, o])

    # Build off-diagonal Fock Matrix and orbital energies
    fprime = f.copy()
    np.fill_diagonal(fprime, 0)
    eps = f.diagonal()

    # Update t amplitudes
    t1 = gmo[v, v, o, o]
    t2 = np.einsum('ac,cbij -> abij', fprime[v, v], t_amp)
    t3 = np.einsum('ki,abkj -> abij', fprime[o, o], t_amp)
    t_amp = t1 + t2 - t2.transpose((1, 0, 2, 3)) \
            - t3 + t3.transpose((0, 1, 3, 2))

    # Divide by a 4D tensor of orbital energies
    t_amp /= (- eps[v, x, x, x] - eps[x, v, x, x] +
              eps[x, x, o, x] + eps[x, x, x, o])

    # Build one particle density matrix
    opdm_corr[v, v] = (1/2)*np.einsum('ijac,bcij -> ba', t_amp.T, t_amp)
    opdm_corr[o, o] = -(1/2)*np.einsum('jkab,abik -> ji', t_amp.T, t_amp)
    opdm = opdm_corr + opdm_ref

    # Build two particle density matrix
    tpdm_corr[v, v, o, o] = t_amp
    tpdm_corr[o, o, v, v] = t_amp.T
    tpdm2 = np.einsum('rp,sq -> rspq', opdm_corr, opdm_ref)
    tpdm3 = np.einsum('rp,sq->rspq', opdm_ref, opdm_ref)
    tpdm = tpdm_corr \
        + tpdm2 - tpdm2.transpose((1, 0, 2, 3)) \
        - tpdm2.transpose((0, 1, 3, 2)) + tpdm2.transpose((1, 0, 3, 2)) \
        + tpdm3 - tpdm3.transpose((1, 0, 2, 3))

    # Newton-Raphson step
    F = np.einsum('pr,rq->pq', hmo, opdm) + (1/2) * np.einsum('prst,stqr -> pq', gmo, tpdm)
    X[v, o] = ((F - F.T)[v, o])/(- eps[v, x] + eps[x, o])

    # Build Newton-Raphson orbital rotation matrix
    U = la.expm(X - X.T)

    # Rotate spin-orbital coefficients
    C = C.dot(U)

    # Transform one and two electron integrals using new C
    hmo = ao_to_mo(hao, C)
    gmo = ao_to_mo_tei(gao, C)

    # Compute the energy
    E_OMP2 = E_nuc + np.einsum('pq,qp ->', hmo, opdm) + \
             (1/4)*np.einsum('pqrs,rspq ->', gmo, tpdm)
    print('OMP2 iteration: %3d Energy: %15.8f dE: %2.5E' % (iteration, E_OMP2, (E_OMP2-E_OMP2_old)))

    if (abs(E_OMP2-E_OMP2_old)) < E_conv:
        break

    # Updating values
    E_OMP2_old = E_OMP2

```

    OMP2 iteration:   0 Energy:    -76.09603493 dE: -7.60960E+01
    OMP2 iteration:   1 Energy:    -76.09617336 dE: -1.38424E-04
    OMP2 iteration:   2 Energy:    -76.09618834 dE: -1.49844E-05
    OMP2 iteration:   3 Energy:    -76.09619159 dE: -3.24371E-06
    OMP2 iteration:   4 Energy:    -76.09619246 dE: -8.72488E-07
    OMP2 iteration:   5 Energy:    -76.09619281 dE: -3.48876E-07
    OMP2 iteration:   6 Energy:    -76.09619294 dE: -1.38242E-07
    OMP2 iteration:   7 Energy:    -76.09619301 dE: -6.80799E-08
    OMP2 iteration:   8 Energy:    -76.09619304 dE: -3.19300E-08
    OMP2 iteration:   9 Energy:    -76.09619306 dE: -1.65529E-08
    OMP2 iteration:  10 Energy:    -76.09619307 dE: -8.24711E-09

We compare the final energy with Psi4's OMP2 energy:

```python
psi4.compare_values(psi4.energy('omp2'), E_OMP2, 6, 'OMP2 Energy')
```

    	OMP2 Energy.......................................................PASSED

    True

## References

1. Background paper:
    >"Quadratically convergent algorithm for orbital optimization in the orbital-optimized
coupled-cluster doubles method and in orbital-optimized second-order Møller-Plesset
perturbation theory"[[Bozkaya:2011:135](http://aip.scitation.org/doi/10.1063/1.3631129)] U. Bozkaya, J. M. Turney, Y. Yamaguchi, H. F. Schaefer III, and C. D. Sherrill, *J. Chem. Phys.* **135**, 104103 (2011).

2. Useful notes on orbital rotation:
	> A. V. Copan, "Orbital Relaxation" accessed with https://github.com/CCQC/chem-8950/tree/master/2017/.

3. Algorithms from:
	> A. V. Copan, "OMP2" accessed with https://github.com/CCQC/chem-8950/tree/master/2017/programming.

```python

```


```python
from IPython.core.display import HTML
from IPython.display import Image
import ipywidgets as widgets
import IPython.display as display

import sympy as sp
import numpy as np
from utils import symplot, symdisp, round_expr

HTML("""
<style>
.output_png {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}
</style>
""")
```

# *Circuitos Elétricos I - Semana 8*

### Circuitos RL e RC de primeira ordem

Os quatro tipos possíveis de circuitos de primeira ordem estão ilustrados na figura abaixo.

Lembrando que um circuito de primeira ordem qualquer, com vários resistores e fontes, por exemplo, pode ser reduzido a um dos quatro circuitos acima fazendo $R=R_{th}$, $v_s=v_{th}$ e $i_s=i_{N}$. Logo, desde que o circuito contenha apenas um elemento indutor ou capacitor, a análise de um circuito de primeira ordem deve ser feita primeiramente determinando-se o circuito equivalente de Thévenin ou de Norton conectado aos terminais do elemento em questão.

### EDO linear homogênea

Seja para a corrente passando pelo indutor (circuito RL) ou para a tensão aplicada aos terminais do capacitor (circuito RC), a aplicação das leis de Kirchhoff e da **convenção passiva** nos circuitos RL e RC sempre levarão a uma EDO homogênea separável de primeira ordem do tipo

$$ \begin{equation}\label{eq1} \large \frac{dx(t)}{dt} = -\frac{1}{\tau}x(t), \end{equation}$$

com $\tau$ sendo a constante de tempo do circuito. A solução de (\ref{eq1}) pode ser obtida via integração fazendo

$$ \begin{equation}  \large \int_{x\left(t_{0}^+\right)}^{x(t)} \frac{d u}{u}=-\frac{1}{\tau} \int_{t_{0}^+}^{t} d v \end{equation}$$.

Logo, a solução da EDO homogênea será dada por  $$ \begin{equation} \large x(t) = x(t_0^+)e^{-\frac{(t-t_0^+)}{\tau}}. \end{equation} $$

### Resposta natural

A resposta natural de circuitos RL/RC corresponderá a solução da EDO homogênea, ou seja,

$$ \begin{equation} \large i_L(t) = i_L(t_0^+)e^{-\frac{(t-t_0^+)}{\tau}}, \end{equation}$$

com $\tau = L/R$ para o circuito RL e

$$\begin{equation}  \large v_C(t) = v_C(t_0^+)e^{-\frac{(t-t_0^+)}{\tau}}, \end{equation} $$

com $\tau = RC$ para o circuito RC.

### Resposta ao degrau

A resposta ao degrau de circuitos RL/RC corresponderá a solução da EDO homogênea adicionada da solução particular (ou solução de regime estacionário). Logo,

$$\large i_L(t) = i_L(\infty) + A_0e^{-\frac{(t-t_0^+)}{\tau}}, $$

com $\tau = L/R$ para o circuito RL e

$$\large v_C(t) = v_C(\infty) + A_0e^{-\frac{(t-t_0^+)}{\tau}}, $$

com $\tau = RC$ para o circuito RC.

A constante $A_o$ da resposta ao degrau pode ser determinada utilizando as condições iniciais de corrente no indutor ou tensão no capacitor.

Desse modo, para o circuito RL

$$\large{\begin{align} i_L(t_0^+) &= i_L(\infty) + A_0e^{-\frac{(t_0^+-t_0^+)}{\tau}}\nonumber\\ &= i_L(\infty) + A_0 \nonumber\\ \Rightarrow A_0 &= i_L(t_0^+)- i_L(\infty)\nonumber
\end{align} }$$

e para o circuito RC

$$\large{\begin{align} v_C(t_0^+) &= v_C(\infty) + A_0e^{-\frac{(t_0^+-t_0^+)}{\tau}}\nonumber\\ &= v_C(\infty) + A_0 \nonumber\\ \Rightarrow A_0 &= v_C(t_0^+)- v_C(\infty)\nonumber
\end{align} }$$

### Resposta geral

A resposta geral de circuitos RL/RC corresponderá às expressões,

$$ \begin{equation} \large i_L(t) = i_L(\infty) + \left[i_L(t_0^+)- i_L(\infty)\right]e^{-\frac{(t-t_0^+)}{\tau}},\end{equation} $$

com $\tau = L/R$ para o circuito RL e

$$ \begin{equation} \large v_C(t) = v_C(\infty) + \left[v_C(t_0^+)- v_C(\infty)\right]e^{-\frac{(t-t_0^+)}{\tau}},\end{equation} $$

com $\tau = RC$ para o circuito RC.

Finalmente, para qualquer circuito de primeira ordem,

$$\begin{equation}  \large x(t) = x(\infty) + \left[x(t_0^+)-x(\infty)\right]e^{-\frac{(t-t_0^+)}{\tau}},\end{equation} $$

com $\tau$ sendo a constante de tempo associada a este circuito.

### Problema 1

Para circuito da figura abaixo, a chave encontra-se conectada ao terminal $a$ há muito tempo. Em $t=0s$, posição da chave muda do ponto $a$ para o ponto $b$. Em $t=20 ms$, a chave é desconectada do ponto $b$, permanecendo aberta. Determine:

a. A corrente no indutor para $0^+s\leq t \leq 20^-ms$.\
b. A tensão nos terminais indutor em $t=0^-s$ e $t=0^+s$.\
c. A tensão nos terminais do resistor de 40 $\Omega$ em $t=0^-s$ e $t=0^+s$.\
d. A corrente no indutor para $t\geq20^+ms$.\
e. A tensão nos terminais do indutor em $t=20^+ms$.

Simulação do circuito disponível no link: https://tinyurl.com/yfs69qqu

#### Visualização das curvas $i_L(t)$, $v_L(t)$ e $p_L(t)$

```python
# parâmetros do circuito
L   = 1
vth = -80e-3
Rth = 18

# informações obtidas pela análise do circuito
iL_inf = vth/Rth # iL(infinito)
iL_t0  = 2.9e-3  # iL(t0+)

τ = L/Rth  # constante de tempo do circuito RL
t0  = 0    # instante inicial

t = sp.symbols('t', real=True) # define a variável tempo

iL = sp.Piecewise( (iL_t0, t<0),                                              # iL(t) para t<t0-
                   (iL_inf + (iL_t0 - iL_inf)*sp.exp(-(t-t0)/τ), t>=0) )*1000 # iL(t) para t>t0+

symdisp('i_L(t) =', round_expr(iL.simplify(), 3), 'mA')

intervalo  = np.linspace(t0-2*τ, t0+8*τ, 400)
symplot(t, iL, intervalo, funLabel = '$i_L(t)$ [mA]')
```

```python
vL = L*sp.diff(iL,t)
vL = vL.simplify()

symdisp('v_L(t) = ', vL, ' mV')

symplot(t, vL, intervalo, funLabel = '$v_L(t)$ [mV]')
```

```python
pL = vL*iL
pL = pL.simplify()

symdisp('p_L(t) = ', round_expr(pL,3), ' μW')

symplot(t, pL, intervalo, funLabel = '$p_L(t)$ [μW]')
```

```python
import ipywidgets as widgets
import IPython.display as display

img1 = open('./figures/J11C2a.gif', 'rb').read()
img2 = open('./figures/J11C2b.gif', 'rb').read()

wi1 = widgets.Image(value=img1, format='gif', width=600, height=400)
wi2 = widgets.Image(value=img2, format='gif', width=600, height=400)

sidebyside = widgets.HBox([wi1, wi2])
display.display(sidebyside)
```

### Problema 2

No circuito da figura abaixo, antes de conectado ao circuito, o capacitor possui uma tensão inicial de $v_C=10~V$. Em $t=0s$, o capacitor é conectado ao circuito. Determine:

a. O circuito equivalente de Thévenin do ponto de vista dos terminais do capacitor.\
b. A tensão no capacitor $v_C(t)$ para $t\geq 0^+s$.\
c. A tensão $v_x(t)$ para $t\geq 0^+s$.

Simulação do circuito disponível no link: https://tinyurl.com/yzhty8w3

Jupyter notebooks/.ipynb_checkpoints/Circuitos Eletricos I - Semana 8.1-checkpoint.ipynb


## GeostatsPy: Multivariate Analysis for Subsurface Data Analytics in Python

### Michael Pyrcz, Associate Professor, University of Texas at Austin

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

### PGE 383 Exercise: Multivariate Analysis for Subsurface Data Analytics in Python

Here's a simple workflow, demonstration of multivariate analysis for subsurface modeling workflows. This should help you get started with building subsurface models that integrate uncertainty in the sample statistics.

#### Bivariate Analysis

Understand and quantify the relationship between two variables

* example: relationship between porosity and permeability
* how can we use this relationship?

What would be the impact if we ignore this relationship and simply modeled porosity and permeability independently?

* no relationship beyond constraints at data locations
* independent away from data
* nonphysical results, unrealistic uncertainty models

#### Bivariate Statistics

Pearson’s Product‐Moment Correlation Coefficient
* Provides a measure of the degree of linear relationship.
* We refer to it as the 'correlation coefficient'

Let's review the sample variance of variable $x$. Of course, I'm truncating our notation as $x$ is a set of samples a locations in our modeling space, $x(\bf{u_\alpha}), \, \forall \, \alpha = 0, 1, \dots, n - 1$.

\begin{equation}
\sigma^2_{x}  = \frac{\sum_{i=1}^{n} (x_i - \overline{x})^2}{(n-1)}
\end{equation}

We can expand the the squared term and replace on of them with $y$, another variable in addition to $x$.

\begin{equation}
C_{xy}  = \frac{\sum_{i=1}^{n} (x_i - \overline{x})(y_i - \overline{y})}{(n-1)}
\end{equation}

We now have a measure that represents the manner in which variables $x$ and $y$ co-vary or vary together.  We can standardized the covariance by the product of the standard deviations of $x$ and $y$ to calculate the correlation coefficent.

\begin{equation}
\rho_{xy}  = \frac{\sum_{i=1}^{n} (x_i - \overline{x})(y_i - \overline{y})}{(n-1)\sigma_x \sigma_y}, \, -1.0 \le \rho_{xy} \le 1.0
\end{equation}

In summary we can state that the correlation coefficient is related to the covariance as:

\begin{equation}
\rho_{xy}  = \frac{C_{xy}}{\sigma_x \sigma_y}
\end{equation}

The Person's correlation coefficient is quite sensitive to outliers and depature from linear behavoir (in the bivariate sense).  We have an altenrative known as the Spearman's rank correlations coefficient.

\begin{equation}
\rho_{R_x R_y}  = \frac{\sum_{i=1}^{n} (R_{x_i} - \overline{R_x})(R_{y_i} - \overline{R_y})}{(n-1)\sigma_{R_x} \sigma_{R_y}}, \, -1.0 \le \rho_{xy} \le 1.0
\end{equation}

The rank correlation applies the rank transform to the data prior to calculating the correlation coefficent.  To calculate the rank transform simply replace the data values with the rank $R_x = 1,\dots,n$, where $n$ is the maximum value and $1$ is the minimum value.

\begin{equation}
x_\alpha, \, \forall \alpha = 1,\dots, n, \, | \, x_i \ge x_j \, \forall \, i \gt j
\end{equation}

\begin{equation}
R_{x_i} = i
\end{equation}

The corelation coefficients provide useful metrics to quantify relationships between two variables at a time. We can also consider bivariate scatter plots and matrix scatter plots to visualize multivariate data. In general, current practical subsurface modeling is bivariate, two variables at a time.

#### Multivariate Statistics

See lecture on Multivariate Statistics, including the concepts of joint, conditional and marginal probability.

#### Objective

In the PGE 383: Stochastic Subsurface Modeling class I want to provide hands-on experience with building subsurface modeling workflows. Python provides an excellent vehicle to accomplish this. I have coded a package called GeostatsPy with GSLIB: Geostatistical Library (Deutsch and Journel, 1998) functionality that provides basic building blocks for building subsurface modeling workflows.

The objective is to remove the hurdles of subsurface modeling workflow construction by providing building blocks and sufficient examples. This is not a coding class per se, but we need the ability to 'script' workflows working with numerical methods.

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

You will need to copy the data file to your working directory.  They are available here:

* Tabular data - sample_data_MV_biased.csv at https://git.io/fhgu0.

There are exampled below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

```python
import geostatspy.GSLIB as GSLIB          # GSLIB utilies, visualization and wrapper
import geostatspy.geostats as geostats    # GSLIB methods convert to Python
```

We will also need some standard packages. These should have been installed with Anaconda 3.

```python
import numpy as np                        # ndarrys for gridded data
import pandas as pd                       # DataFrames for tabular data
import os                                 # set working directory, run executables
import matplotlib.pyplot as plt           # for plotting
from scipy import stats                   # summary statistics
import math                               # trig etc.
import scipy.signal as signal             # kernel for moving window calculation
import random
import seaborn as sns
```

#### Set the working directory

I always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time).

```python
os.chdir("c:/PGE383")                     # set the working directory
```

#### Loading Tabular Data

Here's the command to load our comma delimited data file in to a Pandas' DataFrame object.

```python
df = pd.read_csv('sample_data_MV_biased.csv') # load our data table (wrong name!)
```

Visualizing the DataFrame would be useful and we already learned about these methods in this demo (https://git.io/fNgRW).

We can preview the DataFrame by printing a slice or by utilizing the 'head' DataFrame member function (with a nice and clean format, see below). With the slice we could look at any subset of the data table and with the head command, add parameter 'n=13' to see the first 13 rows of the dataset.

```python
print(df.iloc[0:5,:])                   # display first 4 samples in the table as a preview
df.head(n=13)                           # we could also use this command for a table preview
```

       Unnamed: 0      X      Y  Facies  Porosity        Perm           AI
    0           0  100.0  900.0     0.0  0.101319    1.996868  5590.417154
    1           1  100.0  800.0     1.0  0.147676   10.711789  3470.845666
    2           2  100.0  700.0     1.0  0.145912   17.818143  3586.988513
    3           3  100.0  600.0     1.0  0.186167  217.109365  3732.114787
    4           4  100.0  500.0     1.0  0.146088   16.717367  2534.551236

      Unnamed: 0
      X
      Y
      Facies
      Porosity
      Perm
      AI

      0
      0
      100.0
      900.0
      0.0
      0.101319
      1.996868
      5590.417154

      1
      1
      100.0
      800.0
      1.0
      0.147676
      10.711789
      3470.845666

      2
      2
      100.0
      700.0
      1.0
      0.145912
      17.818143
      3586.988513

      3
      3
      100.0
      600.0
      1.0
      0.186167
      217.109365
      3732.114787

      4
      4
      100.0
      500.0
      1.0
      0.146088
      16.717367
      2534.551236

      5
      9
      200.0
      900.0
      1.0
      0.129949
      23.348473
      4781.590782

      6
      11
      200.0
      700.0
      1.0
      0.185299
      595.674540
      4729.017454

      7
      12
      200.0
      600.0
      1.0
      0.164923
      154.555841
      2147.668805

      8
      13
      200.0
      500.0
      1.0
      0.167026
      50.970899
      4583.679953

      9
      14
      200.0
      400.0
      1.0
      0.125382
      4.061772
      4477.930200

      10
      16
      200.0
      200.0
      0.0
      0.077419
      1.162642
      6844.535923

      11
      19
      300.0
      800.0
      1.0
      0.164797
      497.593578
      3881.489046

      12
      20
      300.0
      700.0
      1.0
      0.165155
      486.504799
      3726.023776

#### Summary Statistics for Tabular Data

The table includes X and Y coordinates (meters), Facies 1 and 0 (1 is sandstone and 0 interbedded sand and mudstone), Porosity (fraction), and permeability as Perm (mDarcy).

There are a lot of efficient methods to calculate summary statistics from tabular data in DataFrames. The describe command provides count, mean, minimum, maximum, and quartiles all in a nice data table. We use transpose just to flip the table so that features are on the rows and the statistics are on the columns.

```python
df.describe().transpose()
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Unnamed: 0
      368.0
      293.260870
      169.058258
      0.000000
      150.500000
      296.000000
      439.500000
      586.000000

      X
      368.0
      499.565217
      289.770794
      0.000000
      240.000000
      500.000000
      762.500000
      990.000000

      Y
      368.0
      520.644022
      277.412187
      9.000000
      269.000000
      539.000000
      769.000000
      999.000000

      Facies
      368.0
      0.597826
      0.491004
      0.000000
      0.000000
      1.000000
      1.000000
      1.000000

      Porosity
      368.0
      0.127026
      0.030642
      0.041122
      0.103412
      0.125842
      0.148623
      0.210258

      Perm
      368.0
      85.617362
      228.362654
      0.094627
      2.297348
      10.377292
      50.581288
      1991.097723

      AI
      368.0
      4791.736646
      974.560569
      1981.177309
      4110.728374
      4713.325533
      5464.043562
      7561.250336

#### Visualizing Tabular Data with Location Maps

It is natural to set the x and y coordinate and feature ranges manually. e.g. do you want your color bar to go from 0.05887 to 0.24230 exactly? Also, let's pick a color map for display. I heard that plasma is known to be friendly to the color blind as the color and intensity vary together (hope I got that right, it was an interesting Twitter conversation started by Matt Hall from Agile if I recall correctly). We will assume a study area of 0 to 1,000m in x and y and omit any data outside this area.

```python
xmin = 0.0; xmax = 1000.0               # range of x values
ymin = 0.0; ymax = 1000.0               # range of y values
pormin = 0.05; pormax = 0.25;           # range of porosity values
permmin = 0.01; permmax = 2000.0         # range of permeability values
AImin = 2000.0; AImax = 8000.0          # range of AI values
nx = 100; ny = 100; csize = 10.0
cmap = plt.cm.plasma                    # color map
```

Let's try out locmap. This is a reimplementation of GSLIB's locmap program that uses matplotlib. I hope you find it simpler than matplotlib, if you want to get more advanced and build custom plots lock at the source. If you improve it, send me the new code.

Now we can populate the plotting parameters and visualize the porosity data.

```python
plt.subplot(221)
GSLIB.locmap_st(df,'X','Y','Facies',xmin,xmax,ymin,ymax,0,1,'Well Data - Porosity','X(m)','Y(m)','Facies (0-shale, 1-sand)',cmap)

plt.subplot(222)
GSLIB.locmap_st(df,'X','Y','Porosity',xmin,xmax,ymin,ymax,pormin,pormax,'Well Data - Porosity','X(m)','Y(m)','Porosity (fraction)',cmap)

plt.subplot(223)
GSLIB.locmap_st(df,'X','Y','Perm',xmin,xmax,ymin,ymax,permmin,permmax,'Well Data - Permeability','X(m)','Y(m)','Permeability (md)',cmap)

plt.subplot(224)
GSLIB.locmap_st(df,'X','Y','AI',xmin,xmax,ymin,ymax,AImin,AImax,'Well Data - Acoustic Impedance','X(m)','Y(m)','Acoustic Impedance (m/s x g/cm^3)',cmap)

plt.subplots_adjust(left=0.0, bottom=0.0, right=3.0, top=3.2, wspace=0.2, hspace=0.2)
plt.show()
```

#### Bivariate Analysis

Let's start with some simple bivariate plotting and calculations. Firsty some scatter plots.

```python
plt.subplot(121)
plt.plot(df['Porosity'].values,df['Perm'].values, 'o', label='', markerfacecolor='red', markeredgecolor='black', alpha=0.2)
plt.title('Well Data Permeability vs. Porostiy')
plt.xlabel('Porosity (fraction)')
plt.ylabel('Permeability (mD)')
#plt.legend()

plt.subplot(122)
plt.plot(df['AI'].values,df['Porosity'].values, 'o', label='', markerfacecolor='red', markeredgecolor='black', alpha=0.2)
plt.title('Well Data Porostiy vs. Acoustic Impedance')
plt.ylabel('Porosity (fraction)')
plt.xlabel('Acoustic Impedance (m/s x g/cm^3)')

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.2, top=1.2, wspace=0.2, hspace=0.2)
plt.show()
```

#### Correlation and Covariance

It is straight forward to calculat the covariance and correlation from the pairs of data in our dataset. Here's the covariance.  Notice that the matrix is symmetrical?  Makes sense, as the $C_{Por,Perm} = C_{Perm,Por}$.  Also, note that the diagonal values ($C_{i,j}$ where $i=j$) equal to the variance.  We check porosity by calculating the variance.

```python
print(df.iloc[:,3:7].cov())                    # the covariance matrix for columns 3,4,5 and 6 and all rows
print('The variance of porosity is ' + str(round(np.var(df['Porosity'].values),6)))
```

                  Facies   Porosity          Perm             AI
    Facies      0.241085   0.011370     33.010926    -248.032706
    Porosity    0.011370   0.000939      4.055029     -17.132244
    Perm       33.010926   4.055029  52149.501968  -46471.695092
    AI       -248.032706 -17.132244 -46471.695092  949768.302409
    The variance of porosity is 0.000936

Here's the correlation coefficient.

```python
df.iloc[:,3:7].corr()
```

      Facies
      Porosity
      Perm
      AI

      Facies
      1.000000
      0.755682
      0.294406
      -0.518340

      Porosity
      0.755682
      1.000000
      0.579493
      -0.573700

      Perm
      0.294406
      0.579493
      1.000000
      -0.208812

      AI
      -0.518340
      -0.573700
      -0.208812
      1.000000

#### Matrix Scatter Plots

If we have 3 or more variables to consider then matrix scatter plot offer an efficient method to display the multivariate relationships, 2 variables at a time.  Once can identify:

1. the range, envelope of the paired data
2. homoscedastic and heteroscedastic behavoirs
3. non-linear features

Here's the seaborn package matrix scatter plot function, pairplot. Let's color the results by facies.

```python
sns.pairplot(df, hue='Facies',vars=['Facies','Porosity','Perm','AI'],markers='o')
```

#### Joint, Conditional and Marginals

We can use kernel density estimation to estimate the joint probabilities density function (pdf) for the paired data, a 2D pdf! We could use this to estimate any required joint, marginal and conditional probability (care must be taken with normalization). Let's use the seaborn package 'kdeplot' function to estimate the joint pdf for porosity and acoustic impedance.

```python
ax = sns.kdeplot(df['AI'].values,df['Porosity'].values, shade=True, n_levels = 10,cmap=cmap,cbar= True, shade_lowest = False)
ax.set_xlabel('Acoustic Impedance (m/s x g/cm^3)'); ax.set_ylabel('Porosity (fraction)'); ax.set_title('Porosity vs. Acoustic Impedance')
```

I think is it useful to visualize the joint pdfs with the marginal pdfs on a single plot. We can use seaborn's 'jointplot' to accomplish this.

```python
ax = sns.jointplot('AI','Porosity', df, kind='kde',shade = False, n_levels = 10,cmap=cmap, shade_lowest = True);
```

The correlation coefficient and the p-value of the correlation coefficient (significant if < $\alpha/2$ or > $1-\alpha/2$).

#### Calculating Conditional Statistics

Of course, we could just calculate the conditional statistics by-hand. We need to select some bins over the variable that we will condition to. Let's calculate conditional statistical of porosity given acoustic impedance. We will select 9 equal spaced bins.

```python
AI_bins = np.linspace(2000,8000,10)            # set the bin boundaries and then the centroids for plotting
AI_centroids = np.linspace((AI_bins[0]+AI_bins[1])*0.5,(AI_bins[8]+AI_bins[9])*0.5,9)
print(AI_bins)                                 # check the boundaries
print(AI_centroids)                            # check the centroids
df['AI_bins'] = pd.cut(df['AI'], AI_bins,labels = AI_centroids) # cut on bondaries and lable with centroids
df.head()                                      # check the new column in the DataFrame
```

    [2000.         2666.66666667 3333.33333333 4000.         4666.66666667
     5333.33333333 6000.         6666.66666667 7333.33333333 8000.        ]
    [2333.33333333 3000.         3666.66666667 4333.33333333 5000.
     5666.66666667 6333.33333333 7000.         7666.66666667]

      Unnamed: 0
      X
      Y
      Facies
      Porosity
      Perm
      AI
      AI_bins

      0
      0
      100.0
      900.0
      0.0
      0.101319
      1.996868
      5590.417154
      5666.666667

      1
      1
      100.0
      800.0
      1.0
      0.147676
      10.711789
      3470.845666
      3666.666667

      2
      2
      100.0
      700.0
      1.0
      0.145912
      17.818143
      3586.988513
      3666.666667

      3
      3
      100.0
      600.0
      1.0
      0.186167
      217.109365
      3732.114787
      3666.666667

      4
      4
      100.0
      500.0
      1.0
      0.146088
      16.717367
      2534.551236
      2333.333333

Now we can use the 'groupby' function built-in to Pandas' DataFrames to extract subsets of porosity values in each bin from the DataFrame and then to calculate the conditional statistics: expectation, P90 and P10. Let's plot the result.

```python
cond_exp = df.groupby('AI_bins')['Porosity'].mean()
cond_P90 = df.groupby('AI_bins')['Porosity'].quantile(.9)
cond_P10 = df.groupby('AI_bins')['Porosity'].quantile(.1)

plt.subplot(111)
plt.plot(AI_centroids,cond_exp,color='black')
plt.plot(AI_centroids,cond_P90,'r--',color='black',linewidth = 1.0)
plt.plot(AI_centroids,cond_P10,'r--',color='black',linewidth = 1.0)

plt.xlabel('Acoustic Impedance (m/s x g/cm^3)')
plt.ylabel('Porosity (fraction) | Acoustic Impedance')
t = plt.title('Porosity Conditional to Accoustic Impedance')
plt.ylim(pormin,pormax)
plt.xlim(AImin,AImax)
plt.text(3200, .10, 'P10')
plt.text(3200, .15, 'Expectation')
plt.text(3200, .19, 'P90')

plt.grid(True)

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.2, top=1.2, wspace=0.2, hspace=0.2)
plt.show()
```

Does acoustic impedance provide information about porosity?

Yes, clearly the conditional statistics vary over acoustic impedance, knowing the acoustic impedance reduces the uncertainty about porosity.

#### Comments

This was a basic demonstration of multivariate analysis. A lot more could be done, for example, there are methods that reduce the dimensionality, and remove dependency to allow for independent variable modeling workflows etc.

I have other demonstrations on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations, trend modeling and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy.

I hope this was helpful,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

```python

```


# Qiskit Aer: Pulse simulation of two qubits using a Duffing oscillator model

This notebook shows how to use the Qiskit Aer pulse simulator, which simulates experiments specified as pulse `Schedule` objects at the Hamiltonian level. The simulator solves the Schrodinger equation for a specified Hamiltonian model and pulse `Schedule` in the frame of the drift Hamiltonian.

In particular, in this tutorial we will:
- Construct a model of a two qubit superconducting system.
- Calibrate $\pi$ pulses on each qubit in the simulated system.
- Observe cross-resonance oscillations when driving qubit 1 with target qubit 0.

The Introduction outlines the concepts and flow of this notebook.

## 1. Introduction

The main sections proceed as follows.

### Section 3: Duffing oscillator model

To simulate a physical system, it is necessary to specify a model. In this notebook, we will model superconducting qubits as a collection of *Duffing oscillators*. The model is specified in terms of the following parameters:

-  Each Duffing oscillator is specified by a frequency $\nu$, anharmonicity $\alpha$, and drive strength $r$, which result in the Hamiltonian terms:

\begin{equation}
    2\pi\nu a^\dagger a + \pi \alpha a^\dagger a(a^\dagger a - 1) + 2 \pi r (a + a^\dagger) \times D(t),
\end{equation}

where $D(t)$ is the signal on the drive channel for the qubit, and $a^\dagger$ and $a$ are, respectively, the creation and annihilation operators for the qubit. Note that the drive strength $r$ sets the scaling of the control term, with $D(t)$ assumed to be a complex and unitless number satisfying $|D(t)| \leq 1$.
-  A coupling between a pair of oscillators $(l,k)$ is specified by the coupling strength $J$, resulting in an exchange coupling term:

\begin{equation}
    2 \pi J (a_l a_k^\dagger + a_l^\dagger a_k),
\end{equation}

where the subscript denotes which qubit the operators act on.
- Additionally, for numerical simulation, it is necessary to specify a cutoff dimension; the Duffing oscillator model is *infinite dimensional*, and computer simulation requires restriction of the operators to a finite dimensional subspace.

**In the code:** We will define a model of the above form for two coupled qubits using the helper function `duffing_system_model`.

### Section 4: $\pi$-pulse calibration using Ignis

Once the model is defined, we will calibrate $\pi$-pulses on each qubit. A $\pi$-pulse is defined as a pulse on the drive channel of a qubit that "flips" the qubit; i.e. that takes the ground state to the first excited state, and the first excited state to the ground state.

We will experimentally find a $\pi$-pulse for each qubit using the following procedure:
- A fixed pulse shape is set - in this case it will be a Gaussian pulse.
- A sequence of experiments is run, each consisting of a Gaussian pulse on the qubit, followed by a measurement, with each experiment in the sequence having a subsequently larger amplitude for the Gaussian pulse.
- The measurement data is fit, and the pulse amplitude that completely flips the qubit is found (i.e. the $\pi$-pulse amplitude).

**In the code:** Using Ignis we will construct `Schedule` objects for the above experiments, then fit the data to find the $\pi$-pulse amplitudes.

### Section 5: Cross-resonance oscillations

Once the $\pi$-pulses are calibrated, we will simulate the effects of cross-resonance driving on qubit $1$ with target qubit $0$. This means that we will drive qubit $1$ at the frequency of qubit $0$, with the goal of observing that the trajectory and oscillations of qubit $0$ *depends* on the state of qubit $1$. This phenomenon provides a basis for creating two-qubit *controlled* gates. Note: This section requires the calibration of the $\pi$-pulse in Section 4.

To observe cross-resonance driving, we will use experiments very similar to the $\pi$-pulse calibration case:
- Initially, qubit $1$ is either left in the ground state, or is driven to its first excited state using the $\pi$-pulse found in Section 4.
- A sequence of experiments is run, each consisting of a Gaussian pulse on qubit $1$ driven at the frequency of qubit $0$, followed by a measurement of both qubits, with each experiment of the sequence having a subsequently larger amplitude for the Gaussian pulse.

**In the code:** Functions for defining the experiments and visualizing the data are constructed, including a visualization of the trajectory of the target qubit on the Bloch sphere.

## 2. Imports

This notebook makes use of the following imports.

```python
import numpy as np
from scipy.optimize import curve_fit, root

# visualization tools
import matplotlib.pyplot as plt
from qiskit.visualization.bloch import Bloch
```

Import qiskit libraries for working with `pulse` and calibration:

```python
import qiskit.pulse as pulse
from qiskit.pulse.commands.parametric_pulses import Gaussian, GaussianSquare
from qiskit.compiler import assemble

from qiskit.ignis.characterization.calibrations import rabi_schedules, RabiFitter
```

Imports for qiskit pulse simulator:

```python
# The pulse simulator
from qiskit.providers.aer import PulseSimulator

# function for constructing duffing models
from qiskit.providers.aer.pulse import duffing_system_model
```

## 3. Duffing oscillator system model

An object representing a model for a collection of Duffing oscillators can be constructed using the `duffing_system_model` function. Here we construct a $2$ Duffing oscillator model with cutoff dimension $3$.

```python
# cutoff dimension
dim_oscillators = 3

# frequencies for transmon drift terms, harmonic term and anharmonic term
# Number of oscillators in the model is determined from len(oscillator_freqs)
oscillator_freqs = [5.0e9, 5.2e9]
anharm_freqs = [-0.33e9, -0.33e9]

# drive strengths
drive_strengths = [0.02e9, 0.02e9]

# specify coupling as a dictionary (qubits 0 and 1 are coupled with a coefficient 0.002e9)
coupling_dict = {(0,1): 0.002e9}

# sample duration for pulse instructions
dt = 1e-9

# create the model
two_qubit_model = duffing_system_model(dim_oscillators=dim_oscillators,
                                       oscillator_freqs=oscillator_freqs,
                                       anharm_freqs=anharm_freqs,
                                       drive_strengths=drive_strengths,
                                       coupling_dict=coupling_dict,
                                       dt=dt)
```

The function `duffing_system_model` returns a `PulseSystemModel` object, which is a general object for storing model information required for simulation with the `PulseSimulator`.

## 4 Calibrating $\pi$ pulses on each qubit using Ignis

As described in the introduction, we now calibrate $\pi$ pulses on each qubit in `two_qubit_model`. The experiments in this calibration procedure are known as *Rabi experiments*, and the data we will observe are known as *Rabi oscillations*.

### 4.1 Constructing the schedules

We construct the schedules using the `rabi_schedules` function in Ignis. To do this, we need to supply an `InstructionScheduleMap` containing a measurement schedule.

```python
# list of qubits to be used throughout the notebook
qubits = [0, 1]

# Construct a measurement schedule and add it to an InstructionScheduleMap
meas_amp = 0.025
meas_samples = 1200
meas_sigma = 4
meas_width = 1150
meas_pulse = GaussianSquare(duration=meas_samples, amp=meas_amp,
                            sigma=meas_sigma, width=meas_width)

acq_sched = pulse.Acquire(meas_samples, pulse.AcquireChannel(0), pulse.MemorySlot(0))
acq_sched += pulse.Acquire(meas_samples, pulse.AcquireChannel(1), pulse.MemorySlot(1))

measure_sched = pulse.Play(meas_pulse, pulse.MeasureChannel(0)) | pulse.Play(meas_pulse, pulse.MeasureChannel(1)) | acq_sched

inst_map = pulse.InstructionScheduleMap()
inst_map.add('measure', qubits, measure_sched)
```

Next, construct the Rabi schedules.

```python
# construct Rabi experiments
drive_amps = np.linspace(0, 0.9, 48)
drive_sigma = 16
drive_duration = 128
drive_channels = [pulse.DriveChannel(0), pulse.DriveChannel(1)]

rabi_experiments, rabi_amps = rabi_schedules(amp_list=drive_amps,
                                             qubits=qubits,
                                             pulse_width=drive_duration,
                                             pulse_sigma=drive_sigma,
                                             drives=drive_channels,
                                             inst_map=inst_map,
                                             meas_map=[[0, 1]])
```

The `Schedule`s in `rabi_schedules` correspond to experiments to generate Rabi oscillations on both qubits in parallel. Each experiment consists of a Gaussian pulse on the qubits of a given magnitude, followed by measurement.

For example:

```python
rabi_experiments[10].draw()
```

### 4.2 Simulate the Rabi experiments

To simulate the Rabi experiments, assemble the `Schedule` list into a qobj. When assembling, pass the `PulseSimulator` as the backend.

Here, we want to use local oscillators with frequencies automatically computed from Duffing model Hamiltonian.

```python
# instantiate the pulse simulator
backend_sim = PulseSimulator()

# compute frequencies from the Hamiltonian
qubit_lo_freq = two_qubit_model.hamiltonian.get_qubit_lo_from_drift()

rabi_qobj = assemble(rabi_experiments,
                     backend=backend_sim,
                     qubit_lo_freq=qubit_lo_freq,
                     meas_level=1,
                     meas_return='avg',
                     shots=512)
```

Run the simulation using the simulator backend.

```python
# run the simulation
rabi_result = backend_sim.run(rabi_qobj, two_qubit_model).result()
```

### 4.3 Fit and plot the data

Next, we use `RabiFitter` in Ignis to fit the data, extract the $\pi$-pulse amplitude, and then plot the data.

```python
rabifit = RabiFitter(rabi_result, rabi_amps, qubits, fit_p0 = [0.5,0.5,0.6,1.5])

plt.figure(figsize=(15, 10))
q_offset = 0
for qubit in qubits:
    ax = plt.subplot(2, 2, qubit + 1)
    rabifit.plot(qubit, ax=ax)
    print('Pi Amp: %f'%rabifit.pi_amplitude(qubit))
plt.show()
```

Plotted is the averaged IQ data for observing each qubit. Observe that here, each qubit oscillates between the 0 and 1 state. The amplitude at which a given qubit reaches the peak of the oscillation is the desired $\pi$-pulse amplitude.

## 5. Oscillations from cross-resonance drive

Next, we simulate the effects of a cross-resonance drive on qubit $1$ with target qubit $0$, observing that the trajectory and oscillations of qubit $0$ *depends* on the state of qubit $1$.

**Note:** This section depends on the $\pi$-pulse calibrations of Section 2.

### 5.1 Cross-resonance `ControlChannel` indices

Driving qubit $1$ at the frequency of qubit $0$ requires use of a pulse `ControlChannel`. The model generating function `duffing_system_model`, automatically sets up `ControlChannels` for performing cross-resonance drives between pairs of coupled qubits. The index of the `ControlChannel` for performing a particular cross-resonance drive is retrievable using the class method `control_channel_index` on the returned `PulseSystemModel`. For example, to get the `ControlChannel` index corresponding to a CR drive on qubit 1 with target 0, call the function `control_channel_index` with the tuple `(1,0)`:

```python
two_qubit_model.control_channel_index((1,0))
```

    1

Hence, to perform a cross-resonance drive on qubit $1$ with target qubit $0$, use `ControlChannel(1)`. This will be made use of when constructing `Schedule` objects in this section.

### 5.2 Functions to generate the experiment list, and analyze the output

First, we define a function `cr_drive_experiments`, which, given the drive and target indices, and the option to either start with the drive qubit in the ground or excited state, returns a list of experiments for observing the oscillations.

```python
# store the pi amplitudes from Section 2 in a list
pi_amps = [rabifit.pi_amplitude(0), rabifit.pi_amplitude(1)]

def cr_drive_experiments(drive_idx,
                         target_idx,
                         flip_drive_qubit = False,
                         cr_drive_amps=np.linspace(0, 0.9, 16),
                         cr_drive_samples=800,
                         cr_drive_sigma=4,
                         pi_drive_samples=128,
                         pi_drive_sigma=16):
    """Generate schedules corresponding to CR drive experiments.

    Args:
        drive_idx (int): label of driven qubit
        target_idx (int): label of target qubit
        flip_drive_qubit (bool): whether or not to start the driven qubit in the ground or excited state
        cr_drive_amps (array): list of drive amplitudes to use
        cr_drive_samples (int): number samples for each CR drive signal
        cr_drive_sigma (float): standard deviation of CR Gaussian pulse
        pi_drive_samples (int): number samples for pi pulse on drive
        pi_drive_sigma (float): standard deviation of Gaussian pi pulse on drive

    Returns:
        list[Schedule]: A list of Schedule objects for each experiment
    """

    # Construct measurement commands to be used for all schedules
    meas_amp = 0.025
    meas_samples = 1200
    meas_sigma = 4
    meas_width = 1150
    meas_pulse = GaussianSquare(duration=meas_samples, amp=meas_amp,
                               sigma=meas_sigma, width=meas_width)

    acq_sched = pulse.Acquire(meas_samples, pulse.AcquireChannel(0), pulse.MemorySlot(0))
    acq_sched += pulse.Acquire(meas_samples, pulse.AcquireChannel(1), pulse.MemorySlot(1))

    # create measurement schedule
    measure_sched = (pulse.Play(meas_pulse, pulse.MeasureChannel(0)) |
                     pulse.Play(meas_pulse, pulse.MeasureChannel(1))|
                     acq_sched)

    # Create schedule
    schedules = []
    for ii, cr_drive_amp in enumerate(cr_drive_amps):

        # pulse for flipping drive qubit if desired
        pi_pulse = Gaussian(duration=pi_drive_samples, amp=pi_amps[drive_idx], sigma=pi_drive_sigma)

        # cr drive pulse
        cr_width = cr_drive_samples - 2*cr_drive_sigma*4
        cr_rabi_pulse = GaussianSquare(duration=cr_drive_samples,
                                       amp=cr_drive_amp,
                                       sigma=cr_drive_sigma,
                                       width=cr_width)

        # add commands to schedule
        schedule = pulse.Schedule(name='cr_rabi_exp_amp_%s' % cr_drive_amp)

        # flip drive qubit if desired
        if flip_drive_qubit:
            schedule += pulse.Play(pi_pulse, pulse.DriveChannel(drive_idx))

        # do cr drive
        # First, get the ControlChannel index for CR drive from drive to target
        cr_idx = two_qubit_model.control_channel_index((drive_idx, target_idx))
        schedule += pulse.Play(cr_rabi_pulse, pulse.ControlChannel(cr_idx))  << schedule.duration

        schedule += measure_sched << schedule.duration

        schedules.append(schedule)
    return schedules
```

Next we create two functions for observing the data:
- `plot_cr_pop_data` - for plotting the oscillations between the ground state and the first excited state
- `plot_bloch_sphere` - for viewing the trajectory of the target qubit on the bloch sphere

```python
def plot_cr_pop_data(drive_idx,
                     target_idx,
                     sim_result,
                     cr_drive_amps=np.linspace(0, 0.9, 16)):
    """Plot the population of each qubit.

    Args:
        drive_idx (int): label of driven qubit
        target_idx (int): label of target qubit
        sim_result (Result): results of simulation
        cr_drive_amps (array): list of drive amplitudes to use for axis labels
    """

    amp_data_Q0 = []
    amp_data_Q1 = []

    for exp_idx in range(len(cr_drive_amps)):
        exp_mem = sim_result.get_memory(exp_idx)
        amp_data_Q0.append(np.abs(exp_mem[0]))
        amp_data_Q1.append(np.abs(exp_mem[1]))

    plt.plot(cr_drive_amps, amp_data_Q0, label='Q0')
    plt.plot(cr_drive_amps, amp_data_Q1, label='Q1')
    plt.legend()
    plt.xlabel('Pulse amplitude, a.u.', fontsize=20)
    plt.ylabel('Signal, a.u.', fontsize=20)
    plt.title('CR (Target Q{0}, driving on Q{1})'.format(target_idx, drive_idx), fontsize=20)
    plt.grid(True)

def bloch_vectors(drive_idx, drive_energy_level, sim_result):
    """Plot the population of each qubit.

    Args:
        drive_idx (int): label of driven qubit
        drive_energy_level (int): energy level of drive qubit at start of CR drive
        sim_result (Result): results of simulation

    Returns:
        list: list of Bloch vectors corresponding to the final state of the target qubit
              for each experiment
    """

    # get the dimension used for simulation
    dim = int(np.sqrt(len(sim_result.get_statevector(0))))

    # get the relevant dressed state indices
    idx0 = 0
    idx1 = 0
    if drive_idx == 0:
        if drive_energy_level == 0:
            idx0, idx1 = 0, dim
        elif drive_energy_level == 1:
            idx0, idx1 = 1, dim + 1
    if drive_idx == 1:
        if drive_energy_level == 0:
            idx0, idx1 = 0, 1
        elif drive_energy_level == 1:
            idx0, idx1 = dim, dim + 1

    # construct Pauli operators for correct dressed manifold
    state0 = np.array([two_qubit_model.hamiltonian._estates[idx0]])
    state1 = np.array([two_qubit_model.hamiltonian._estates[idx1]])

    outer01 = np.transpose(state0)@state1
    outer10 = np.transpose(state1)@state0
    outer00 = np.transpose(state0)@state0
    outer11 = np.transpose(state1)@state1

    X = outer01 + outer10
    Y = -1j*outer01 + 1j*outer10
    Z = outer00 - outer11

    # function for computing a single bloch vector
    bloch_vec = lambda vec: np.real(np.array([np.conj(vec)@X@vec, np.conj(vec)@Y@vec, np.conj(vec)@Z@vec]))

    return [bloch_vec(sim_result.get_statevector(idx)) for idx in range(len(sim_result.results))]

def plot_bloch_sphere(bloch_vectors):
    """Given a list of Bloch vectors, plot them on the Bloch sphere

    Args:
        bloch_vectors (list): list of bloch vectors
    """
    sphere = Bloch()
    sphere.add_points(np.transpose(bloch_vectors))
    sphere.show()
```

### 5.3 Drive qubit 1 to observe CR oscillations on qubit 0

#### Qubit 1 in the ground state

First, we drive with both qubit 0 and qubit 1 in the ground state.

```python
# construct experiments
drive_idx = 1
target_idx = 0
flip_drive = False
experiments = cr_drive_experiments(drive_idx, target_idx, flip_drive)

# compute frequencies from the Hamiltonian
qubit_lo_freq = two_qubit_model.hamiltonian.get_qubit_lo_from_drift()

# assemble the qobj
cr_rabi_qobj = assemble(experiments,
                        backend=backend_sim,
                        qubit_lo_freq=qubit_lo_freq,
                        meas_level=1,
                        meas_return='avg',
                        shots=512)
```

Run the simulation:

```python
sim_result = backend_sim.run(cr_rabi_qobj, two_qubit_model).result()

plot_cr_pop_data(drive_idx, target_idx, sim_result)
```

Observe that qubit 1 remains in the ground state, while excitations are driven in qubit 0.

We may also observe the trajectory of qubit 0 on the Bloch sphere:

```python
bloch_vecs = bloch_vectors(drive_idx, int(flip_drive), sim_result)
plot_bloch_sphere(bloch_vecs)
```

#### Qubit 1 in the first excited state

Next, we again perform a CR drive qubit 1 with qubit 0 as the target, but now we start each experiment by flipping qubit 1 into the first excited state.

```python
# construct experiments, now with flip_drive == True
drive_idx = 1
target_idx = 0
flip_drive = True
experiments = cr_drive_experiments(drive_idx, target_idx, flip_drive)

# compute frequencies from the Hamiltonian
qubit_lo_freq = two_qubit_model.hamiltonian.get_qubit_lo_from_drift()

# assemble the qobj
cr_rabi_qobj = assemble(experiments,
                        backend=backend_sim,
                        qubit_lo_freq=qubit_lo_freq,
                        meas_level=1,
                        meas_return='avg',
                        shots=512)
```

```python
sim_result = backend_sim.run(cr_rabi_qobj, two_qubit_model).result()

plot_cr_pop_data(drive_idx, target_idx, sim_result)
```

Observe that now qubit 1 is in the excited state, while oscillations are again being driven on qubit 0, now at a different rate as before.

Again, observe the trajectory of qubit 0 on the Bloch sphere:

```python
bloch_vecs = bloch_vectors(drive_idx, int(flip_drive), sim_result)
plot_bloch_sphere(bloch_vecs)
```

Here we see that qubit 0 takes a *different* trajectory on the Bloch sphere when qubit 1 is in the excited state. This is what enables controlled operations between two qubits.

```python
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright
```

Version Information

Qiskit Software	Version

Qiskit	0.19.6

Terra	0.15.0.dev0+46faf3f

Aer	0.6.0

Ignis	0.4.0.dev0+f885a59

Aqua	0.7.3

IBM Q Provider	0.7.2

System information

Python	3.8.3 (default, Jul  2 2020, 11:26:31)
[Clang 10.0.0 ]

OS	Darwin

CPUs	8

Memory (Gb)	32.0

Wed Jul 15 21:56:18 2020 EDT

This code is a part of Qiskit

© Copyright IBM 2017, 2020.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
 of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.


## Comparing the harmonic structure of different biosignal modalities

```python
#import sys
#sys.path.append(os.path.join'C:\Users\Dell\GitHub\biotuner\biotuner')
import numpy as np
import biotuner
from biotuner.biotuner_utils import *
from biotuner.biotuner_functions import *
import matplotlib.pyplot as plt
from biotuner.biotuner2d import*
from biotuner.biotuner_object import *
import mne
from scipy import signal
import warnings
import neurokit2 as nk
warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', category=RuntimeWarning)
warnings.filterwarnings('ignore', category=FutureWarning)
warnings.filterwarnings('ignore', category=UserWarning)
```

    pygame 2.0.1 (SDL 2.0.14, Python 3.8.5)
    Hello from the pygame community. https://www.pygame.org/contribute.html

    C:\Users\Antoine\anaconda3\lib\site-packages\sympy\__init__.py:672: SymPyDeprecationWarning:

    importing sympy.core.power with 'from sympy import *' has been
    deprecated since SymPy 1.6. Use import sympy.core.power instead. See
    https://github.com/sympy/sympy/issues/18245 for more info.

      self.Warn(

## Comparing EEG with EMG

First, we load the EMG data and slice it windows of 3 seconds

```python
path = 'C:/Users/Dell/GitHub/biotuner/examples/data_examples/EMG'
EMG_data = np.load(path+'/EMG_data.npy')
EMG_sf = 2048
EMG_data = slice_data(EMG_data, EMG_sf, window=3)
```

Then, we load the EEG data

```python
path = 'C:/Users/Dell/GitHub/biotuner/examples/data_examples/'
EEG_data = np.load(path+'parei_data_1000ts.npy')
EEG_sf = 1000
EEG_data.shape
```

    (500, 9501)

and we downsample the data with the highest sampling frequency (MEG) to match EEG and MEG sampling frequencies

```python
EMG_data = resample_2d(EMG_data, EMG_sf, EEG_sf)
```

We equate the number of time series for the EEG and MEG data, and crop each EEG time series to equate the 3 second window that we have determined

```python
EEG_data, EMG_data = equate_dimensions(EEG_data, EMG_data)
data_all = [EEG_data, EMG_data]
conditions = ['EEG', 'EMG']
```

```python
title = 'Comparing harmonic similarity of spectral peaks for EEG and EMG data'
graph_conditions(data_all, sf = 1000, conditions=conditions, metric_to_graph='harmsim',
                 peaks_function='EEMD', precision=0.5,
                display=True, save=False, low_cut = 1, high_cut = 200, title=title)
```

```python
data_all = [EEG_data, EMG_data]
conditions = ['EEG', 'EMG']
title = 'Comparing number of spectral chords (3 notes) for EEG and EMG data'
graph_conditions(data_all, sf = 1000, conditions=conditions, metric_to_graph='n_spectro_chords',
                 peaks_function='fixed', precision=0.5, min_notes=3, cons_limit=0.1,
                display=True, save=False, low_cut = 1, high_cut = 200, title=title)
```

```python
data_all = [EEG_data[0:50], EMG_data[0:50]]

conditions = ['EEG', 'EMG']
graph_conditions(data_all, sf = 1000, conditions=conditions, metric_to_graph='n_IF_chords',
                 peaks_function='HH1D_max', precision=0.5, min_notes=3, cons_limit=0.1,
                display=True, save=False, low_cut = 1, high_cut = 200)
```

    EEG
    EMG

## Comparing EEG with iEEG

Load the EEG data

```python
path = 'data_examples/'
EEG_data = np.load(path+'parei_data_1000ts.npy')
EEG_sf = 1000
EEG_data.shape
```

    (500, 9501)

Load the iEEG data and slice it

```python
path = 'data_examples/'
data = mne.io.read_raw_edf(path + '/ieeg_amyg.edf', preload = True)
iEEG_data = data.load_data()
iEEG_data = iEEG_data.get_data()
iEEG_sf = 2000
iEEG_data = slice_data(iEEG_data, iEEG_sf, window=5)
iEEG_data.shape
```

    Extracting EDF parameters from C:\Users\Dell\GitHub\biotuner\examples\data_examples\ieeg_amyg.edf...
    EDF file detected
    Setting channel info structure...
    Creating raw.info structure...
    Reading 0 ... 1043999  =      0.000 ...   522.000 secs...

    (416, 10000)

Resample the data with highest sampling frequency and equate size of datasets

```python
iEEG_data = resample_2d(iEEG_data, 2000, 1000)
print(iEEG_data.shape)
EEG_data, iEEG_data = equate_dimensions(EEG_data, iEEG_data)
data_all = [EEG_data, iEEG_data]
conditions = ['EEG', 'iEEG']
```

    (416, 5000)

Compare **n_spectro_chords** between distributions (by default it uses the **EEMD** function)

```python
colors = ['yellow', 'orange']
title = 'Comparing number of spectral chords (3 notes) for EEG and iEEG data'
graph_conditions(data_all, sf = 1000, conditions=conditions, metric_to_graph='n_spectro_chords', peaks_function='fixed', precision=0.5,
                display=True, save=False, low_cut = 1, high_cut = 250, min_notes = 2, cons_limit=0.1, colors=colors, title=title)
```

Compare **harmonic similarity** between distributions using the **EEMD** function

```python
data_all = [EEG_data, iEEG_data]
conditions = ['EEG', 'iEEG']
colors = ['yellow', 'darkorange']
title = 'Comparing harmonic similarity for EEG and iEEG using Empirical Mode Decomposition '
graph_conditions(data_all, sf = 1000, conditions=conditions, metric_to_graph='harmsim', peaks_function='EEMD', precision=0.5,
                display=True, save=False, low_cut = 1, high_cut = 250, min_notes = 2, cons_limit=0.1, colors=colors, title=title)
```

We compare the distribution of averaged peaks values using EEMD function for EEG and iEEG signals and show a significant difference. This effect, combined with the non-significant effect of when comparing harmonic similarity with EEMD function points to the fact that the levels of consonance are not directly dependant on the averaged frequency values

```python
data_all = [EEG_data, iEEG_data]
conditions = ['EEG', 'iEEG']
colors = ['yellow', 'darkorange']
title = 'Comparing average peaks frequency for EEG and iEEG using Empirical Mode Decomposition '
graph_conditions(data_all, sf = 1000, conditions=conditions, metric_to_graph='peaks', peaks_function='EEMD', precision=0.5,
                display=True, save=False, low_cut = 1, high_cut = 250, min_notes = 2, cons_limit=0.1, title=title, colors=colors)
```

## Comparing ECG with EEG

ECG data was obtained here: https://physionet.org/content/adfecgdb/1.0.0/

These abdominal electrocardiogram data were recorded during labor.

First we will load the EEG and ECG data and equate their sampling frequency and sizes

```python
#ECG
path = 'data_examples/ECG/'
data = mne.io.read_raw_edf(path + '/r01.edf', preload = True)
ECG_data = data.load_data()
ECG_data = ECG_data.get_data()
ECG_sf = 1000
ECG_data = slice_data(ECG_data[0:4], ECG_sf, window=5)
ECG_data.shape
#EEG
path = 'data_examples/'
EEG_data = np.load(path+'parei_data_1000ts.npy')
EEG_sf = 1000
EEG_data.shape

EEG_data, ECG_data = equate_dimensions(EEG_data, ECG_data)
data_EEG_ECG = [EEG_data, ECG_data]
conditions = ['EEG', 'ECG']
```

    Extracting EDF parameters from C:\Users\Dell\GitHub\biotuner\examples\data_examples\ECG\r01.edf...
    EDF file detected
    Setting channel info structure...
    Creating raw.info structure...
    Reading 0 ... 299999  =      0.000 ...   299.999 secs...

We see that the **harmonic similarity** metric using the **EEMD** method doesn't allow to distinguish between the two types of signals

```python
data_EEG_ECG = [EEG_data, ECG_data]
colors = ['white', 'deeppink']
title = 'Comparing harmonic similarity of peaks for EEG and ECG data using EEMD'
graph_conditions(data_EEG_ECG, sf = 1000, conditions=conditions, metric_to_graph='harmsim',
                 peaks_function='EEMD', precision=0.5, display=True, save=False, low_cut = 1,
                 high_cut = 250, min_notes = 2, cons_limit=0.1, colors=colors, title=title)
```

However, the **n_spectro_chords** metric using the **EEMD** method allows to distinguish between the two types of signals

```python
data_EEG_ECG = [EEG_data, ECG_data]
colors = ['white', 'deeppink']
title = 'Comparing number of spectral chords of peaks for EEG and ECG data using EEMD'
FREQ_BANDS = [[1, 2], [2, 4], [4, 8], [8, 16], [16, 32], [32, 64]]
graph_conditions(data_EEG_ECG, sf = 1000, conditions=conditions, metric_to_graph='n_spectro_chords',
                 peaks_function='fixed', precision=0.5, display=True, save=False, low_cut = 1,
                 high_cut = 250, min_notes = 3, cons_limit=0.1, FREQ_BANDS = FREQ_BANDS,
                colors=colors, title=title)
```

We reproduce the result using the ECG from another participant

```python
#ECG
path = 'data_examples/ECG/'
data_ = mne.io.read_raw_edf(path + '/r10.edf', preload = True)
ECG_data = data_.load_data()
ECG_data = ECG_data.get_data()
ECG_sf = 1000
ECG_data = slice_data(ECG_data[0:4], ECG_sf, window=5)
ECG_data.shape
#EEG
path = 'data_examples/'
EEG_data = np.load(path+'parei_data_1000ts.npy')
EEG_sf = 1000
EEG_data.shape

EEG_data, ECG_data = equate_dimensions(EEG_data, ECG_data)
data_EEG_ECG = [EEG_data, ECG_data]
conditions = ['EEG', 'ECG']
```

    Extracting EDF parameters from C:\Users\Dell\GitHub\biotuner\examples\data_examples\ECG\r10.edf...
    EDF file detected
    Setting channel info structure...
    Creating raw.info structure...
    Reading 0 ... 299999  =      0.000 ...   299.999 secs...

```python
data_EEG_ECG = [EEG_data, ECG_data]
colors = ['white', 'deeppink']
title = 'Comparing number of spectral chords of peaks for EEG and ECG data using EEMD'
FREQ_BANDS = [[1, 2], [2, 4], [4, 8], [8, 16], [16, 32], [32, 64]]
graph_conditions(data_EEG_ECG, sf = 1000, conditions=conditions, metric_to_graph='n_spectro_chords',
                 peaks_function='fixed', precision=0.5, display=True, save=False, low_cut = 1,
                 high_cut = 250, min_notes = 2, cons_limit=0.1, FREQ_BANDS = FREQ_BANDS,
                colors=colors, title=title)
```

## Comparing ECG signal with simulated ECG signal

We generate an ECG dataset that matches the dimensions and properties of a real ECG dataset

```python
print(ECG_data.shape)
ECG_sim = generate_ecg_dataset(duration=5, sf=1000, n_trials=240,noise_amp=0.01, artifacts_amp=0.01)
ECG_sim_noise = generate_ecg_dataset(duration=5, sf=1000, n_trials=240,noise_amp=0.4, artifacts_amp=0.4)
sim_noise = generate_ecg_dataset(duration=5, sf=1000, n_trials=240,noise_amp=0.4, artifacts_amp=0.4, mode='noise')
print(ECG_sim.shape)
print(ECG_sim_noise.shape)
data_sim = [ECG_data, ECG_sim, ECG_sim_noise, sim_noise]
conditions = ['ECG', 'Sim clean', 'Sim noisy', 'Noise']
```

    (240, 5000)
    (240, 5000)
    (240, 5000)

When using fixed frequency bands spanning one octave each, the simulated signal has higher harmonic similarity between peaks

```python
data_sim = [ECG_data, ECG_sim, ECG_sim_noise]
conditions = ['ECG', 'Sim clean', 'Sim noisy']
FREQ_BANDS = [[1, 2], [2, 4], [4, 8], [8, 16], [16, 32], [32, 64]]
graph_conditions(data_sim, sf = 1000, conditions=conditions, metric_to_graph='tenney',
                 peaks_function='harmonic_peaks', precision=0.5, display=True, save=False, low_cut = 1,
                 high_cut = 250, min_notes = 2, cons_limit=0.1, FREQ_BANDS = FREQ_BANDS)
```

```python
data_sim = [ECG_data, ECG_sim, ECG_sim_noise]
conditions = ['ECG', 'Sim clean', 'Sim noisy']
FREQ_BANDS = [[1, 2], [2, 4], [4, 8], [8, 16], [16, 32], [32, 64]]
graph_conditions(data_sim, sf = 1000, conditions=conditions, metric_to_graph='harmsim',
                 peaks_function='EEMD', precision=0.5, display=True, save=False, low_cut = 1,
                 high_cut = 250, min_notes = 2, cons_limit=0.1, FREQ_BANDS = FREQ_BANDS)
```

### We will compare simulated signal ('sim_clean') with simulated signal+noise ('sim_noisy') with the noise component alone ('noise')

This will allow to see if the signal+noise differs from the noise alone
We can see that the simulated signal has less variable consonance values as compared to signal+noise.
We also can see that the **signal+noise differs from the noise component alone**, which points towards the fact that the **consonance metrics grasp information from the signal itself.**

```python
data_sim = [ECG_sim_noise, ECG_sim, sim_noise]
conditions = ['Sim noisy', 'Sim clean', 'Noise']
colors = ['deeppink', 'cyan', 'orange', 'white']
graph_conditions(data_sim, sf = 1000, conditions=conditions, metric_to_graph='harmsim',
                 peaks_function='EEMD', precision=0.5, display=True, save=False, low_cut = 1,
                 high_cut = 250, min_notes = 2, cons_limit=0.1, colors=colors)
```

```python
title = 'Comparing number of spectral chords (4 notes) for simulated ECG data and noise component'
data_sim = [ECG_sim_noise[0:150], ECG_sim[0:150], sim_noise[0:150]]
conditions = ['Sim noisy', 'Sim clean', 'Noise']
colors = ['deeppink', 'cyan', 'orange', 'white']
graph_conditions(data_sim, sf = 1000, conditions=conditions, metric_to_graph='n_spectro_chords',
                 peaks_function='fixed', precision=0.5, display=True, save=False, low_cut = 1,
                 high_cut = 250, min_notes = 4, cons_limit=0.1, colors=colors, title=title)
```

```python

```

    (500, 500)
    (500, 500)
    (500, 600)
    (500, 600)

```python

```