```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

# Toggle cell visibility

from IPython.display import HTML
tag = HTML('''
Promijeni vidljivost <a href="javascript:code_toggle()">ovdje</a>.''')
display(tag)

# Hide the code completely

# from IPython.display import HTML
# tag = HTML('''<style>
# div.input {
#     display:none;
# }
# </style>''')
# display(tag)
```

Promijeni vidljivost ovdje.

```python
%matplotlib notebook

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
import sympy as sym

from ipywidgets import widgets, Layout
from ipywidgets import interact

from IPython.display import Latex, display, Markdown # For displaying Markdown and LaTeX code

from matplotlib import patches
```

## Sustav upravljanja položajem azimuta antene

Primjer jednostavnog sustava upravljanja položajem azimuta antene shematski je prikazan na slici ispod (lijevo). Cilj ovog upravljačkog sustava je zadržati referentni (željeni) položaj antene postavljanjem željenog kuta $\theta_{ref}$ referentnim potenciometrom (RP). Stoga, blok-dijagram ovog sustava (prikazan na slici dolje desno) započinje signalom $\theta_{ref}$, koji se pretvara u napon $U_1$. Napon $U_2$ tada se oduzima od napona $U_1$. $U_2$ je izlaz iz mjernog potenciometra (MP) koji pruža informacije o stvarnom kutu. Razlika napona $U_1-U_2$ predstavlja pogrešku koja nam kazuje koliko se stvarni kut razlikuje od željenoga. Na temelju ove pogreške kontroler upravlja elektromotorom koji (pomoću zupčanika) zakreće antenu s ciljem smanjenja iznosa pogreške. $d_w$ predstavlja smetnju uslijed vjetra koji nasumično zakreće antenu.

        Shematski prikaz sustava upravljanja položajem azimuta antene
        Blok-dijagram sustava upravljanja položajem azimuta antene

        Legenda: RP-referentni potenciometar, MP-mjerni potenciometar, dw-smetnja uzrokovana vjetrom.

---

### Kako koristiti ovaj interaktivni primjer?

- Upotrijebite klizače za promjenu vrijednosti željenog kuta/azimuta antene ($\theta_{ref}$), smetnje uzrokovane vjetrom ($d_w$), te proporcionalnog ($K_p$), integracijskog ($K_i$) i derivacijskog ($K_d$) koeficijenta podešavanja.

- Aktivirajte gumbe za promjenu proporcionalnog (P), proporcionalno-integracijskog (PI) i proporcionalno-integracijsko-derivacijskog (PID) tipa kontrolera.

---

### Napomene

- Veličina crvene strelice na shematskom prikazu antene proporcionalna je veličini smetnje uzrokovane vjetrom ($d_w$), dok smjer strelice predstvlja smjer dotične smetnje.
- Isprekidana plava crta na shematskom prikazu antene označava stvarni kut antene.
- Isprekidana zelena crta na shematskom prikazu antene označava željeni kut antene.
- Isprekidano-točkasta crvena crta na crtici na shematskom prikazu antene označava prethodni stvarni kut antene.

Moguće je izabrati između dvije ponuđene opcije za prikaz rezultata:
1. Resetiranje shematskog prikaza kada se promijeni tip kontrolera.
2. Resetiranje grafa kada se promijeni tip kontrolera.

```python
# define system constants
_Kpot = 0.318

_K1 = 100
_a = 100
_Km = 2.083
_am = 1.71
_Kg = 0.1
_R = 8
_Kt = 0.5
_Tv = 200 #in milliseconds

#set current theta and theta reference:
th = [0,0,0,0,0,0]
thref = [0,0,0,0,0,0]
# disturbance:
m = [0,0,0,0,0,0]
#joined together (first theta reference, second disturbance, then theta measured):
variables = [thref, m, th]

# variables of controller:
_K = 1
_taui = 1
_taud = 1

```

```python
# symbolic calculus:
taui, taud, K, s, z = sym.symbols('taui, taud, K, s, z')

_alpha=0.1
#controller:
P = K
I = K/(taui*s)
D = K*taud*s/(_alpha*taud*s+1)

def make_model(controller):
    if controller == 'P':
        C = P
    elif controller == 'PI':
        C = P+I
    elif controller == 'PID':
        C = P+I+D
    else:
        print('Tip kontrolera ne nalazi se u koristenoj knjiznici')

    tf_s = C*_K1*_Km*_Kg*_Kpot/(s*(s+_a)*(s+_am)+C*_K1*_Km*_Kg*_Kpot)
    tf_s = tf_s.simplify()

    tf_z = tf_s.subs(s,2/(_Tv/1000)*(z-1)/(z+1))
    tf_z = tf_z.simplify()

    num = [sym.fraction(tf_z.factor())[0].expand().coeff(z, i) for i in reversed(range(1+sym.degree(sym.fraction(tf_z.factor())[0], gen=z)))]
    den = [sym.fraction(tf_z.factor())[1].expand().coeff(z, i) for i in reversed(range(1+sym.degree(sym.fraction(tf_z.factor())[1], gen=z)))]
    #print(num)
    #print(den)

    tf_sM = _Km*_Kg*_R*(s+_a)/(s*(s+_a)*(s+_am)*_Kt+C*_K1*_Km*_Kg*_Kpot*_Kt)

    tf_zM = tf_sM.subs(s,2/(_Tv/1000)*(z-1)/(z+1))
    tf_zM = tf_zM.simplify()
    num_M = [sym.fraction(tf_zM.factor())[0].expand().coeff(z, i) for i in reversed(range(1+sym.degree(sym.fraction(tf_zM.factor())[0], gen=z)))]
    #print(num_M)
    #print(den_M)

    #print('\n........finished........')
    return sym.lambdify((K, taui, taud), [np.array(num), -np.array(num_M), -np.array(den)])

z_transform_p = make_model('P')
z_transform_pi = make_model('PI')
z_transform_pid = make_model('PID')
```

```python
def calculate_next(z_transform):
    variables[-1][0] = 0 # set current to zero
    z_transform = z_transform(_K, _taui, _taud)

    temp = 0
    for i in range(len(z_transform)): # for every polynomial
        for j in range(len(z_transform[i])): # for every term in polynomial
            temp += z_transform[i][j] * variables[i][j]

    return temp / z_transform[-1][0]*(-1)
```

```python
fig = plt.figure(figsize=(9.8, 4),num='Sustav upravljanja položajem azimuta antene')
# add axes
ax = fig.add_subplot(121)
graph = fig.add_subplot(122)

#set current theta and theta reference:
th = [0,0,0,0,0,0]
thref = [1,0,0,0,0,0]
# disturbance:
m = [.1,0,0,0,0,0]
#joined together (first theta reference, second disturbance, then theta measured):
variables = [thref, m, th]

# variables of controller:
_K = 20
_taui = 10
_taud = 1

new_flag_value = [True, 0] # flag for displaying old value of th, before th_ref was changed [flag, angle]

#slider widgets:
th_ref_widget = widgets.FloatSlider(value=variables[0][0],min=0.0,max=2*np.pi,step=.01,description=r'\(\theta_{ref} \) [rad]',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
m_widget = widgets.FloatSlider(value=variables[1][0],min=-.3,max=.3,step=.01,description=r'\(d_{w} \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
K_widget = widgets.FloatSlider(value=_K,min=0.0,max=40,step=.1,description=r'\(K_p \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.1f')
taui_widget = widgets.FloatSlider(value=_taui,min=0.01,max=60,step=.01,description=r'\(K_i \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
taud_widget = widgets.FloatSlider(value=_taud,min=0.0,max=5,step=.1,description=r'\(K_d \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
#interact(set_coefficients, setK=K_widget, setthref=th_ref_widget, setm=m_widget, settaui=taui_widget, settaud=taud_widget)

#checkboxes
#checkbox_reset_antenna = widgets.Checkbox(value=False, description='Reset schematic representation of antenna when type of controller is changed', disabled=False)
#checkbox_reset_graph = widgets.Checkbox(value=False, description='Reset graph when type of controller is changed', disabled=False)

checkbox_reset_antenna = widgets.Checkbox(value=False, disabled=False, layout=Layout(width='100px'))
label_scheme = widgets.Label('Resetiranje shematskog prikaza kada se promijeni tip kontrolera', layout=Layout(width='500px'))
box1 = widgets.HBox([checkbox_reset_antenna, label_scheme])

checkbox_reset_graph = widgets.Checkbox(value=False, disabled=False, layout=Layout(width='100px'))
label_graph = widgets.Label('Resetiranje grafa "Kut/azimut antene vs. vrijeme" kada se promijeni tip kontrolera', layout=Layout(width='500px'))
box2 = widgets.HBox([checkbox_reset_graph, label_graph])

style = {'description_width': 'initial'}

#buttons:
def buttons_clicked(event):
    global controller_type, equation, list_th, list_th_ref, list_time
    controller_type = buttons.options[buttons.index]
    if controller_type =='P':
        taui_widget.disabled=True
        taud_widget.disabled=True
        equation = '$Kp$'
    if controller_type =='PI':
        taui_widget.disabled=False
        taud_widget.disabled=True
        equation = '$Kp\,(1+\dfrac{1}{T_{i}\,s})$'
    if controller_type =='PID':
        taui_widget.disabled=False
        taud_widget.disabled=False
        equation = '$Kp\,(1+\dfrac{1}{T_{i}\,s}+\dfrac{T_{d}\,s}{a\,T_{d}\,s+1})$'
    if checkbox_reset_antenna.value:
        #reset values to zero:
        for i in range(len(variables)):
            for j in range(1, len(variables[i])):
                variables[i][j] = 0
        variables[-1][0] = 0
    if checkbox_reset_graph.value:
        list_th = []
        list_th_ref = []
        list_time = []

buttons = widgets.ToggleButtons(
    options=['P', 'PI', 'PID'],
    description='Odaberi tip kontrolera:',
    disabled=False,
    style=style)
buttons.observe(buttons_clicked)

#updating values
def set_values(event):
    global _K, _taui, _taud
    if event['name'] != 'value':
        return
    if th_ref_widget.value != variables[0][0] and not new_flag_value[0]:
        new_flag_value[0] = True
        new_flag_value[1] = variables[-1][0]

    variables[0][0] = th_ref_widget.value
    variables[1][0] = m_widget.value
    _K = K_widget.value
    _taui = taui_widget.value
    _taud = taud_widget.value
th_ref_widget.observe(set_values)
m_widget.observe(set_values)
K_widget.observe(set_values)
taui_widget.observe(set_values)
taud_widget.observe(set_values)

#displaying widgets:
display(buttons)
vbox1 = widgets.VBox([th_ref_widget, m_widget, K_widget, taui_widget, taud_widget])
vbox2 = widgets.VBox([box1, box2])
hbox = widgets.HBox([vbox1, vbox2])
display(hbox)

#setting at start:
controller_type = 'P'
taui_widget.disabled=True
taud_widget.disabled=True
equation = '$Kp$'
set_values({'name':'value'})

#lists for graph in time:
list_time = []
list_th = []
list_th_ref = []

#previous th before change of th_ref:
prev_th = 0

cycles_flag = True

def update_figure(i_time):
    global cycles_flag, variables, _K, controller_type, equation

    if cycles_flag == True:
        cycles_flag = False
        return

    if controller_type == 'P':
        th = calculate_next(z_transform_p)
    elif controller_type == 'PI':
        th = calculate_next(z_transform_pi)
    elif controller_type == 'PID':
        th = calculate_next(z_transform_pid)
    variables[-1][0] = th

    # save variables for next time step:
    for i in range(len(variables)):
        for j in reversed(range(len(variables[i])-1)):
            variables[i][j+1] = variables[i][j]

    list_time.append((i_time+1)*_Tv/1000)
    list_th.append(th)
    list_th_ref.append(variables[0][0])

    #plot:
    ax.clear()
    ax.plot([-1.5, 1.5, 1.5, -1.5], [-1.5, -1.5, 1.5, 1.5], ',', color='b')

    #plot line:
    ax.plot([np.cos(th)*-.5, np.cos(th)*1.5], [np.sin(th)*-.5, np.sin(th)*1.5], 'b--', linewidth=.7, alpha=.7)

    #plot antenna:
    center1 = 1
    center2 = 3
    d1 = 2.2
    d2 = 5.5
    x1 = center1*np.cos(th)
    y1 = center1*np.sin(th)
    x2 = center2*np.cos(th)
    y2 = center2*np.sin(th)
    arc1 = patches.Arc((x1, y1), d1, d1,
                 angle=th/np.pi*180+180, theta1=-58, theta2=58, linewidth=2, color='black', alpha=.7)
    arc2 = patches.Arc((x2, y2), d2, d2,
                 angle=th/np.pi*180+180, theta1=-20, theta2=20, linewidth=2, color='black', alpha=.7)
    ax.add_patch(arc1)
    ax.add_patch(arc2)
    if m_widget.value > 0:
        ax.plot(0, 0, 'r', alpha=.1, marker=r'$\circlearrowright$',ms=150*m_widget.value)
    elif m_widget.value < 0:
        ax.plot(0, 0, 'r', alpha=.1, marker=r'$\circlearrowleft$',ms=-150*m_widget.value)
    ax.set_title('Shematski prikaz antene')

    #plot direction of antenna before thref change
    if abs(variables[0][0] - th) < 0.03:
        new_flag_value[0] = False
    if new_flag_value[0]:
        ax.plot([0,np.cos(new_flag_value[1])], [0, np.sin(new_flag_value[1])], 'r-.', alpha=.3, linewidth=0.5)
    #plot desired direction of antenna
    ax.plot([0,np.cos(variables[0][0])], [0, np.sin(variables[0][0])], 'g-.', alpha=.7, linewidth=0.7)

    ax.text(-1, 1.3, 'stvarni kut: %.2f rad' %th)
    ax.text(-1, -1.3, 'tip kontrolera:')
    ax.text(-1, -1.6, equation)

    ax.set_aspect('equal', adjustable='datalim')
    ax.set_xlim(-1.5,1.5)
    ax.set_ylim(-1.5,1.5)
    ax.axis('off')

    graph.clear()
    graph.plot(list_time, list_th_ref, 'g', label='željeni kut')
    graph.plot(list_time, list_th, 'b', label='stvarni kut')
    graph.set_xlabel('$t$ [s]')
    graph.set_ylabel('$\\theta$ [rad]')
    graph.legend(loc=4, fontsize=8)
    graph.set_title('Kut/azimut antene vs. vrijeme')

    plt.show()

ani = animation.FuncAnimation(fig, update_figure, interval=_Tv)
```

    <IPython.core.display.Javascript object>

    ToggleButtons(description='Odaberi tip kontrolera:', options=('P', 'PI', 'PID'), style=ToggleButtonsStyle(desc…

    HBox(children=(VBox(children=(FloatSlider(value=1.0, description='\\(\\theta_{ref} \\) [rad]', max=6.283185307…

```python

```


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

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

__copyright_amp__ = "(c) 2014-2020, The Psi4Julia Developers"
__license__   = "BSD-3-Clause"
__date__      = "2020-08-03"
```

    "2020-08-03"

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

In this tutorial, we will implement the orbital-optimized second-order Moller-Plesset 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, NumPy, TensorOperations, and LinearAlgebra 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.

```julia
# ==> Import Psi4, NumPy, & TensorOperations <==
using PyCall: pyimport
psi4 = pyimport("psi4")
np   = pyimport("numpy") # used only to cast to Psi4 arrays
using TensorOperations: @tensor
import LinearAlgebra
eye(n) = LinearAlgebra.I(n)
using Formatting: printfmt

# ==> Set Basic Psi4 Options <==

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

# Output options
psi4.core.set_output_file("output.dat", false)
```

      Memory set to   1.863 GiB by Python driver.

We now define the molecule and set Psi4 options:

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

psi4.set_options(Dict("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:

```julia
# 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
println("\nSize of the ERI tensor will be $I_size GB.")
memory_footprint = I_size * 1.5
if I_size > numpy_memory
    psi4.core.clean()
    throw(OutOfMemoryError("Estimated memory utilization ($memory_footprint GB) exceeds " *
                           "allotted memory limit of $numpy_memory GB."))
end
```

    Size of the ERI tensor will be 0.00022848800000000003 GB.

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

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

    1.0e-8

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

```julia
# 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 = 2nbf                 # Total number of spin orbitals
nvirt = nso - nocc         # Number of virtual orbitals
```

    16

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.

```julia
"""
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
"""
function spin_block_tei(I)
    identity = eye(2)
    I = np.kron(identity, I)
    np.kron(identity, permutedims(I, reverse(1:4)))
end

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 = permutedims(I_spinblock, (1, 3, 2, 4)) - permutedims(I_spinblock, (1, 3, 4, 2));
```

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

```julia
# ==> 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 = kron(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 Julia 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 `sortperm()`:

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

# Get coefficients, block, and sort
Ca = np.asarray(scf_wfn.Ca())
Cb = np.asarray(scf_wfn.Cb())
C = [Ca zero(Ca); zero(Cb) Cb]; # direct sum

# Sort the columns of C according to the order of orbital energies
C = C[:, sortperm(eps)];
```

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:

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

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

    @tensor begin
       hmo[P,Q] := hao[p,Q] * C[p,P]
       hmo[p,Q] := hmo[p,q] * C[q,Q]
    end
end

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

   @tensor begin
      gmo[P,Q,R,S] := gao[p,Q,R,S] * C[p,P]
      gmo[p,Q,R,S] := gmo[p,q,R,S] * C[q,Q]
      gmo[p,q,R,S] := gmo[p,q,r,S] * C[r,R]
      gmo[p,q,r,S] := gmo[p,q,r,s] * C[s,S]
   end
end

# 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:
~~~julia
gmo[v, v, o, o]
~~~

```julia
# Make slices
x = [CartesianIndex()]
o = [p ≤ nocc for p in 1:nso]
v = [p > nocc for p in 1:nso];
```

**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:
   ~~~julia
   @tensor t2[a,b,i,j] := (fprime[v, v])[a,c] * t_amp[c,b,i,j]
   ~~~
   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:
   ~~~julia
   t2 = t2 - permutedims(t2, (2, 1, 3, 4))
   ~~~
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:
    ~~~julia
    @tensor (opdm_corr[v, v])[b,a] :=  0.5(permutedims(t_amp, reverse(1:4)))[i,j,a,c] * t_amp[b,c,i,j]
    @tensor (opdm_corr[o, o])[j,i] := -0.5(permutedims(t_amp, reverse(1:4)))[j,k,a,b] * t_amp[a,b,i,k]
    ~~~

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:
    ~~~julia
    X[v, o] = ((F - F')[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.

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

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

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

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

# Initialize the rotation matrix parameter
E_OMP2 = let hmo=hmo, gmo=gmo, tpdm_corr=tpdm_corr, opdm_corr=opdm_corr, opdm_ref=opdm_ref,
   E_OMP2_old = E_OMP2_old, t_amp=t_amp, C=C

   E_OMP2 = 0.0
   X = zeros(nso, nso)

   for iteration in 1:MAXITER

       # Build the Fock matrix
       @tensor f[p,q] := hmo[p,q] + (gmo[:, o, :, o])[p,i,q,i]

       # Build off-diagonal Fock Matrix and orbital energies
       fprime = copy(f)
       fprime[LinearAlgebra.diagind(fprime)] .= 0
       eps = LinearAlgebra.diag(f)

       # Update t amplitudes
       t1 = @view gmo[v, v, o, o]
       @tensor t2[a,b,i,j] := (fprime[v, v])[a,c] * t_amp[c,b,i,j]
       @tensor t3[a,b,i,j] := (fprime[o, o])[k,i] * t_amp[a,b,k,j]
       t_amp = t1 .+ t2 .- permutedims(t2, (2, 1, 3, 4)) .-
               t3 .+ permutedims(t3, (1, 2, 4, 3))

       # 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
       @tensor (opdm_corr[v, v])[b,a] :=  0.5(permutedims(t_amp, reverse(1:4)))[i,j,a,c] * t_amp[b,c,i,j]
       @tensor (opdm_corr[o, o])[j,i] := -0.5(permutedims(t_amp, reverse(1:4)))[j,k,a,b] * t_amp[a,b,i,k]
       opdm = opdm_corr + opdm_ref

       # Build two particle density matrix
       tpdm_corr[v, v, o, o] = t_amp
       tpdm_corr[o, o, v, v] = permutedims(t_amp, reverse(1:4))
       @tensor tpdm2[r,s,p,q] := opdm_corr[r,p] * opdm_ref[s,q]
       @tensor tpdm3[r,s,p,q] := opdm_ref[r,p] * opdm_ref[s,q]
       tpdm = tpdm_corr +
           tpdm2 - permutedims(tpdm2, (2, 1, 3, 4)) -
           permutedims(tpdm2, (1, 2, 4, 3)) + permutedims(tpdm2, (2, 1, 4, 3)) +
           tpdm3 - permutedims(tpdm3, (2, 1, 3, 4))

       # Newton-Raphson step
       @tensor F[p,q] := hmo[p,r] * opdm[r,q] + 0.5gmo[p,r,s,t] * tpdm[s,t,q,r]
       X[v, o] = ((F - F')[v, o]) ./ (- eps[v, x] .+ eps[x, o])

       # Build Newton-Raphson orbital rotation matrix
       U = exp(X - X')

       # Rotate spin-orbital coefficients
       C = C * 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 + @tensor scalar(hmo[p,q] * opdm[q,p]) +
                  1/4 * @tensor scalar(gmo[p,q,r,s] * tpdm[r,s,p,q])
       printfmt("OMP2 iteration: {1:3d} Energy: {2:15.8f} dE: {3:2.5e}\n", iteration, E_OMP2, E_OMP2-E_OMP2_old)

       abs(E_OMP2-E_OMP2_old) < E_conv && break

       # Updating values
       E_OMP2_old = E_OMP2
   end
   E_OMP2
end
```

    OMP2 iteration:   1 Energy:    -76.09603493 dE: -7.60960e+01
    OMP2 iteration:   2 Energy:    -76.09617336 dE: -1.38424e-04
    OMP2 iteration:   3 Energy:    -76.09618834 dE: -1.49844e-05
    OMP2 iteration:   4 Energy:    -76.09619159 dE: -3.24371e-06
    OMP2 iteration:   5 Energy:    -76.09619246 dE: -8.72489e-07
    OMP2 iteration:   6 Energy:    -76.09619281 dE: -3.48877e-07
    OMP2 iteration:   7 Energy:    -76.09619295 dE: -1.38242e-07
    OMP2 iteration:   8 Energy:    -76.09619301 dE: -6.80802e-08
    OMP2 iteration:   9 Energy:    -76.09619305 dE: -3.19299e-08
    OMP2 iteration:  10 Energy:    -76.09619306 dE: -1.65530e-08
    OMP2 iteration:  11 Energy:    -76.09619307 dE: -8.24744e-09

    -76.09619307058347

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

```julia
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.


# Resumen
Este cuaderno digital interactivo tiene como objetivo evaluar las relaciones y procesos básicos entre la radiación incidente.

Para ello haremos uso de modelos de simulación, en particular de modelos de transferencia radiativa tanto a nivel de hoja individual como a nivel de dosel vegetal.

# Instrucciones
Lee con detenimiento todo el texto, y sigue sus instrucciones.

Una vez leida cada sección de texto ejecuta la celda de código siguiente (marcada como `In []`) presionando el icono de `Run`/`Ejecutar` o presionando en el teclado ALT + ENTER. Aparecerá una interfaz gráfica con la que poder realizar las tareas asignadas.

Como ejemplo ejectuta la siguiente celda para importar todas  las librerías necesarias para el correcto funcionamiento del cuaderno. Una vez ejecutada debería aparecer un mensaje de agradecimiento.

```python
%matplotlib inline
from ipywidgets import interact, interactive, fixed
from IPython.display import display
from functions import radiation_and_available_energy as fn
import numpy as np
```

# Ecuación básica
La evapotranspiración es básicamente un intercambio de energía y vapor de agua entre la superficie y la atmósfera. Es por ello que una variable fundamental para su cálculo es la estimación de la energía disponible de la superficie.

.

Seǵun el estado hídrico de la superficie, esta energía disponible es utilizada tanto en evaporar el agua (la ET o $\lambda E$) como en aumentar su temperatura ($H$).

$$\lambda E + H = R_n - G$$

donde $G$ es el calor transmitido por conducción y almacenado en el suelo y $R_n$ es la radiación neta

La radiación neta ($R_n$) puede calcularse como:

\begin{equation}
R_n = S\downarrow \left(1 - \alpha\right) +  \epsilon \left(L\downarrow - \sigma LST^4\right)
\end{equation}

donde $S\downarrow$ y $L\downarrow$ son las radiaciones entrantes de onda corta y larga respectivamente y pueden obtenerse a través de estaciones o modelos meteorológicos, $\alpha$ y $\epsilon$ son el albedo y la emisividad del superficie, $LST$ es la temperatura de superficie, y $\sigma\approx5.67E-8$ (W/m²K⁴) es la constante de Boltzman. $\alpha$, $\epsilon$ y $LST$ pueden estimarse con mayor o menor grado de incertidumbre mediante datos y productos de teledetección.

Teniendo en cuenta la mayor magnitud de la radiación de onda corta ($S$) con respecto a la onda larga ($L$), en este cuaderno nos centraremos sobre todo a las propiedades del dosel vegetal que influyen en el albedo y en la fracción de radiación fotosintéticamente activa absorbida por la vegetación.

Por otro lado $G$ suele ser de menor magnitud, tendiendo a 0 cuando trabajamos en escalas diarias o más largas, o estimándose como una porcentaje de la radiación neta ($G \approx 0.1 R_n$ para supeficies vegetales densas; $G \approx 0.4 R_n$ para suelos desnudos) para escalas horarias o instantáneas.

# La ley de Beer-Lambert
La ley de Beer-Lambert es la ley física fundamental que permite describir e interpretar la mayoría de los procesos y fenómenos radiativos que nos interesan. Cuando un haz de luz atraviesa un medio que contiene un elemento absorbente de radiación, esta luz se atenúa de manera proporcional a la concentración del elemento ($\kappa$) y la longitud del camino que recorre la luz ($\ell$). La transmisión de luz o de radiación electromagnética:

\begin{equation}
T = e^{-\kappa\ell}
\end{equation}

En el siguiente gráfico interactivo podrás ver el efecto de esta ley.

Puedes modificar los valores de absorción $\kappa$, que aumenta cuanta mayor sea la concentración del material abosrbente en el medio. El gráfico mostrará el porcentaje de luz que se transmite a través del medio conforme su recorrido se va haciendo más largo:

```python
w_lambert = interactive(fn.beer_lambert_law, kappa=fn.w_kappa, length=fixed(np.linspace(0, 10, 50)))
display(w_lambert)
```

Esta ley permite explicar por ejemplo la atenuación de la radiación solar desde la estratosfera hasta la superficie de la tierra: El aire está formado por gases y partículas que abosrben radiación (es más, la mayoría absorben sólo radiación en ciertas longides de onda, como es el ozono que abosrbe rayos UV). Cuanto más alto esté el sol en el cielo, el recorrido a través de la atmósfera es menor y por tanto la radiación solar que nos llega a la superficie será mayor.

.

## Aplicación a la Fracción de Radiación Interceptada (fIPAR)
La ley de Beer-Lambert nos permite también analizar la interceptación (y absorción) de radiación del dosel vegetal y equivalentemente la radiacion que finalmente llega al suelo.

Asumiendo que un dosel vegetal está formado en su mayoría por hojas, y que éstas interceptan y absorben radiación (sobre todo radiación fotosintéticamente activa, PAR), podemos ver cuánta radiación es inteceptada según la cantidad de hojas o la densidad foliar del dosel. Ésta suele definirse mediante el Índice de Área Foliar (LAI), la superficie de hojas por unidad de superficie de suelo. Cuanto mayor sea el LAI más probabilidades hay de que los rayos solares sean interceptados por la vegetación, y por tanto lleguen menos al suelo. De igual modo, el ángulo de elevación solar $\beta$, o su complemntario el ángulo zenital solar (SZA), juega también un papel importante, ya que cuanto más bajo esté el sol máyor camino tendrá que recorrer la luz a través del dosel y mayor será su atenuación.

Como las hojas son elementos sólidos con un tamaño finito (al contrario que los gases atmosféricos), su orientación con respecto a la vertical también juega un rol importante, por lo que la ley de Beer-Lambert ha de modificarse para reflejar este fenómeno. En el siguiente gráfico puedes experimentar con este efecto. Puedes variar tanto la cantidad de hojas (en términos de LAI) como la orientación dominante de las hojas, desde hojas predominantemente verticales ($\approx$90º) a hojas predominantemente horizontales ($\approx$0º).

```python
w_fipar = interactive(fn.plot_fipar, lai=fn.w_lai, leaf_angle=fn.w_leaf_angle)
display(w_fipar)
```

Observa en el gráfico cómo las mayores atenuaciones de la luz (o lo que es lo mismo mayor fracción interceptada de radiación) ocurren siempre con ángulos de elevación solar menores, pero que esa tasa de atenuación depende en mayor medido de la cantidad de hojas y de su disposición angular.

El LAI depende en gran medida del desarrollo fenológico y del aumento de biomasa de la planta, mientras que su disposición angular suele ser específica de la especie, habiendo plantas con una disposición más erectófila (hojas predominantemente verticales) como pueden ser algunas gramíneas y plantas con una disposición más planófila (hojas predominantemente horizontales. Generalmente podemos asumir que especies erectófilas están más adaptadas a sobervivir en climas con altos niveles de insolación, con el fin de evitar condiciones extremas de irradiancia solar en las horas centrales del día, mientras que plantas planófilas tienen a desarrollarse en condiciones más de penumbra o de poca exposición, de manera que intercepten el máximo posible de radiación durante todo el día.

Entre esos dos extremos la mayoría de las plantas presentan una diposición angular de las hojas más o menos aleatoria. Para estos casos el cálculo de la interceptación de radiación se simplifica como:

\begin{equation}
fIPAR = 1 - \exp \left(\frac{-0.5 LAI}{\cos\theta_s}\right)
\end{equation}
siendo $\theta_s$ el ángulo zenital solar.

> Si tienes interés en cómo realizar los cálculos para cualquier otro tipo de disposición angular de las hojas y tienes conocimientos de programación, picha [aquí](https://github.com/hectornieto/pyTSEB/blob/6dd5dffff08bc1f08edb3e89b4a79879e348146b/pyTSEB/TSEB.py#L1587).

# El albedo
Como hemos comentado anteriormente el albedo ($\alpha$) es la variable fundamental que determina la cantidad de radiación solar que es absorbida por la superficie. Se definie como la proporción de radiación de onda corta que es reflejada por la superficie. La radiación neta de onda corta es un balance entre la radación de onda corta incidente ($S\downarrow$) y la saliente o reflejada ($S\uparrow = \alpha S\downarrow$)

Las propiedades espectrales de la superficie terrestre son claves en la determinación de su albedo, por ejemplo la nieve tiene valores muy altos de albedo ($\approx 0.9$), y por tanto reflejan la mayoría de la radiación solar. Los océanos por el contrario tienen valores muy bajos de albedo ($\approx0.05$) por lo que aborben la mayoría de la energía solar que les llega.

Las hojas, debido a la actividad fotosinténtica absorben una gran proporción luz por parte de las clorofilas y otros pigmentos, es por ello que también presentan valores relativamente bajos de albedo ($\approx0.15$). Por otro lado, el suelo según su composición mineralógica puede tener valores de albedo entre 0.20 y 0.40 o incluso más. Por tanto el albedo de un dosel vegetal dependerá sobre todo de la concentración de clorofilas de sus hojas pero también de la cantidad o densidad foliar del dosel, luego en menor medida del albedo del suelo, pero sólo en situaciones de escasa vegetación o estados fenológicos iniciales.

> Profundizaremos más sobre este tema en la práctica sobre el comportamiento espectral de la vegetación.

## Anisotropía de la vegetación
La mayoría de las superficies terrestres presentan cierta anisotropía a la hora de reflejar la radiación. Es decir, reflejan más radiación en unas direcciones que en otras. El caso más extremos son las superficies especulares (espejos y algunas láminas de agua y hielo) que reflejan la mayoría de la radiación en la dirección contraria a la posición del sol. El caso contrario son las superficies lambertianes, las  cuales dan la aparencia de reflejar por igual en todas las direcciones.

.

> La madera sin pulir es un ejemplo superficie lambertiana, mientras que la madera pulida y barnizada presenta por el contrario comportamientos especulares.

La vegetación, al estar formada prinpicalmente de un cojunto más o menos agrupado de hojas, no se libra de este comportamieno anisotrópico, que provoca que según las geometría de iluminación, es decir según al posición del sol, refleja de manera distinta según la dirección, e incluso pueda variar el albedo global.

En el siguiente gráfico podrás ver este efecto. Se trata de un gráfico polar que simula la reflectividad de una superficie vegetal en todas direcciones, tanto para la región del PAR como para todo el espectro solar. Los anillos concéntricos representan los distintos ángulos cenitales con respecto al nadir (0 para el punto central del gráfico y 90º para el anillo más externo). Las radiales del gráfico polar representa la dirección azimultal (0º hacia el Norte, 90º hacia el Este, 180º hacia el Sur y 270º hacia el Oeste). Tambien se muestra el valor integrado de la reflectividad, que reulta en el albedo.

La posición del sol está representada en el gráfico por una estrella. Por defecto al ejecutar el gráfico el sol está situado al sur con un ángulo cenital de 37 grados, lo que implica un día tipico de verano en el hemisferio Norte al mediodía.

```python
w_bidirectional = interactive(fn.bidirectional_reflectance,
                              cab=fn.w_cab, lai=fn.w_lai, leaf_angle=fn.w_leaf_angle,
                              sza=fn.w_sza, saa=fn.w_saa, skyl=fn.w_skyl)
display(w_bidirectional)
```

* Modifica sustancialmente la concentración de clorofila. ¿Cómo varía la reflectividad y el albedo tanto en el PAR como en el espectro completo solar?
* Modifica la posición del sol. Por un lado podrás ver que siempre hay un pico de reflectividad alrededor de la posición solar. Esto se trata del la zona de la vegetación que está más illuminada por el sol. Por el contrario, en la dirección opuesta se ecuentran los valores de reflectividad menores, al estar las hojas más ocluidas de los rayos de sol. Además los valores de albedo también varían según la posición del sol, es decir, según la hora del día.
* Esta variación es más significativa cuanta menor sea la proporción de radiación difusa. Aumenta la proporción de radación difusa, simulando días con mayor cobertura nubosa, verás que la posición del sol, como es de esperar ya no es relevante.

# Radiación neta de onda corta en un cultivo
Teniendo en cuenta lo visto hasta hora, el LAI, la distribución angular de las hojas la concentración de colorofila son las variables fundamentales que determinan la abosorción de radiación del cultivo. En el siguiente gráfico podemos ver la evolución de la radiación solar neta de un cultivo herbáceo ($C_{a+b}\approx40\mu g/cm^2$} a lo largo un día típico de verano para una latitud dada (independientemente de que sea el hemisferio Sur o Norte). Este gráfico asume que las condiciones de nubosidad permanecen constantes durante el día, con una proporción de radiación difusa fija, y por tanto la evolución de radiación incidente presenta una curva sinusoidal. Si las condiciones de nubosidad fueran cambiantes la curva presentaría una forma más aserrada, formando valles y crestas en los momentos en los que el sol estuviese visible u oculto tas las nubes.

El gráfico muestra tanto la radiación neta de onda corta de la superficie (curva de color negro, $S_n$) como el reparto de esa radiación entre la radiación absorbida por el cultivo (en verde, $S_{n,C}$) como el suelo (en amarillo, $S_{n,S}$)

> $S_{n,C}+S_{n,S}=S_n$

```python
w_sn = interactive(fn.plot_net_solar_radiation,
                   local_lai=fn.w_lai, leaf_angle=fn.w_leaf_angle, h_c=fixed(1), f_c=fixed(1),
                   row_distance=fixed(1), row_direction=fixed(0), sdn_day=fn.w_sdn, skyl=fn.w_skyl,
                   fvis=fixed(0.55), lat=fn.w_lat)
display(w_sn)
```

Observa que al forzar la clorofila constante, las variaciones de radiación neta son apenas perceptibles con variaciones de LAI. Sin embargo el reparto de radiación entre el suelo y la vegetación varían considerablemente con el LAI. Esto tiene implicaciones importantes ya que el LAI influye en el reparto de energía entre suelo y vegetación y por lo tanto en la capacidad de la superficie de evaporar el agua del suelo y/o la capacidad transpirativa y fotosintética del cultivo.

Mantén valores relativavente bajos de LAI (<1.5) y observa de nuevo el efecto de la distribución angular de las hojas en el dosel. Hojas más verticales (`Leaf Angle`\approx$90) provocarían una disminución de la radiación absorbida por la vegetación en las horas centrales del día, cuando el sol está más elevado y la irradiancia solar es mayor. Observa que este efecto sin embargo ya no ocurre cuando hay una mayor proporción de radiacion difusa. En ese caso casi toda la radiaicón viene indistintamente de todas direcciones y por tanto la distribución angular de la hoja no juega un papel importante.

## Radiación en cultivos en hilera
Hasta ahora todas las simulaciones y procesos que hemos realizado asumen un dosel vegetal o un cultivo horizontalmente homogéneo, o al menos suficientemente denso. Sin embargo, ¿qué ocurre cuando trabajamos con cultivos en hilera, como pueden ser viñedos o explotaciones intensivas de frutales, entre otros?

La cruda realidad es que todo restula algo más complejo ya que por un lado el dosel vegetal está agrupado en torno a un tronco o tallo, dejando partes del suelo completamente expuestas. Por otro lado, la dimensión y orientación de la hilera también afecta a cómo la luz es interceptada y transmitida entre el cultivo y el suelo.

Para evaluar esta situación es necesario considerar parámetros estructurales del cultivo adicionales:

* La altura del dosel en metros, ya que doseles más altos generarán sombras en el suelo más alargadas entre las hileras.
* El porcentaje relativo de ocupación del mismo con respecto a la separación entre hileras, que indica la anchura del dosel con respecto al espacio entre hileras.
* La orientación de las hileras con respecto al Norte. 0 indica que la hilera está orientada de Sur a Norte, valores negativos indica que la hilera va de NW as SW, valores positivos del NE and SW, con -90 y/o 90 indicando orientación E-W.

```python
w_sn = interactive(fn.plot_net_solar_radiation,
                   local_lai=fn.w_lai, leaf_angle=fn.w_leaf_angle, h_c=fn.w_hc, f_c=fn.w_fc,
                   row_distance=fn.w_interrow, row_direction=fn.w_psi, sdn_day=fn.w_sdn, skyl=fn.w_skyl,
                   fvis=fixed(0.55), lat=fn.w_lat)
display(w_sn)
```

> Esta gráfica y la anterior están sincronizadas, para facilitar la comparación entre ambas. Es decir, si modificas en alguna de ellas algún parámetro común, como el LAI, automáticamente ambas gráficas se actualizan.

Teniendo en cuenta que el sol sale hacia el Este, alcanza su cénit al Norte o al Sur seǵun el hemisferio, y se pone hacia el Oeste, observa el efecto que tiene cambiar la orientación de las hileras de N-S a E-W. El mínimo de radiacion interceptada por el cultivo es cuando el sol está en paralalo a la dirección de la hilera.

Observa que cuanta mayor sea el porcentaje de ocupación del cultivo con respecto al ancho de la hilera, el cultivo se comporta de manera más similar a un cultivo homogéneo (sección previa).

Hay un efecto combinado entre la altura del cultivo y la separación entre hileras. Hileras estrechas en cultivos bajos tienen un efecto similar que hileras anchas con vegetación alta.

Finalmente, al igual de lo que hemos estado observando en los gráficos anteriores, estos efectos se disipan cuanta mayor sea la radiación difusa, es decir cuanto mayor sea la nubosidad del cielo.

## La radiación fotosintética absorbida
Finalmente, esto también tiene consecuencas en la radiación fotosintéticamente activa (PAR) absorbida por la vegetación, la cual es un parámetro fundamental a la hora de calcular la productividad primaria bruta de la vegetación.

En el siguente gráfico puedes jugar con todos los parámetros descritos y ver cómo el PAR es interceptado y absorbida por la vegetación. Tan sólo hay un parámetro adicional que representa la absortividad de la hoja en el PAR, estrechamente ligada a la concentración de clorofila de la hoja como veremos en unos días en la práctica sobre el [espectro y las propiedades biofisicas de la hoja](./ES_espectro_vegetacion.ipynb).

```python
w_par = interactive(fn.plot_apar,
                    local_lai=fn.w_lai, leaf_angle=fn.w_leaf_angle, leaf_absorbance=fn.w_leaf_abs,
                    h_c=fn.w_hc, f_c=fn.w_fc, row_distance=fn.w_interrow, row_direction=fn.w_psi,
                    sdn_day=fn.w_sdn, soil_albedo=fixed(0.15), skyl=fn.w_skyl, fvis=fixed(0.55), lat=fn.w_lat)
display(w_par)
```

# La radiación neta de onda larga
La radiación de onda larga corresponde a la emisión y absorción de energía proveniente del calor propio de la atmósfera y la superficie. La emisión de energía de un cuerpo depende de su temperatura interna y de la emisividad del mismo. Por tanto, en este caso la radiación neta de onda larga viene determinada por la emisividad y temperatura atmosférica y la emisividad y temperatura de la superficie.

Este gráfico muestra cómo varia la radiación neta de onda larga en función de las condiciones atmosféricas (temperatura y humedad relativa) y de la temperatura de la superficie y su emisividad. Ya que la temperatura de superficie depende en cierto grado también de las condiciones atmosféricas, asi como del estado hídrico del cultivo, vamos a mostrar cómo cambia $L_n$ a distintos niveles de estrés hídrico, desde un cultivo bien hidratado en el que su temperatura de superficie está en torno a la temperatura del aire, hasta un cultivo estresado, donde su temperatura está significativamente varios grados por encima de la temperatura del aire.

> En estas prácticas trabajaremos con el sistema internacional de medidas para las temperaturas (Kelvin). Para convertin Kelvin a grados Celsius tan sólo tienes que restar 273 (273 K = 0ºC y 293K = 20ºC)

```python
w_ln = interactive(fn.plot_longwave_radiation, t_air=fn.w_tair, hr=fn.w_hr, delta_t=fixed(np.linspace(-1, 20, 50)),
                   emiss=fn.w_emiss)
display(w_ln)
```

Podrás observar que la radiación neta de onda larga suele resultar en valores significativamente menores que la radiación neta de onda corta. Sólo en casos de muy altas temperaturas y mucha humedad la radiación neta de onda larga excede los 400 W/m².

Además la radiación neta disminuye según aumenta el nivel de estrés del cultivo (es decir a mayores diferencias entre su temperatura y la temperatura del aire). Esto es uno de los dos mecanismos que permiten al cultivo regular su exceso de calor. Al aumentar su temperatura emite más energía radiante, lo que produce un efecto de retroalimentación para liberar el exceso de calor.

> La otra estrategia está relacionada con el flujo de calor sensible y la disipación de exceso de energía por convección. Este fenómeno lo veremos en la práctica de mañana sobre el [calor sensible y la turbulencia](./ES_turbulencia_y_calor_sensible.ipynb)

Finalmente la emisividad de la superficie, la cual suele estar en torno a valores de 0.95 para zonas áridas y con escasa vegetación a valores de 0.99 para vegetación densa, tiene un efecto menor en el cálculo de la radiación neta. Por lo que su estimación no requiere de gran precisión.

# Conclusiones
En esta práctica hemos visto que la radiación neta de onda corta depende principalmente de:
1. la radiacíón incidente, la cual puede obtenerse a partir de estaciones o modelos meteorológicos
2. El albedo de la superficie

Por otro lado el albedo depende principalmente de
1. Las propiedades espectrales de la vegetación, principalmente la clorofila como principal pigmento absorbente de radiación PAR.
2. Las propiedades estructurales de la vegetación, principalmente el LAI pero también de la distribución angular de la hoja. Además en cultivos en hilera la estructura de la hilera puede resultar determinante en el reparto de radiaicón entre suelo y vegetación.

La radiación neta de onda larga tiene un mayor peso en la radiación neta global, y además su modelización suele resultar más sencilla, necisitando
1. la radiacíón incidente de onda larga, la cual puede obtenerse a partir de estaciones o modelos meteorológicos
2. La emisividad y temperatura de superficie, que pueden otenerse mediante datos de teledetección.

Los análisis realizados te permitirán además poder evaluar el coste/beneficio de usar modelos más o menos complejos a la hora de estimar las radiación neta y el reparto de energía en cultivos complejos.

En prácticas futuras veremos cómo estimar el albedo y las otras propiedades biofísicas de la vegetación a partir de datos de teledetección.


# Noisy quantum device simulation with QuTiP

Author: Boxi Li (etamin1201@gmail.com)

This is the introduction notebook to the deliverable of one of the Google Summer of Code 2019 project (GSoC2019) "Noise Models in QIP Module", under the organization NumFocus. The final product of the project is a framework of noisy quantum device simulator based on QuTiP open system solvers.

The simulation of quantum information processing (QIP) is usually achieved by gate matrix product. Many simulators such as the simulation backend of Qiskit and porjectQ are based on it. QuTiP offers this common way of simulation with the class `qutit.qip.QubitCircuit`. It simulates QIP in the circuit model. You can find the introduction notebook for this matrix gate representation [here](quantum-gates.ipynb).

The simulation introduced here is different as it simulates the dynamics of the quantum device at the level of driving Hamiltonians. It is closer to the physical realization than the matrix product approach and is more convenient when simulating the noise of physical hardware. The simulator is based on QuTiP Lindbladian equation solvers and is defined as `qutip.qip.device.Processor`. The basic element is the control pulse characterized by the driving Hamiltonian, target qubits, time sequence and pulse strength. Our way of simulation offers a practical way to diagnostically add noise to each pulse or the whole device at the Hamiltonian level. Based on this pulse level control, different backends can be defined for different physical systems such as Cavity QED, Ion trap or Circuit QED. For each backend, a compiler needs to be defined. In the end, the `Processor` will be able to transfer a simple quantum circuit into the control pulse sequence, add noise automatically and perform the noisy simulation.

This notebook contains the most basic part of this quantum device simulator, i.e. the noisy evolution under given control pulses. It demonstrates how to set up the parameters and introduce different kinds of noise into the evolution.

### Note
This module is still under active development. Be ready for some adventures and unexpected edges. Please do not hesitate to raise an issue on our GitHub website if you find any bugs. A new release might break some backwards compatibility on this module, therefore we recommend you to check our GitHub website if you are facing some unexpected errors after an update.

## Links to other related notebook
There is a series of notebooks on specialized subclasses and application of the simulator `Processor`, including finding pulses realizing certain quantum gate based on optimization algorithm or physical model and simulating simple quantum algorithms:

The notebook [QuTiP example: Physical implementation of Spin Chain Qubit model](spin-chain-model.ipynb) shows the simulation of a spin-chain based quantum computing model both with `qutit.qip.QubitCircuit` and `qutip.qip.device.Processor`.

The notebook [Examples for OptPulseProcessor](qip-optpulseprocessor.ipynb) describes the class `OptPulseProcessor`, which uses the optimal control module in QuTiP to find the control pulses for quantum gates.

The notebook [Running the Deutsch–Jozsa algorithm on the noisy device simulator
](qip-processor-DJ-algorithm.ipynb) gives an example of simulating simple quantum algorithms in the presence of noise.

## The pulse level control

```python
import copy
import numpy as np
import matplotlib.pyplot as plt
pi = np.pi

from qutip.qip.device import Processor
from qutip.operators import sigmaz, sigmay, sigmax, destroy
from qutip.states import basis
from qutip.metrics import fidelity
from qutip.qip.operations import rx, ry, rz, hadamard_transform
```

### Controlling a single qubit
The simulation of a unitary evolution with `Processor` is defiend by the control pulses. Each pulse is represented by a `Pulse` object consisting of the control Hamiltonian $H_j$, the target qubits, the pulse strength $c_j$ and the time sequence $t$. The evolution is given by

\begin{equation}
U(t)=\exp(-\mathrm{i} \sum_j c_j(t) H_j t)
\end{equation}

In this example, we define a single-qubit quantum device with $\sigma_z$ and $\sigma_y$ pulses.

```python
processor = Processor(N=1)
processor.add_control(0.5 * sigmaz(), targets=0, label="sigmaz")
processor.add_control(0.5 * sigmay(), targets=0, label="sigmay")
```

The list of defined pulses are saved in an attribute `Processor.pulses`. We can see the pulse that we just defined by

```python
for pulse in processor.pulses:
    pulse.print_info()
```

    ----------------------------------------------------------------------
    Pulse label: sigmaz
    The pulse contains: 0 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[ 0.5  0. ]
     [ 0.  -0.5]], 'targets': [0], 'tlist': None, 'coeff': None}
    ----------------------------------------------------------------------
    ----------------------------------------------------------------------
    Pulse label: sigmay
    The pulse contains: 0 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[0.+0.j  0.-0.5j]
     [0.+0.5j 0.+0.j ]], 'targets': [0], 'tlist': None, 'coeff': None}
    ----------------------------------------------------------------------

We can see that the pulse strength `coeff` and time sequence `tlist` still remain undefined. To fully characterize the evolution, we need to define them both.

The pulse strength and time are both given as a NumPy array. For discrete pulses, `tlist` specifies the start and the end time of each pulse coefficient, and thus is one element longer than `coeff`. (This is different from the usual requirement in QuTiP solver where `tlist` and `coeff` needs to have the same length.) The definition below means that we turn on the $\sigma_y$ pulse for $t=\pi$ and with strength 1. (Notice that the Hamiltonian is $H=\frac{1}{2} \sigma_z$)

```python
processor.pulses[1].coeff = np.array([1.])
processor.pulses[1].tlist = np.array([0., pi])
for pulse in processor.pulses:
    pulse.print_info()
```

    ----------------------------------------------------------------------
    Pulse label: sigmaz
    The pulse contains: 0 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[ 0.5  0. ]
     [ 0.  -0.5]], 'targets': [0], 'tlist': None, 'coeff': None}
    ----------------------------------------------------------------------
    ----------------------------------------------------------------------
    Pulse label: sigmay
    The pulse contains: 0 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[0.+0.j  0.-0.5j]
     [0.+0.5j 0.+0.j ]], 'targets': [0], 'tlist': array([0.        , 3.14159265]), 'coeff': array([1.])}
    ----------------------------------------------------------------------

This pulse is a $\pi$ pulse that flips the qubit from $\left |0 \right\rangle$ to $\left |1 \right\rangle$, equivalent to a rotation around y-axis of angle $\pi$:

$$R_y(\theta) = \begin{pmatrix} cos(\theta/2) & -sin(\theta/2) \\ sin(\theta/2) & cos(\theta/2) \end{pmatrix}$$

We can run the simulation to see the result of the evolution starting from $\left |0 \right\rangle$:

```python
basis0 = basis(2, 0)
result = processor.run_state(init_state=basis0)
result.states[-1].tidyup(1.e-5)
```

Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}0.0\\1.000\\\end{array}\right)\end{equation*}

As arbitrary single-qubit gate can be decomposed into $R_z(\theta_1) \cdot R_y(\theta_2) \cdot R_z(\theta_3)$, it is enough to use three pulses. For demonstration purpose, we choose $\theta_1=\theta_2=\theta_3=\pi/2$

```python
processor.pulses[0].coeff = np.array([1., 0., 1.])
processor.pulses[1].coeff = np.array([0., 1., 0.])
processor.pulses[0].tlist = np.array([0., pi/2., 2*pi/2, 3*pi/2])
processor.pulses[1].tlist = np.array([0., pi/2., 2*pi/2, 3*pi/2])
```

```python
result = processor.run_state(init_state=basis(2, 1))
result.states[-1].tidyup(1.0e-5)
```

Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}-0.707\\0.707j\\\end{array}\right)\end{equation*}

### Pulse with continuous amplitude
If your pulse strength is generated somewhere else and is a discretization of a continuous function, you can also tell the `Processor` to use them with the cubic spline interpolation. In this case `tlist` and `coeff` must have the same length.

```python
tlist = np.linspace(0., 2*np.pi, 20)
processor = Processor(N=1, spline_kind="step_func")
processor.add_control(sigmaz(), 0)
processor.pulses[0].tlist = tlist
processor.pulses[0].coeff = np.array([np.sin(t) for t in tlist])
processor.plot_pulses();
```

```python
tlist = np.linspace(0., 2*np.pi, 20)
processor = Processor(N=1, spline_kind="cubic")
processor.add_control(sigmaz())
processor.pulses[0].tlist = tlist
processor.pulses[0].coeff = np.array([np.sin(t) for t in tlist])
processor.plot_pulses();
```

## Noisy evolution

In real quantum devices, noise affects the perfect execution of gate-based quantum circuits, limiting their depths. In general, we can divide quantum noise into two types: coherent and incoherent noise. The former one usually dues to the deviation of the control pulse. The noisy evolution is still unitary. Incoherent noise comes from the coupling of the quantum system with the environment. This type of noise leads to the loss of information. In QIP theory, we describe this type of noise with a noisy channel, corresponding to the collapse operators in the Lindblad equation.

Although noise can, in general, be simulated with quantum channel representation, it will need some pre-analysis and approximation, which can be difficult in a large system. This simulator offers an easier, but computationally more demanding solution from the viewpoint of quantum control. `Processor`, as a circuit simulator, is different from the common simulator of QIP, as it simulates the evolution of the qubits under the driving Hamiltonian. The noise will be defined according to the control pulses and the evolution will be calculated using QuTiP solvers. This enables one to define more complicated noise such as cross-talk and leakage error, depending on the physical device and the problem one wants to study. On the one hand, the simulation can help one analyze the noise composition and identify the dominant noise source. On the other hand, together with a backend compiler, one can also use it to study if an algorithm is sensitive to a certain type of noise.

### Decoherence

In `Processor`, decoherence noise is simulated by adding collapse operator into the Lindbladian equation. For single-qubit decoherence, it is equivalent to applying random bit flip and phase flip error after applying the quantum gate. For qubit relaxation, one can simply specify the $t_1$ and $t_2$ time for the device or for each qubit. Here we assume the qubit system has a drift Hamiltonian $H_d=\hbar \omega \sigma_z$, for simplicity, we let $\hbar \omega = 10$

```python
a = destroy(2)
initial_state = basis(2,1)
plus_state = (basis(2,1) + basis(2,0)).unit()
tlist = np.arange(0.00, 2.02, 0.02)
H_d = 10.*sigmaz()
```

#### Decay time $T_1$
The $T_1$ relaxation time describes the strength of amplitude damping and can be described, in a two-level system, by a collapse operator $\frac{1}{\sqrt{T_1}}a$, where $a$ is the annihilation operator. This leads to an exponential decay of the population of excited states proportional to $\exp({-t/T_1})$. This amplitude damping can be simulated by specifying the attribute `t1` of the processor

```python
from qutip.qip.pulse import Pulse
t1 = 1.
processor = Processor(1, t1=t1)
# creat a dummpy pulse that has no Hamiltonian, but only a tlist.
processor.add_pulse(Pulse(None, None, tlist=tlist, coeff=False))
result = processor.run_state(init_state=initial_state, e_ops=[a.dag()*a])
```

```python
fig, ax = plt.subplots()
ax.plot(tlist[0: 100: 10], result.expect[0][0: 100: 10], 'o', label="simulation")
ax.plot(tlist, np.exp(-1./t1*tlist), label="theory")
ax.set_xlabel("t")
ax.set_ylabel("population in the excited state")
ax.legend()
plt.grid()
```

#### Decay time $T_2$
The $T_2$ time describes the dephasing process. Here one has to be careful that the amplitude damping channel characterized by $T_1$ will also lead to a dephasing proportional to $\exp(-t/2T_1)$. To make sure that the overall phase dampling is $exp(-t/T_2)$, the processor (internally) uses an collapse operator $\frac{1}{\sqrt{2*T'_2}} \sigma_z$ with $\frac{1}{T'_2}+\frac{1}{2T_1}=\frac{1}{T_2}$ to simulate the dephasing. (This also indicates that $T_2 \leqslant 2T_1$)

Usually, the $T_2$ time is measured by the Ramsey experiment, where the qubit starts from the excited state, undergoes a $\pi/2$ pulse, proceeds for a time $t$, and measured after another $\pi/2$ pulse. For simplicity, here we directly calculate the expectation value of $\rm{H}\circ a^\dagger a \circ\rm{H}$, where $\rm{H}$ denotes the Hadamard transformation. This is equivalent to measure the population of $\frac{1}{\sqrt{2}}(|0\rangle+|1\rangle)$. The envelope should follow an exponential decay characterized by $T_2$.

```python
t1 = 1.
t2 = 0.5
processor = Processor(1, t1=t1, t2=t2)
processor.add_control(H_d, 0)
processor.pulses[0].coeff = True
processor.pulses[0].tlist = tlist
Hadamard = hadamard_transform(1)
result = processor.run_state(init_state=plus_state, e_ops=[Hadamard*a.dag()*a*Hadamard])
```

```python
fig, ax = plt.subplots()
# detail about lenght of tlist needs to be fixed
ax.plot(tlist[:-1], result.expect[0][:-1], '.', label="simulation")
ax.plot(tlist[:-1], np.exp(-1./t2*tlist[:-1])*0.5 + 0.5, label="theory")
plt.xlabel("t")
plt.ylabel("Ramsey signal")
plt.legend()
ax.grid()
```

### Random noise in the pulse intensity

Despite single-qubit decoherence, `Processor` can also simulate coherent  control noise. For general types of noise, one can define a noise object and add it to the processor. An example of predefined noise is the random amplitude noise, where random value is added to the pulse every `dt`. `loc` and `scale` are key word arguments for the random number generator `np.random.normal`.

```python
from qutip.qip.noise import RandomNoise
processor = Processor(N=1)
processor.add_control(0.5 * sigmaz(), targets=0, label="sigmaz")
processor.add_control(0.5 * sigmay(), targets=0, label="sigmay")
processor.coeffs = np.array([[1., 0., 1.],
                             [0., 1., 0.]])
processor.set_all_tlist(np.array([0., pi/2., 2*pi/2, 3*pi/2]))
processor_white = copy.deepcopy(processor)
processor_white.add_noise(RandomNoise(rand_gen=np.random.normal, dt=0.1, loc=-0.05, scale=0.02))  # gausian white noise
```

We again compare the result of the evolution with and without noise.

```python
result = processor.run_state(init_state=basis(2, 1))
result.states[-1].tidyup(1.0e-5)
```

Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}-0.707\\0.707j\\\end{array}\right)\end{equation*}

```python
result_white = processor_white.run_state(init_state=basis(2, 1))
result_white.states[-1].tidyup(1.0e-4)
```

Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}(-0.645-0.002j)\\(0.120+0.755j)\\\end{array}\right)\end{equation*}

```python
fidelity(result.states[-1], result_white.states[-1])
```

    0.9932265373557044

Since the result of this this noise is still a pure state, we can visualize it on a Bloch sphere

```python
from qutip.bloch import Bloch
b = Bloch()
b.add_states([result.states[-1], result_white.states[-1]])
b.make_sphere()
```

We can print the pulse information to see the noise.

The ideal pulses:

```python
for pulse in processor_white.pulses:
    pulse.print_info()
```

    ----------------------------------------------------------------------
    Pulse label: sigmaz
    The pulse contains: 0 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[ 0.5  0. ]
     [ 0.  -0.5]], 'targets': [0], 'tlist': array([0.        , 1.57079633, 3.14159265, 4.71238898]), 'coeff': array([1., 0., 1.])}
    ----------------------------------------------------------------------
    ----------------------------------------------------------------------
    Pulse label: sigmay
    The pulse contains: 0 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[0.+0.j  0.-0.5j]
     [0.+0.5j 0.+0.j ]], 'targets': [0], 'tlist': array([0.        , 1.57079633, 3.14159265, 4.71238898]), 'coeff': array([0., 1., 0.])}
    ----------------------------------------------------------------------

And the noisy pulses:

```python
for pulse in processor_white.get_noisy_pulses():
    pulse.print_info()
```

    ----------------------------------------------------------------------
    Pulse label: sigmaz
    The pulse contains: 1 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[ 0.5  0. ]
     [ 0.  -0.5]], 'targets': [0], 'tlist': array([0.        , 1.57079633, 3.14159265, 4.71238898]), 'coeff': array([1., 0., 1.])}

    Coherent noise:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[ 0.5  0. ]
     [ 0.  -0.5]], 'targets': [0], 'tlist': array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1, 1.2,
           1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2. , 2.1, 2.2, 2.3, 2.4, 2.5,
           2.6, 2.7, 2.8, 2.9, 3. , 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8,
           3.9, 4. , 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7]), 'coeff': array([-0.04111324, -0.06137941, -0.05426716, -0.04919859, -0.04264403,
           -0.02250504, -0.03375665, -0.05148854, -0.02148299, -0.06914471,
           -0.04056917, -0.08183237, -0.07305101, -0.0299176 , -0.01313447,
           -0.07059048, -0.01996858, -0.05178805, -0.04831687, -0.05601915,
           -0.06255294, -0.04576603, -0.06118154, -0.05631155, -0.06109015,
           -0.03506447, -0.03834254, -0.04466844, -0.00812871, -0.08938573,
           -0.02702706, -0.05005737, -0.05355745, -0.01683736, -0.09146077,
           -0.01140311, -0.02461833, -0.06842676, -0.01661333, -0.06730827,
           -0.06145252, -0.04804409, -0.06802508, -0.03654118, -0.04002454,
           -0.01915866, -0.04294549, -0.09000612])}
    ----------------------------------------------------------------------
    ----------------------------------------------------------------------
    Pulse label: sigmay
    The pulse contains: 1 coherent noise elements and 0 Lindblad noise elements.

    Ideal pulse:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[0.+0.j  0.-0.5j]
     [0.+0.5j 0.+0.j ]], 'targets': [0], 'tlist': array([0.        , 1.57079633, 3.14159265, 4.71238898]), 'coeff': array([0., 1., 0.])}

    Coherent noise:
    {'qobj': Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
    Qobj data =
    [[0.+0.j  0.-0.5j]
     [0.+0.5j 0.+0.j ]], 'targets': [0], 'tlist': array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1, 1.2,
           1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2. , 2.1, 2.2, 2.3, 2.4, 2.5,
           2.6, 2.7, 2.8, 2.9, 3. , 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8,
           3.9, 4. , 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7]), 'coeff': array([-0.06078027, -0.05756695, -0.04474645, -0.10254234, -0.06073313,
           -0.04446161, -0.06643672, -0.02880216, -0.04554076, -0.03670151,
           -0.09613619, -0.01831891, -0.04388728, -0.07712125, -0.02209791,
           -0.05018907, -0.03550091, -0.01924158, -0.03945293, -0.02134434,
           -0.04928715, -0.03777599, -0.0269203 , -0.01067214, -0.04945603,
           -0.0600917 , -0.04800358, -0.04740925, -0.03920404, -0.04555261,
           -0.04992821, -0.04579555, -0.05915739, -0.05714142, -0.05466035,
           -0.06307616, -0.03623067, -0.04228734, -0.05886232, -0.06229366,
           -0.0272317 , -0.03896859, -0.06195115, -0.06732644, -0.06956238,
           -0.05670983, -0.05463061, -0.06188376])}
    ----------------------------------------------------------------------

## Getting a `Pulse` or `QobjEvo` representation
If you define a complicate `Processor` but don't want to run the simulation right now, you can extract an ideal/noisy `Pulse` representation or `QobjEvo` representation. The later one can be feeded directly to QuTiP sovler for the evolution.

```python
ideal_pulses = processor_white.pulses
```

```python
noisy_pulses = processor_white.get_noisy_pulses(device_noise=True, drift=True)
```

```python
qobjevo = processor_white.get_qobjevo(noisy=False)
```

```python
noisy_qobjevo, c_ops = processor_white.get_qobjevo(noisy=True)
```

## Structure inside the simulator

The figures below help one understanding the workflow inside the simulator. The first figure shows how the noise is processed in the circuit processor. The noise is defined separately in a class object. When called, it takes parameters and the unitary noiseless `qutip.QobjEvo` from the processor, generates the noisy version and sends the noisy `qutip.QobjEvo` together with the collapse operators to the processor.

When calculating the evolution, the processor first creates its own `qutip.QobjEvo` of the noiseless evolution. It will then find all the noise objects saved in the attributes `qutip.qip.device.Processor.noise` and call the corresponding methods to get the `qutip.QobjEvo` and a list of collapse operators representing the noise. (For collapse operators, we don't want to add all the constant collapse into one time-independent operator, so we use a list). The processor then combines its own `qutip.QobjEvo` with those from the noise objects and give them to the solver. The figure below shows how the noiseless part and the noisy part are combined.

```python
from qutip.ipynbtools import version_table
version_table()
```

<table><tr><th>Software</th><th>Version</th></tr><tr><td>QuTiP</td><td>4.5.0.dev0+4ad874f6</td></tr><tr><td>Numpy</td><td>1.17.5</td></tr><tr><td>SciPy</td><td>1.2.1</td></tr><tr><td>matplotlib</td><td>2.2.4</td></tr><tr><td>Cython</td><td>0.29.14</td></tr><tr><td>Number of CPUs</td><td>12</td></tr><tr><td>BLAS Info</td><td>Generic</td></tr><tr><td>IPython</td><td>7.11.1</td></tr><tr><td>Python</td><td>3.6.7 (default, Dec  6 2019, 07:03:06) [MSC v.1900 64 bit (AMD64)]</td></tr><tr><td>OS</td><td>nt [win32]</td></tr><tr><td colspan='2'>Tue Jan 28 22:01:08 2020 W. Europe Standard Time</td></tr></table>


# Common Functions for `GiRaFFEfood` Initial Data for `GiRaFFE`

### NRPy+ Source Code for this module: [GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Common_Functions.py](../../edit/in_progress/GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Common_Functions.py)

**Notebook Status:**  In Progress

**Validation Notes:** This tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module through the main initial data modules that depend on it.

## Introduction:
We will need to "feed" our giraffe with initial data to evolve. There are several different choices of initial data we can use here; while each represents different physical systems, they all have some steps in common with each other. To avoid code duplication, we will first write several functions that we will use for all of them.

# Table of Contents:
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#initializenrpy): Import core NRPy+ modules and set NRPy+ parameters
1. [Step 2](#vectorpotential): Set the vector potential from input functions
1. [Step 3](#velocity): Compute $v^i_{(n)}$ from $E^i$ and $B^i$
1. [Step 4](#setall): Generate specified initial data

# Step 1: Import core NRPy+ modules and set NRPy+ parameters \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

Here, we will import the NRPy+ core modules, set the reference metric to Cartesian, and set commonly used NRPy+ parameters. We will also set up a parameter to determine what initial data is set up, although it won't do much yet.

```python
# Step 0: Import the NRPy+ core modules and set the reference metric to Cartesian
import NRPy_param_funcs as par
import grid as gri               # NRPy+: Functions having to do with numerical grids
import indexedexp as ixp
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import reference_metric as rfm

# Use the Jacobian matrix to transform the vectors to Cartesian coordinates.
# Construct Jacobian & Inverse Jacobians:
par.set_parval_from_str("reference_metric::CoordSystem","Spherical")
rfm.reference_metric()
Jac_dUCart_dDrfmUD,Jac_dUrfm_dDCartUD = rfm.compute_Jacobian_and_inverseJacobian_tofrom_Cartesian()
# Transform the coordinates of the Jacobian matrix from spherical to Cartesian:
par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
rfm.reference_metric()
tmpa,tmpb,tmpc = sp.symbols("tmpa,tmpb,tmpc")
for i in range(3):
    for j in range(3):
        Jac_dUCart_dDrfmUD[i][j] = Jac_dUCart_dDrfmUD[i][j].subs([(rfm.xx[0],tmpa),(rfm.xx[1],tmpb),(rfm.xx[2],tmpc)])
        Jac_dUCart_dDrfmUD[i][j] = Jac_dUCart_dDrfmUD[i][j].subs([(tmpa,rfm.xxSph[0]),(tmpb,rfm.xxSph[1]),(tmpc,rfm.xxSph[2])])
        Jac_dUrfm_dDCartUD[i][j] = Jac_dUrfm_dDCartUD[i][j].subs([(rfm.xx[0],tmpa),(rfm.xx[1],tmpb),(rfm.xx[2],tmpc)])
        Jac_dUrfm_dDCartUD[i][j] = Jac_dUrfm_dDCartUD[i][j].subs([(tmpa,rfm.xxSph[0]),(tmpb,rfm.xxSph[1]),(tmpc,rfm.xxSph[2])])

# Step 1a: Set commonly used parameters.
thismodule = "GiRaFFEfood_NRPy"
```

# Step 2: Set the vector potential from input functions \[Back to [top](#toc)\]
$$\label{vectorpotential}$$

First, we will write a function to generate the vector potential from input functions for each component. This function will also apply the correct coordinate staggering if the input is set as such. That is, in the staggered prescription, $A_x$ is sampled at $(i,j+1/2,k+1/2)$, $A_y$ at $(i+1/2,j,k+1/2)$, and $A_z$ at $(i+1/2,j+1/2,k)$.

We will first do this for initial data that are given with Cartesian vector components.

```python
# Generic function for all 1D tests: Compute Ax,Ay,Az
def Axyz_func_Cartesian(Ax_func,Ay_func,Az_func, stagger_enable, **params):
    x = rfm.xx_to_Cart[0]
    y = rfm.xx_to_Cart[1]
    z = rfm.xx_to_Cart[2]
    AD = ixp.zerorank1()
    # First Ax
    if stagger_enable:
        y += sp.Rational(1,2)*gri.dxx[1]
        z += sp.Rational(1,2)*gri.dxx[2]
    AD[0] = Ax_func(x,y,z, **params)
    # Then Ay
    if stagger_enable:
        x += sp.Rational(1,2)*gri.dxx[0]
        y -= sp.Rational(1,2)*gri.dxx[1]
        z += sp.Rational(1,2)*gri.dxx[2]
    AD[1] = Ay_func(x,y,z, **params)
    # Finally Az
    if stagger_enable:
        x += sp.Rational(1,2)*gri.dxx[0]
        y += sp.Rational(1,2)*gri.dxx[1]
        z -= sp.Rational(1,2)*gri.dxx[2]
    AD[2] = Az_func(x,y,z, **params)

    return AD
```

```python
# Generic function for all 1D tests: Compute Ax,Ay,Az
def Axyz_func_spherical(Ar_func,At_func,Ap_func, stagger_enable, **params):
    if "KerrSchild_radial_shift" in params:
        KerrSchild_radial_shift = params["KerrSchild_radial_shift"]
        r = rfm.xxSph[0] + KerrSchild_radial_shift # We are setting the data up in Shifted Kerr-Schild coordinates
    else:
        r = rfm.xxSph[0] # Some other coordinate system
    theta = rfm.xxSph[1]
    phi   = rfm.xxSph[2]
    AsphD = ixp.zerorank1()
    # First Ax
    if stagger_enable:
        y += sp.Rational(1,2)*gri.dxx[1]
        z += sp.Rational(1,2)*gri.dxx[2]
    AsphD[0] = Ar_func(r,theta,phi, **params)
    # Then Ay
    if stagger_enable:
        x += sp.Rational(1,2)*gri.dxx[0]
        y -= sp.Rational(1,2)*gri.dxx[1]
        z += sp.Rational(1,2)*gri.dxx[2]
    AsphD[1] = At_func(r,theta,phi, **params)
    # Finally Az
    if stagger_enable:
        x += sp.Rational(1,2)*gri.dxx[0]
        y += sp.Rational(1,2)*gri.dxx[1]
        z -= sp.Rational(1,2)*gri.dxx[2]
    AsphD[2] = Ap_func(r,theta,phi, **params)

    # Use the Jacobian matrix to transform the vectors to Cartesian coordinates.
    AD = change_basis_spherical_to_Cartesian(AsphD)
    return AD
```

# Step 3: Compute $v^i_{(n)}$ from $E^i$ and $B^i$ \[Back to [top](#toc)\]
$$\label{velocity}$$

This function computes the Valenciea 3-velocity from input electric and magnetic fields. It can also take the three-metric $\gamma_{ij}$ as an optional input; if this is not set, the function defaults to flat spacetime.

```python
# Generic function for all 1D tests: Valencia 3-velocity from ED and BU
def compute_ValenciavU_from_ED_and_BU(ED, BU, gammaDD=None):
    # Now, we calculate v^i = ([ijk] E_j B_k) / B^2,
    # where [ijk] is the Levi-Civita symbol and B^2 = \gamma_{ij} B^i B^j$ is a trivial dot product in flat space.
    LeviCivitaSymbolDDD = ixp.LeviCivitaSymbol_dim3_rank3()

    B2 = sp.sympify(0)
    # In flat spacetime, use the Minkowski metric; otherwise, use the input metric.
    if gammaDD is None:
        gammaDD = ixp.zerorank2()
        for i in range(3):
            gammaDD[i][i] = sp.sympify(1)
    for i in range(3):
        for j in range(3):
            B2 += gammaDD[i][j] * BU[i] * BU[j]

    BD = ixp.zerorank1()
    for i in range(3):
        for j in range(3):
            BD[i] = gammaDD[i][j]*BU[j]
    ValenciavU = ixp.zerorank1()
    for i in range(3):
        for j in range(3):
            for k in range(3):
                ValenciavU[i] += LeviCivitaSymbolDDD[i][j][k] * ED[j] * BD[k] / B2

    return ValenciavU
```

# Step 4: Generate specified initial data \[Back to [top](#toc)\]
$$\label{setall}$$

This is the main function that users can call to generate the initial data by passing the name of the initial data as a string and specifying if they want to enable staggering.

```python
def GiRaFFEfood_NRPy_generate_initial_data(ID_type = "DegenAlfvenWave", stagger_enable = False,**params):
    global AD, ValenciavU
    if ID_type == "ExactWald":
        AD = gfcf.Axyz_func_spherical(gfew.Ar_EW,gfew.Ath_EW,gfew.Aph_EW,stagger_enable,**params)
        ValenciavU = gfew.ValenciavU_func_EW(**params)
    elif ID_type == "MagnetosphericWald":
        AD = gfcf.Axyz_func_spherical(gfmw.Ar_MW,gfmw.Ath_MW,gfmw.Aph_MW,stagger_enable,**params)
        ValenciavU = gfmw.ValenciavU_func_MW(**params)
    elif ID_type == "SplitMonopole":
        AD = gfcf.Axyz_func_spherical(gfsm.Ar_SM,gfsm.Ath_SM,gfsm.Aph_SM,stagger_enable,**params)
        ValenciavU = gfsm.ValenciavU_func_SM(**params)
    elif ID_type == "AlfvenWave":
        AD = gfcf.Axyz_func_Cartesian(gfaw.Ax_AW,gfaw.Ay_AW,gfaw.Az_AW, stagger_enable, **params)
        ValenciavU = gfaw.ValenciavU_func_AW(**params)
    elif ID_type == "FastWave":
        AD = gfcf.Axyz_func_Cartesian(gffw.Ax_FW,gffw.Ay_FW,gffw.Az_FW, stagger_enable, **params)
        ValenciavU = gffw.ValenciavU_func_FW(**params)
    elif ID_type == "DegenAlfvenWave":
        AD = gfcf.Axyz_func_Cartesian(gfdaw.Ax_DAW,gfdaw.Ay_DAW,gfdaw.Az_DAW, stagger_enable, **params)
        ValenciavU = gfdaw.ValenciavU_func_DAW(**params)
    elif ID_type == "ThreeWaves":
        AD = gfcf.Axyz_func_Cartesian(gftw.Ax_TW,gftw.Ay_TW,gftw.Az_TW, stagger_enable, **params)
        ValenciavU = gftw.ValenciavU_func_TW(**params)
    elif ID_type == "FFE_Breakdown":
        AD = gfcf.Axyz_func_Cartesian(gffb.Ax_FB,gffb.Ay_FB,gffb.Az_FB, stagger_enable, **params)
        ValenciavU = gffb.ValenciavU_func_FB(**params)
    elif ID_type == "AlignedRotator":
        AD = gfcf.Axyz_func_spherical(gfar.Ar_AR,gfar.Ath_AR,gfar.Aph_AR, stagger_enable, **params)
        ValenciavU = gfar.ValenciavU_func_AR(**params)
```