# DiffEqBiological Tutorial I; Introduction
### Samuel Isaacson

DiffEqBiological.jl is a domain specific language (DSL) for writing chemical reaction networks in Julia. The generated chemical reaction network model can then be translated into a variety of mathematical models which can be solved using components of the broader DifferentialEquations.jl ecosystem.

In this tutorial we'll provide an introduction to using DiffEqBiological to specify chemical reaction networks, and then to solve ODE, jump, tau-leaping and SDE models generated from them. Let's start by using the DiffEqBiological `reaction_network` macro to specify a simply chemical reaction network; the well-known Repressilator.

We first import the basic packages we'll need, and use PyPlot for making figures:

```julia
# If not already installed, first hit "]" within a Julia REPL. Then type:
# add DifferentialEquations DiffEqBiological PyPlot Plots Latexify

using DifferentialEquations, DiffEqBiological, Plots, Latexify
pyplot(fmt=:svg);
```

    ┌ Info: Precompiling PyPlot [d330b81b-6aea-500a-939a-2ce795aea3ee]
    └ @ Base loading.jl:1186
    ┌ Info: Installing matplotlib via the Conda matplotlib package...
    └ @ PyCall C:\Users\haixin\.julia\packages\PyCall\RQjD7\src\PyCall.jl:698
    ┌ Info: Running `conda install -y matplotlib` in root environment
    └ @ Conda C:\Users\haixin\.julia\packages\Conda\CpuvI\src\Conda.jl:112

    Collecting package metadata: ...working... done
    Solving environment: ...working... done

    ## Package Plan ##

      environment location: C:\Users\haixin\.julia\conda\3

      added / updated specs:
        - matplotlib

    The following packages will be downloaded:

        package                    |            build
        ---------------------------|-----------------
        cycler-0.10.0              |   py36h009560c_0          13 KB
        freetype-2.9.1             |       ha9979f8_1         470 KB
        icu-58.2                   |       ha66f8fd_1        21.9 MB
        jpeg-9b                    |       hb83a4c4_2         313 KB
        kiwisolver-1.0.1           |   py36h6538335_0          61 KB
        libpng-1.6.36              |       h2a8f88b_0         550 KB
        matplotlib-3.0.3           |   py36hc8f65d3_0         6.6 MB
        pyparsing-2.3.1            |           py36_0         105 KB
        pyqt-5.9.2                 |   py36h6538335_2         4.2 MB
        python-dateutil-2.8.0      |           py36_0         282 KB
        pytz-2018.9                |           py36_0         257 KB
        qt-5.9.7                   |   vc14h73c81de_0        92.3 MB
        sip-4.19.8                 |   py36h6538335_0         282 KB
        sqlite-3.27.2              |       he774522_0         941 KB
        tornado-6.0.1              |   py36he774522_0         643 KB
        zlib-1.2.11                |       h62dcd97_3         128 KB
        ------------------------------------------------------------
                                               Total:       128.9 MB

    The following NEW packages will be INSTALLED:

      cycler             pkgs/main/win-64::cycler-0.10.0-py36h009560c_0
      freetype           pkgs/main/win-64::freetype-2.9.1-ha9979f8_1
      icu                pkgs/main/win-64::icu-58.2-ha66f8fd_1
      jpeg               pkgs/main/win-64::jpeg-9b-hb83a4c4_2
      kiwisolver         pkgs/main/win-64::kiwisolver-1.0.1-py36h6538335_0
      libpng             pkgs/main/win-64::libpng-1.6.36-h2a8f88b_0
      matplotlib         pkgs/main/win-64::matplotlib-3.0.3-py36hc8f65d3_0
      pyparsing          pkgs/main/win-64::pyparsing-2.3.1-py36_0
      pyqt               pkgs/main/win-64::pyqt-5.9.2-py36h6538335_2
      python-dateutil    pkgs/main/win-64::python-dateutil-2.8.0-py36_0
      pytz               pkgs/main/win-64::pytz-2018.9-py36_0
      qt                 pkgs/main/win-64::qt-5.9.7-vc14h73c81de_0
      sip                pkgs/main/win-64::sip-4.19.8-py36h6538335_0
      sqlite             pkgs/main/win-64::sqlite-3.27.2-he774522_0
      tornado            pkgs/main/win-64::tornado-6.0.1-py36he774522_0
      zlib               pkgs/main/win-64::zlib-1.2.11-h62dcd97_3

    Downloading and Extracting Packages
    pytz-2018.9          | 257 KB    | ########## | 100%
    zlib-1.2.11          | 128 KB    | ########## | 100%
    tornado-6.0.1        | 643 KB    | ########## | 100%
    kiwisolver-1.0.1     | 61 KB     | ########## | 100%
    sip-4.19.8           | 282 KB    | ########## | 100%
    matplotlib-3.0.3     | 6.6 MB    | ########## | 100%
    pyparsing-2.3.1      | 105 KB    | ########## | 100%
    python-dateutil-2.8. | 282 KB    | ########## | 100%
    cycler-0.10.0        | 13 KB     | ########## | 100%
    libpng-1.6.36        | 550 KB    | ########## | 100%
    icu-58.2             | 21.9 MB   | ########## | 100%
    pyqt-5.9.2           | 4.2 MB    | ########## | 100%
    jpeg-9b              | 313 KB    | ########## | 100%
    sqlite-3.27.2        | 941 KB    | ########## | 100%
    freetype-2.9.1       | 470 KB    | ########## | 100%
    qt-5.9.7             | 92.3 MB   | ########## | 100%
    Preparing transaction: ...working... done
    Verifying transaction: ...working... done
    Executing transaction: ...working... done

    ┌ Warning: `getindex(o::PyObject, s::AbstractString)` is deprecated in favor of dot overloading (`getproperty`) so elements should now be accessed as e.g. `o."s"` instead of `o["s"]`.
    │   caller = top-level scope at none:0
    └ @ Core none:0

We now construct the reaction network. The basic types of arrows and predefined rate laws one can use are discussed in detail within the DiffEqBiological [documentation](http://docs.juliadiffeq.org/latest/models/biological.html). Here we use a mix of first order, zero order and repressive Hill function rate laws. Note, $\varnothing$ corresponds to the empty state, and is used for zeroth order production and first order degradation reactions:

```julia
repressilator = @reaction_network begin
    hillr(P₃,α,K,n), ∅ --> m₁
    hillr(P₁,α,K,n), ∅ --> m₂
    hillr(P₂,α,K,n), ∅ --> m₃
    (δ,γ), m₁ ↔ ∅
    (δ,γ), m₂ ↔ ∅
    (δ,γ), m₃ ↔ ∅
    β, m₁ --> m₁ + P₁
    β, m₂ --> m₂ + P₂
    β, m₃ --> m₃ + P₃
    μ, P₁ --> ∅
    μ, P₂ --> ∅
    μ, P₃ --> ∅
end α K n δ γ β μ;
```

We can use Latexify to look at the corresponding reactions and understand the generated rate laws for each reaction

```julia
latexify(repressilator; env=:chemical)
```

\begin{align}
\require{mhchem}
\ce{ \varnothing &->[\frac{\alpha \cdot K^{n}}{K^{n} + P_3^{n}}] m_{1}}\\
\ce{ \varnothing &->[\frac{\alpha \cdot K^{n}}{K^{n} + P_1^{n}}] m_{2}}\\
\ce{ \varnothing &->[\frac{\alpha \cdot K^{n}}{K^{n} + P_2^{n}}] m_{3}}\\
\ce{ m_{1} &<=>[\delta][\gamma] \varnothing}\\
\ce{ m_{2} &<=>[\delta][\gamma] \varnothing}\\
\ce{ m_{3} &<=>[\delta][\gamma] \varnothing}\\
\ce{ m_{1} &->[\beta] m_{1} + P_{1}}\\
\ce{ m_{2} &->[\beta] m_{2} + P_{2}}\\
\ce{ m_{3} &->[\beta] m_{3} + P_{3}}\\
\ce{ P_{1} &->[\mu] \varnothing}\\
\ce{ P_{2} &->[\mu] \varnothing}\\
\ce{ P_{3} &->[\mu] \varnothing}
\end{align}

We can also use Latexify to look at the corresponding ODE model for the chemical system

```julia
latexify(repressilator)
```

\begin{align}
\frac{dm_1}{dt} =& \frac{\alpha \cdot K^{n}}{K^{n} + P_3^{n}} - \delta \cdot m_1 + \gamma \\
\frac{dm_2}{dt} =& \frac{\alpha \cdot K^{n}}{K^{n} + P_1^{n}} - \delta \cdot m_2 + \gamma \\
\frac{dm_3}{dt} =& \frac{\alpha \cdot K^{n}}{K^{n} + P_2^{n}} - \delta \cdot m_3 + \gamma \\
\frac{dP_1}{dt} =& \beta \cdot m_1 - \mu \cdot P_1 \\
\frac{dP_2}{dt} =& \beta \cdot m_2 - \mu \cdot P_2 \\
\frac{dP_3}{dt} =& \beta \cdot m_3 - \mu \cdot P_3 \\
\end{align}

To solve the ODEs we need to specify the values of the parameters in the model, the initial condition, and the time interval to solve the model on. To do this it helps to know the orderings of the parameters and the species. Parameters are ordered in the same order they appear after the `end` statement in the `@reaction_network` macro. Species are ordered in the order they first appear within the `@reaction_network` macro. We can see these orderings using the `speciesmap` and `paramsmap` functions:

```julia
speciesmap(repressilator)
```

    OrderedCollections.OrderedDict{Symbol,Int64} with 6 entries:
      :m₁ => 1
      :m₂ => 2
      :m₃ => 3
      :P₁ => 4
      :P₂ => 5
      :P₃ => 6

```julia
paramsmap(repressilator)
```

    OrderedCollections.OrderedDict{Symbol,Int64} with 7 entries:
      :α => 1
      :K => 2
      :n => 3
      :δ => 4
      :γ => 5
      :β => 6
      :μ => 7

## Solving the ODEs:
Knowing these orderings, we can create parameter and initial condition vectors, and setup the `ODEProblem` we want to solve:

```julia
# parameters [α,K,n,δ,γ,β,μ]
p = (.5, 40, 2, log(2)/120, 5e-3, 20*log(2)/120, log(2)/60)

# initial condition [m₁,m₂,m₃,P₁,P₂,P₃]
u₀ = [0.,0.,0.,20.,0.,0.]

# time interval to solve on
tspan = (0., 10000.)

# create the ODEProblem we want to solve
oprob = ODEProblem(repressilator, u₀, tspan, p)
```

    ODEProblem with uType Array{Float64,1} and tType Float64. In-place: true
    timespan: (0.0, 10000.0)
    u0: [0.0, 0.0, 0.0, 20.0, 0.0, 0.0]

At this point we are all setup to solve the ODEs. We can now use any ODE solver from within the DiffEq package. We'll just use the default DifferentialEquations solver for now, and then plot the solutions:

```julia
sol = solve(oprob, saveat=10.)
plot(sol, fmt=:svg)
```

We see the well-known oscillatory behavior of the repressilator! For more on choices of ODE solvers, see the JuliaDiffEq [documentation](http://docs.juliadiffeq.org/latest/solvers/ode_solve.html).

---

## Stochastic Simulation Algorithms (SSAs) for Stochastic Chemical Kinetics
Let's now look at a stochastic version of the repressilator, modeling it with jump processes. Here we will construct a DiffEqJump `JumpProblem` that uses Gillespie's `Direct` method, and then solve it to generate one realization of the jump process:

```julia
# first we redefine the initial condition to be integer valued
u₀ = [0,0,0,20,0,0]

# next we create a discrete problem to encode that our species are integer valued:
dprob = DiscreteProblem(repressilator, u₀, tspan, p)

# now we create a JumpProblem, and specify Gillespie's Direct Method as the solver:
jprob = JumpProblem(dprob, Direct(), repressilator, save_positions=(false,false))

# now let's solve and plot the jump process:
sol = solve(jprob, SSAStepper(), saveat=10.)
plot(sol, fmt=:svg)
```

Here we see that oscillations remain, but become much noiser. Note, in constructing the `JumpProblem` we could have used any of the SSAs that are part of DiffEqJump instead of the `Direct` method, see the list of SSAs (i.e. constant rate jump aggregators) in the [documentation](http://docs.juliadiffeq.org/latest/types/jump_types.html#Constant-Rate-Jump-Aggregators-1).

---
## $\tau$-leaping Methods:
While SSAs generate exact realizations for stochastic chemical kinetics jump process models, [$\tau$-leaping](https://en.wikipedia.org/wiki/Tau-leaping) methods offer a performant alternative by discretizing in time the underlying time-change representation of the stochastic process. The DiffEqJump package has limited support for $\tau$-leaping methods in the form of the basic Euler's method type approximation proposed by Gillespie. We can simulate the repressilator by constructing a `JumpProblem` from the `reaction_network.regular_jumps` field:

```julia
lprob = JumpProblem(dprob, Direct(), repressilator.regular_jumps)
lsol = solve(lprob, SimpleTauLeaping(), dt=.1)
plot(lsol, plotdensity=1000, fmt=:svg)
```

---
## Chemical Langevin Equation (CLE) Stochastic Differential Equation (SDE) Models:
At an intermediary physical scale between macroscopic ODE models and microscopic stochastic chemical kinetic models lies the CLE, a SDE version of the model. The SDEs add to each ODE above a noise term. As the repressilator has species that get very close to zero in size, it is not a good candidate to model with the CLE (where solutions can then go negative and become unphysical). Let's create a simpler reaction network for a birth-death process that will stay non-negative:

```julia
bdp = @reaction_network begin
  c₁, X --> 2X
  c₂, X --> 0
  c₃, 0 --> X
end c₁ c₂ c₃
p = (1.0,2.0,50.)
u₀ = [5.]
tspan = (0.,4.);
```

The corresponding Chemical Langevin Equation SDE is then

$$
dX_t = \left(c_1 X - c_2 X + c_3 \right) dt + \left( \sqrt{c_1 X} - \sqrt{c_2 X} + \sqrt{c_3} \right)dW_t,
$$

where $W_t$ denotes a standard Brownian Motion. We can solve the CLE SDE model by creating an SDEProblem and solving it similar to what we did for ODEs above:

```julia
# SDEProblem for CLE
sprob = SDEProblem(bdp, u₀, tspan, p)

# solve and plot
sol = solve(sprob, saveat=.004)
plot(sol, fmt=:svg)
```

We again have complete freedom to select any of the StochasticDifferentialEquations.jl SDE solvers, see the [documentation](http://docs.juliadiffeq.org/latest/solvers/sde_solve.html).

---
## What information the reaction_network provides:
The generated `reaction_network` contains a lot of basic information. For example
- `repressilator.f` is a function `f(du,u,p,t)` that given the current state vector `u` and time `t` fills `du` with the time derivatives of `u` (i.e. the right hand side of the ODEs).
- `repressilator.jac` is a function `jac(J,u,p,t)` that evaluates and returns the Jacobian of the ODEs in `J`. The corresponding expressions for each Jacobian term are stored in the `symjac` field:

```julia
latexify(repressilator.symjac)
```

\begin{equation}
\left[
\begin{array}{cccccc}
 - \delta & 0 & 0 & 0 & 0 & \frac{ - K^{n} \cdot n \cdot \alpha \cdot P_3^{-1 + n}}{\left( K^{n} + P_3^{n} \right)^{2}} \\
0 &  - \delta & 0 & \frac{ - K^{n} \cdot n \cdot \alpha \cdot P_1^{-1 + n}}{\left( K^{n} + P_1^{n} \right)^{2}} & 0 & 0 \\
0 & 0 &  - \delta & 0 & \frac{ - K^{n} \cdot n \cdot \alpha \cdot P_2^{-1 + n}}{\left( K^{n} + P_2^{n} \right)^{2}} & 0 \\
\beta & 0 & 0 &  - \mu & 0 & 0 \\
0 & \beta & 0 & 0 &  - \mu & 0 \\
0 & 0 & \beta & 0 & 0 &  - \mu \\
\end{array}
\right]
\end{equation}

- `repressilator.paramjac` is a function `pjac(pJ,u,p,t)` that evaluates and returns the Jacobian, `pJ`, of the ODEs *with respect to the parameters*. This allows `reaction_network`s to be used in the DifferentialEquations.jl local sensitivity analysis package [DiffEqSensitivity](http://docs.juliadiffeq.org/latest/analysis/sensitivity.html).

By default, generated `ODEProblems` will be passed the corresponding Jacobian function, which will then be used within implicit ODE/SDE methods.

See the [DiffEqBiological documentation](http://docs.juliadiffeq.org/latest/models/biological.html#The-Reaction-Network-Object-1) for a thorough description of the many other fields within the generated network.

---

```julia

```


# Full CI - Part 2

**This assignment is due Friday March 20 (the Friday we come back from Spring Break) at 5PM via email.**

In this part you'll be constructing the Hamiltonian using the determinantal information you produce this past week.

The following steps are needed:
1. Integral transformation
2. Application of Slater's Rules to Construct the Hamiltonian
3. Form Hamiltonian and Diagonalize It
4. Analysis of the CI wave function

For this assignment, you can safely assume that only closed shell references will be used. However, the determinants you constructed last week are open-shell.

## Integral Transformation

All post-Hartree-Fock theories make use of integrals in the molecular orbital basis. To transform integrals from the AO to the MO basis use the MO coefficients from your Hartree-Fock program.

### One-Electron Integrals
\begin{equation}
 (p|q) = C_{\mu}^p C_{\mu}^q (\mu|\nu)
\end{equation}

### Two-Electron Integrals

Chemist's Notation
\begin{equation}
    (pq | rs) = C_{\mu}^p C_{\nu}^q C_{\rho}^r C_{\sigma}^s (\mu\nu | \rho\sigma)\\
    (ia | jb) = C_{\mu}^i C_{\nu}^a C_{\rho}^j C_{\sigma}^b (\mu\nu | \rho\sigma)
\end{equation}

Physicist's Notation
\begin{equation}
    \left<pr | qs \right> = (pq|rs) \\
    \left<ij | ab \right> = (ia|jb)
\end{equation}

Antisymmetrized Two-Electron Integrals
\begin{equation}
    \left< ij || kl \right> = \left< ij | kl \right> - \left< ij | lk \right>
\end{equation}

## Slater's Rules

1. Identicial Determinants

\begin{equation}
    \left<\Phi_1 | \hat H | \Phi_1 \right> = \sum_m^N \left< m | \hat h | m \right> + \sum_{m>n}^N \left< mn || mn \right>
\end{equation}

Since you need to concern yourself with $\alpha$ and $\beta$ spin cases, this is similar to solving for the unrestricted Hartree-Fock energy in the MO basis:
\begin{align}
E_{one} =&\ \sum_i^{n_\alpha} h_{ii}^\alpha + \sum_i^{n_\beta} h_{ii}^\beta \\
E_{two} =&\ \sum_{i<j}^{n_\alpha} \biggl\{ J_{ij}^{\alpha} - K_{ij}^{\alpha} \biggr\} + \sum_{i<j}^{n_\beta} \biggl\{ J_{ij}^{\beta} - K_{ij}^{\beta} \biggr\} + \sum_i^{n_\alpha} \sum_j^{n_\beta} J_{ij}^{\alpha\beta} + \sum_i^{n_\beta} \sum_j^{n_\alpha} J_{ij}^{\beta\alpha} \\
E_{UHF} =& E_{one} + E_{two}
\end{align}

2. Determinants that Differ by One Spin Orbital

\begin{equation}
    \left.|\Phi_1\right> = \left.|\cdots m n \cdots \right> \\
    \left.|\Phi_2\right> = \left.|\cdots p n \cdots \right> \\
    \left<\Phi_1 | \hat H | \Phi_2 \right> = \left< m | \hat h | p \right> + \sum_{n}^N \left< mn || pn \right>
\end{equation}

3. Determinants that Differ by Two Spin Orbitals
\begin{equation}
    \left.|\Phi_1\right> = \left.|\cdots m n \cdots \right> \\
    \left.|\Phi_2\right> = \left.|\cdots p q \cdots \right> \\
    \left<\Phi_1 | \hat H | \Phi_2 \right> = \left< mn || pq \right>
\end{equation}

4. Determinants that Differ by More than Two Spin Orbitals
\begin{equation}
    \left.|\Phi_1\right> = \left.|\cdots m n o\cdots \right> \\
    \left.|\Phi_2\right> = \left.|\cdots p q r\cdots \right> \\
    \left<\Phi_1 | \hat H | \Phi_2 \right> = 0
\end{equation}

## Form Hamiltonian
Using the list of determinants from Part 1 as the rows and columns of the Hamiltonian compute the matrix element using the phase and Slater's Rules.

The matrix will be mostly filled with zeros.

Representative example:

    [[-83.4132421767  -0.2116128853  -0.           ...   0.             0.             0.          ]
     [ -0.2116128853 -82.4719521464  -0.           ...   0.             0.             0.          ]
     [ -0.            -0.           -83.3613114063 ...   0.             0.             0.          ]
     ...
     [  0.             0.             0.           ... -83.7345728523  -0.0037882607   0.0002641961]
     [  0.             0.             0.           ...  -0.0037882607 -83.1154830212   0.0282860251]
     [  0.             0.             0.           ...   0.0002641961   0.0282860251 -82.7111119598]]

Once constructed, diagonalize the Hamiltonian. Eigenvalues are electronic energies and eigenvectors describe the determinant contribution to the electronic energy.

### Perform Analysis

Using the eigenvector information, perform an analysis for the ground electronic state.  Print the $\alpha$ and $\beta$ determinant string, excitation level, CI coefficient, and percent contribution.

## Example

Sample output is debug against.  Below is the input file used to generate the data:

    Settings = dict()

    Settings["basis"] = "STO-3G"
    Settings["molecule"] = """
      0 1
      O
      H 1 R
      H 1 R 2 A
      R = 0.9
      A = 104.5
      symmetry c1
    """
    Settings["nalpha"] = 5
    Settings["nbeta"] = 5
    Settings["scf_max_iter"] = 50

    # 'o' frozen doubly occupied orbitals
    # 'a' active orbitals
    # 'u' frozen unoccupied orbitals
    #       If insufficient u's are provided, you're code will append additional
    #       u's to get the length to the number of molecular orbitals
    # 'full' automatically set the active space to 'a' * nmo
    Settings["active_space"] = 'oooaaaa'

    # If 'full' perform a full CI
    # If an integer then is the excitation level
    #    (1 = CIS, 2 = CISD, 3 = CISDT, etc.)
    Settings["excitation_level"] = 'full'

Output

    Psi4 Hartree-Fock Energy: -74.94502100876632
    Number of basis functions: 7
    Performing AO->MO integral transformation ... done in  0.0092 seconds.

    Active space is oooaaaa
      frozen core orbitals    : 3
      active orbitals         : 4
      frozen virtual orbitals : 0

    Reference determinant : α: 1111100 β: 1111100

    Number of occupied orbitals in active space 2
    Number of possible permutations is 6

    Excitation level is "full"

    Generating determinants ... done in  0.0095 seconds.
      Found 36 determinants.

    Generating Hamiltonian matrix ... done in  0.0220 seconds
    Non-zero elements of H:
    H[ α: 1110101 β: 1110101, α: 1110101 β: 1110101 ] = -83.075685
    H[ α: 1110101 β: 1110101, α: 1110011 β: 1110011 ] =   0.040364
    H[ α: 1110101 β: 1110101, α: 1110011 β: 1111001 ] =   0.004670
    H[ α: 1110101 β: 1110101, α: 1110110 β: 1110110 ] =   0.115713
    H[ α: 1110101 β: 1110101, α: 1110110 β: 1111100 ] =  -0.056629
    H[ α: 1110101 β: 1110101, α: 1111100 β: 1110110 ] =  -0.056629
    H[ α: 1110101 β: 1110101, α: 1111100 β: 1111100 ] =   0.068162
    H[ α: 1110101 β: 1110101, α: 1111001 β: 1110011 ] =   0.004670
    H[ α: 1110101 β: 1110101, α: 1111001 β: 1111001 ] =   0.058522
    H[ α: 1110101 β: 1110011, α: 1110101 β: 1110011 ] = -82.394611
    H[ α: 1110101 β: 1110011, α: 1110101 β: 1111001 ] =   0.089829
    H[ α: 1110101 β: 1110011, α: 1110011 β: 1110101 ] =   0.040364
    H[ α: 1110101 β: 1110011, α: 1110110 β: 1111010 ] =  -0.056629
    H[ α: 1110101 β: 1110011, α: 1111100 β: 1111010 ] =   0.068162
    H[ α: 1110101 β: 1110011, α: 1111001 β: 1110101 ] =   0.004670
    H[ α: 1110101 β: 1110110, α: 1110101 β: 1110110 ] = -83.152946
    H[ α: 1110101 β: 1110110, α: 1110101 β: 1111100 ] =  -0.076085
    H[ α: 1110101 β: 1110110, α: 1110011 β: 1111010 ] =   0.004670
    H[ α: 1110101 β: 1110110, α: 1110110 β: 1110101 ] =   0.115713
    H[ α: 1110101 β: 1110110, α: 1111100 β: 1110101 ] =  -0.056629
    H[ α: 1110101 β: 1110110, α: 1111001 β: 1111010 ] =   0.058522
    H[ α: 1110101 β: 1111010, α: 1110101 β: 1111010 ] = -83.361311
    H[ α: 1110101 β: 1111010, α: 1110011 β: 1110110 ] =   0.004670
    H[ α: 1110101 β: 1111010, α: 1110011 β: 1111100 ] =   0.040364
    H[ α: 1110101 β: 1111010, α: 1110110 β: 1110011 ] =  -0.056629
    H[ α: 1110101 β: 1111010, α: 1110110 β: 1111001 ] =   0.115713
    H[ α: 1110101 β: 1111010, α: 1111100 β: 1110011 ] =   0.068162
    H[ α: 1110101 β: 1111010, α: 1111100 β: 1111001 ] =  -0.056629
    H[ α: 1110101 β: 1111010, α: 1111001 β: 1110110 ] =   0.058522
    H[ α: 1110101 β: 1111010, α: 1111001 β: 1111100 ] =   0.004670
    H[ α: 1110101 β: 1111100, α: 1110101 β: 1111100 ] = -84.001802
    H[ α: 1110101 β: 1111100, α: 1110011 β: 1111010 ] =   0.040364
    H[ α: 1110101 β: 1111100, α: 1110110 β: 1110101 ] =  -0.056629
    H[ α: 1110101 β: 1111100, α: 1111100 β: 1110101 ] =   0.068162
    H[ α: 1110101 β: 1111100, α: 1111001 β: 1111010 ] =   0.004670
    H[ α: 1110101 β: 1111001, α: 1110101 β: 1111001 ] = -83.279141
    H[ α: 1110101 β: 1111001, α: 1110011 β: 1110101 ] =   0.004670
    H[ α: 1110101 β: 1111001, α: 1110110 β: 1111010 ] =   0.115713
    H[ α: 1110101 β: 1111001, α: 1111100 β: 1111010 ] =  -0.056629
    H[ α: 1110101 β: 1111001, α: 1111001 β: 1110101 ] =   0.058522
    H[ α: 1110011 β: 1110101, α: 1110011 β: 1110101 ] = -82.394611
    H[ α: 1110011 β: 1110101, α: 1111010 β: 1110110 ] =  -0.056629
    H[ α: 1110011 β: 1110101, α: 1111010 β: 1111100 ] =   0.068162
    H[ α: 1110011 β: 1110101, α: 1111001 β: 1110101 ] =   0.089829
    H[ α: 1110011 β: 1110011, α: 1110011 β: 1110011 ] = -81.418820
    H[ α: 1110011 β: 1110011, α: 1110011 β: 1111001 ] =   0.151005
    H[ α: 1110011 β: 1110011, α: 1110110 β: 1110110 ] =   0.023945
    H[ α: 1110011 β: 1110011, α: 1111010 β: 1111010 ] =   0.068162
    H[ α: 1110011 β: 1110011, α: 1111001 β: 1110011 ] =   0.151005
    H[ α: 1110011 β: 1110011, α: 1111001 β: 1111001 ] =   0.058617
    H[ α: 1110011 β: 1110110, α: 1110011 β: 1110110 ] = -82.405042
    H[ α: 1110011 β: 1110110, α: 1110011 β: 1111100 ] =  -0.137261
    H[ α: 1110011 β: 1110110, α: 1110110 β: 1110011 ] =   0.023945
    H[ α: 1110011 β: 1110110, α: 1111010 β: 1110101 ] =  -0.056629
    H[ α: 1110011 β: 1110110, α: 1111001 β: 1110110 ] =   0.085302
    H[ α: 1110011 β: 1110110, α: 1111001 β: 1111100 ] =  -0.058617
    H[ α: 1110011 β: 1111010, α: 1110011 β: 1111010 ] = -82.517778
    H[ α: 1110011 β: 1111010, α: 1111010 β: 1110011 ] =   0.068162
    H[ α: 1110011 β: 1111010, α: 1111010 β: 1111001 ] =  -0.056629
    H[ α: 1110011 β: 1111010, α: 1111001 β: 1111010 ] =   0.074920
    H[ α: 1110011 β: 1111100, α: 1110011 β: 1111100 ] = -83.452985
    H[ α: 1110011 β: 1111100, α: 1110110 β: 1111001 ] =  -0.023945
    H[ α: 1110011 β: 1111100, α: 1111010 β: 1110101 ] =   0.068162
    H[ α: 1110011 β: 1111100, α: 1111001 β: 1110110 ] =  -0.058617
    H[ α: 1110011 β: 1111100, α: 1111001 β: 1111100 ] =   0.013744
    H[ α: 1110011 β: 1111001, α: 1110011 β: 1111001 ] = -82.502436
    H[ α: 1110011 β: 1111001, α: 1110110 β: 1111100 ] =  -0.023945
    H[ α: 1110011 β: 1111001, α: 1111010 β: 1111010 ] =  -0.056629
    H[ α: 1110011 β: 1111001, α: 1111001 β: 1110011 ] =   0.058617
    H[ α: 1110011 β: 1111001, α: 1111001 β: 1111001 ] =   0.079447
    H[ α: 1110110 β: 1110101, α: 1110110 β: 1110101 ] = -83.152946
    H[ α: 1110110 β: 1110101, α: 1111010 β: 1110011 ] =   0.004670
    H[ α: 1110110 β: 1110101, α: 1111010 β: 1111001 ] =   0.058522
    H[ α: 1110110 β: 1110101, α: 1111100 β: 1110101 ] =  -0.076085
    H[ α: 1110110 β: 1110011, α: 1110110 β: 1110011 ] = -82.405042
    H[ α: 1110110 β: 1110011, α: 1110110 β: 1111001 ] =   0.085302
    H[ α: 1110110 β: 1110011, α: 1111010 β: 1110101 ] =   0.004670
    H[ α: 1110110 β: 1110011, α: 1111100 β: 1110011 ] =  -0.137261
    H[ α: 1110110 β: 1110011, α: 1111100 β: 1111001 ] =  -0.058617
    H[ α: 1110110 β: 1110110, α: 1110110 β: 1110110 ] = -83.144606
    H[ α: 1110110 β: 1110110, α: 1110110 β: 1111100 ] =  -0.071558
    H[ α: 1110110 β: 1110110, α: 1111010 β: 1111010 ] =   0.058522
    H[ α: 1110110 β: 1110110, α: 1111100 β: 1110110 ] =  -0.071558
    H[ α: 1110110 β: 1110110, α: 1111100 β: 1111100 ] =   0.058617
    H[ α: 1110110 β: 1111010, α: 1110110 β: 1111010 ] = -83.383845
    H[ α: 1110110 β: 1111010, α: 1111010 β: 1110110 ] =   0.058522
    H[ α: 1110110 β: 1111010, α: 1111010 β: 1111100 ] =   0.004670
    H[ α: 1110110 β: 1111010, α: 1111100 β: 1111010 ] =  -0.061176
    H[ α: 1110110 β: 1111100, α: 1110110 β: 1111100 ] = -84.091166
    H[ α: 1110110 β: 1111100, α: 1111010 β: 1111010 ] =   0.004670
    H[ α: 1110110 β: 1111100, α: 1111100 β: 1110110 ] =   0.058617
    H[ α: 1110110 β: 1111001, α: 1110110 β: 1111001 ] = -83.387275
    H[ α: 1110110 β: 1111001, α: 1111010 β: 1110101 ] =   0.058522
    H[ α: 1110110 β: 1111001, α: 1111100 β: 1110011 ] =  -0.058617
    H[ α: 1110110 β: 1111001, α: 1111100 β: 1111001 ] =  -0.065703
    H[ α: 1111010 β: 1110101, α: 1111010 β: 1110101 ] = -83.361311
    H[ α: 1111010 β: 1110101, α: 1111100 β: 1110011 ] =   0.040364
    H[ α: 1111010 β: 1110101, α: 1111100 β: 1111001 ] =   0.004670
    H[ α: 1111010 β: 1110101, α: 1111001 β: 1110110 ] =   0.115713
    H[ α: 1111010 β: 1110101, α: 1111001 β: 1111100 ] =  -0.056629
    H[ α: 1111010 β: 1110011, α: 1111010 β: 1110011 ] = -82.517778
    H[ α: 1111010 β: 1110011, α: 1111010 β: 1111001 ] =   0.074920
    H[ α: 1111010 β: 1110011, α: 1111100 β: 1110101 ] =   0.040364
    H[ α: 1111010 β: 1110011, α: 1111001 β: 1111010 ] =  -0.056629
    H[ α: 1111010 β: 1110110, α: 1111010 β: 1110110 ] = -83.383845
    H[ α: 1111010 β: 1110110, α: 1111010 β: 1111100 ] =  -0.061176
    H[ α: 1111010 β: 1110110, α: 1111100 β: 1111010 ] =   0.004670
    H[ α: 1111010 β: 1110110, α: 1111001 β: 1110101 ] =   0.115713
    H[ α: 1111010 β: 1111010, α: 1111010 β: 1111010 ] = -83.413242
    H[ α: 1111010 β: 1111010, α: 1111100 β: 1110110 ] =   0.004670
    H[ α: 1111010 β: 1111010, α: 1111100 β: 1111100 ] =   0.040364
    H[ α: 1111010 β: 1111010, α: 1111001 β: 1110011 ] =  -0.056629
    H[ α: 1111010 β: 1111010, α: 1111001 β: 1111001 ] =   0.115713
    H[ α: 1111010 β: 1111100, α: 1111010 β: 1111100 ] = -84.216193
    H[ α: 1111010 β: 1111100, α: 1111100 β: 1111010 ] =   0.040364
    H[ α: 1111010 β: 1111100, α: 1111001 β: 1110101 ] =  -0.056629
    H[ α: 1111010 β: 1111001, α: 1111010 β: 1111001 ] = -83.385799
    H[ α: 1111010 β: 1111001, α: 1111100 β: 1110101 ] =   0.004670
    H[ α: 1111010 β: 1111001, α: 1111001 β: 1111010 ] =   0.115713
    H[ α: 1111100 β: 1110101, α: 1111100 β: 1110101 ] = -84.001802
    H[ α: 1111100 β: 1110011, α: 1111100 β: 1110011 ] = -83.452985
    H[ α: 1111100 β: 1110011, α: 1111100 β: 1111001 ] =   0.013744
    H[ α: 1111100 β: 1110011, α: 1111001 β: 1110110 ] =  -0.023945
    H[ α: 1111100 β: 1110110, α: 1111100 β: 1110110 ] = -84.091166
    H[ α: 1111100 β: 1110110, α: 1111001 β: 1110011 ] =  -0.023945
    H[ α: 1111100 β: 1111010, α: 1111100 β: 1111010 ] = -84.216193
    H[ α: 1111100 β: 1111100, α: 1111100 β: 1111100 ] = -84.724427
    H[ α: 1111100 β: 1111100, α: 1111001 β: 1111001 ] =   0.023945
    H[ α: 1111100 β: 1111001, α: 1111100 β: 1111001 ] = -84.121920
    H[ α: 1111100 β: 1111001, α: 1111001 β: 1111100 ] =   0.023945
    H[ α: 1111001 β: 1110101, α: 1111001 β: 1110101 ] = -83.279141
    H[ α: 1111001 β: 1110011, α: 1111001 β: 1110011 ] = -82.502436
    H[ α: 1111001 β: 1110011, α: 1111001 β: 1111001 ] =   0.079447
    H[ α: 1111001 β: 1110110, α: 1111001 β: 1110110 ] = -83.387275
    H[ α: 1111001 β: 1110110, α: 1111001 β: 1111100 ] =  -0.065703
    H[ α: 1111001 β: 1111010, α: 1111001 β: 1111010 ] = -83.385799
    H[ α: 1111001 β: 1111100, α: 1111001 β: 1111100 ] = -84.121920
    H[ α: 1111001 β: 1111001, α: 1111001 β: 1111001 ] = -83.272754

    SCF Energy             : -74.94502100876632
    Active Space CI Energy : -74.95108222838542

     Analysis
    ==========

                                     -74.9510822
                                   ----------------
        α: 1110101 β: 1110101 ( 2) -0.0388 (  0.2%)
        α: 1110011 β: 1110011 ( 4)  0.0015 (  0.0%)
        α: 1110011 β: 1111001 ( 3) -0.0003 (  0.0%)
        α: 1110110 β: 1110110 ( 2) -0.0336 (  0.1%)
        α: 1110110 β: 1111100 ( 1) -0.0064 (  0.0%)
        α: 1111010 β: 1111010 ( 2) -0.0280 (  0.1%)
        α: 1111100 β: 1110110 ( 1) -0.0064 (  0.0%)
        α: 1111100 β: 1111100 ( 0)  0.9982 ( 99.6%)
        α: 1111001 β: 1110011 ( 3) -0.0003 (  0.0%)
        α: 1111001 β: 1111001 ( 2) -0.0126 (  0.0%)

```python

```


# Deep Cox Mixtures with Heterogenous Effects (CMHE) Demo

Author: ***Mononito Goswami***  <mgoswami@cs.cmu.edu>

# Contents

### 1. [Introduction](#introduction)

### 2. [Synthetic Data](#syndata)
####                   2.1 [Generative Process for the Synthetic Dataset.](#gensyndata)
####                   2.2 [Loading and Visualizing the Dataset.](#vissyndata)
####                   2.2 [Split Dataset into Train and Test.](#splitdata)

### 3. [Counterfactual Phenotyping](#phenotyping)

####                   3.1 [Phenotyping with CMHE](#phenocmhe)

####                   3.1 [Comparison with Clustering](#clustering)

### 4. [Factual Regression](#regression)

####                 4.1 [Factual Regression with CMHE](#regcmhe)

####                   4.1 [Comparison with a Deep Cox Proportional Hazards Model](#deepcph)

## 1. Introduction

Figure A: Schematic Description of CMHE:The set of features (confounders) $\mathbf{x}$ are passed through an encoder to obtain deep non-linear representations. These representations then describe the latent phenogroups $\mathbf{P}(Z|X=\mathbf{x})$ and $\mathbf{P}(\mathbf{\phi}|X=\mathbf{x})$ that determine the base survival rate and the treatment effect respectively. Finally, the individual level hazard (survival) curve under an intervention $A=\mathbf{a}$ is described by marginalizing over $Z$ and $\mathbf{\phi}$ as $\mathbf{S}(t|X=x, A=a) = \mathbf{E}_{(Z,\mathbf{\phi)}\sim \mathbf{P}(\cdot|X)}\big[ \mathbf{S}(t|A=\mathbf{a}, X, Z, \mathbf{\phi})\big]$.

 Cox Mixture with Heterogenous Effects (CMHE) is a flexible approach to recover counterfactual phenotypes of individuals that demonstrate heterogneous effects to an intervention in terms of censored Time-to-Event outcomes. CMHE is not restricted by the strong Cox Proportional Hazards assumption or any parametric assumption on the time to event distributions. CMHE achieves this by describing each individual as belonging to two different latent groups,
$Z$ that mediate the base survival rate and $\phi$ the effect of the treatment. CMHE can also be employed to model individual level counterfactuals or for standard factual survival regression.

**Figure B (Right)**: CMHE in Plate Notation. $\mathbf{x}$ confounds treatment assignment $A$ and outcome $T$ (Model parameters and censoring distribution have been abstracted out).

*For full details on Cox Mixtures with Heterogenous Effects, please refer to our preprint*:

[Counterfactual Phenotyping with Censored Time-to-Events, arXiv preprint, C. Nagpal, M. Goswami, K. Dufendach, A. Dubrawski](https://arxiv.org/abs/2202.11089)

## 2. Synthetic Data Example

```python
import pandas as pd
import torch
from tqdm import tqdm
import sys
sys.path.append('../')

from auton_survival.datasets import load_dataset
from cmhe_demo_utils import *
```

### 2.1. Generative Process for the Synthetic Data

1. Features $x_1$, $x_2$ and the base survival phenotypes $Z$ are sampled from $\texttt{scikit-learn's make_blobs(...)}$ function which generates isotropic Gaussian blobs:
$$[x_1, x_2], Z \sim \texttt{sklearn.datasets.make_blobs(K = 3)}$$
2. Features $x_3$ and $x_4$ are sampled uniformly, whereas the underlying treatment effect phenotypes $\phi$ are defined according to an $L_1$-ball:
$$ [x_1, x_2] \sim \texttt{Uniform}(-2, 2) $$
$$ \phi \triangleq \mathbb{1}\{|x_3| + |x_3| > 2\} $$
3. We then sample treat assignments from a Bernoulli distribution:
$$ A \sim  \texttt{Bernoulli}(\frac{1}{2}) $$
4. Next, the time-to-event $T$ conditioned on the confounders $x$, latent $Z$ and latent effect group $\phi$ are generated from a Gompertz distribution:
$$ T^{*}| (Z=k, {\phi}=m, A={a}) \sim \nonumber \texttt{Gompertz}\big({\beta}_{k}^{\top}{x} +({-a}^m)\big) $$
5. Finally, the observed time $T$ is obtained after censoring some of the events and censoring time is chosen uniformly at random upto $T^*$:
$$\delta \sim \texttt{Bernoulli}(\frac{3}{4}), \quad C \sim \texttt{Uniform}(0, {T}^{*})$$
$$ T =  \begin{cases} T^*, & \text{if } \delta = 1 \\ C, & \text{if } \delta = 0 \end{cases} $$

```python
# Load the synthetic dataset
outcomes, features, interventions = load_dataset(dataset='SYNTHETIC')

# Let's take a look at take the dataset
features.head(5)
```

      X1
      X2
      X3
      X4
      X5
      X6
      X7
      X8

      0
      0.148745
      1.892484
      0.195254
      0.860757
      0.696523
      0.483697
      0.339551
      0.374794

      1
      1.139439
      -0.943330
      0.411054
      0.179533
      0.428686
      0.683057
      0.600948
      0.070483

      2
      -0.961237
      0.782706
      -0.305381
      0.583576
      0.157478
      0.070556
      0.034590
      0.776005

      3
      0.466508
      0.694348
      -0.249651
      1.567092
      0.850959
      0.416178
      0.968841
      0.863598

      4
      -0.249002
      -0.552091
      1.854651
      -0.466234
      0.860385
      0.367184
      0.954347
      0.748930

### 2.2. Visualizing the Synthetic Data

```python
plot_synthetic_data(outcomes, features, interventions)
```

### 2.3 Split Dataset into  Train and Test folds

```python
# Hyper-parameters
random_seed = 0
test_size = 0.25

# Split the synthetic data into training and testing data
import numpy as np

np.random.seed(random_seed)
n = features.shape[0]

test_idx = np.zeros(n).astype('bool')
test_idx[np.random.randint(n, size=int(n*test_size))] = True

features_tr = features.iloc[~test_idx]
outcomes_tr = outcomes.iloc[~test_idx]
interventions_tr = interventions[~test_idx]
print(f'Number of training data points: {len(features_tr)}')

features_te = features.iloc[test_idx]
outcomes_te = outcomes.iloc[test_idx]
interventions_te = interventions[test_idx]
print(f'Number of test data points: {len(features_te)}')

x_tr = features_tr.values.astype('float32')
t_tr = outcomes_tr['time'].values.astype('float32')
e_tr = outcomes_tr['event'].values.astype('float32')
a_tr = interventions_tr.values.astype('float32')

x_te = features_te.values.astype('float32')
t_te = outcomes_te['time'].values.astype('float32')
e_te = outcomes_te['event'].values.astype('float32')
a_te = interventions_te.values.astype('float32')

print('Training Data Statistics:')
print(f'Shape of covariates: {x_tr.shape} | times: {t_tr.shape} | events: {e_tr.shape} | interventions: {a_tr.shape}')
```

    Number of training data points: 3899
    Number of test data points: 1101
    Training Data Statistics:
    Shape of covariates: (3899, 8) | times: (3899,) | events: (3899,) | interventions: (3899,)

```python
def find_max_treatment_effect_phenotype(g, zeta_probs, factual_outcomes):
    """
    Find the group with the maximum treatement effect phenotype
    """
    mean_differential_survival = np.zeros(zeta_probs.shape[1]) # Area under treatment phenotype group
    outcomes_train, interventions_train = factual_outcomes

    # Assign each individual to their treatment phenotype group
    for gr in range(g): # For each treatment phenotype group
        # Probability of belonging the the g^th treatment phenotype
        zeta_probs_g = zeta_probs[:, gr]
        # Consider only those individuals who are in the top 75 percentiles in this phenotype
        z_mask = zeta_probs_g>np.quantile(zeta_probs_g, 0.75)

        mean_differential_survival[gr] = find_mean_differential_survival(
            outcomes_train.loc[z_mask], interventions_train.loc[z_mask])

    return np.nanargmax(mean_differential_survival)
```

## 3. Counterfactual Phenotyping

### 3.1 Counterfactual Phenotyping with CMHE

```python
# Hyper-parameters to train model
k = 1 # number of underlying base survival phenotypes
g = 2 # number of underlying treatment effect phenotypes.
layers = [50, 50] # number of neurons in each hidden layer.

random_seed = 3
iters = 100 # number of training epochs
learning_rate = 0.001
batch_size = 128
vsize = 0.15 # size of the validation split
patience = 3
optimizer = "Adam"
```

```python
from auton_survival.models.cmhe import DeepCoxMixturesHeterogenousEffects

torch.manual_seed(random_seed)
np.random.seed(random_seed)

# Instantiate the CMHE model
model = DeepCoxMixturesHeterogenousEffects(random_seed=7, k=k, g=g, layers=layers)

model = model.fit(x_tr, t_tr, e_tr, a_tr, vsize=vsize, val_data=None, iters=iters,
                  learning_rate=learning_rate, batch_size=batch_size,
                  optimizer=optimizer, patience=patience)
```

     28%|██████████████████████████████▊                                                                               | 28/100 [00:15<00:41,  1.75it/s]

```python
print(f'Treatment Effect for the {g} groups: {model.torch_model[0].omega.detach()}')

zeta_probs_train = model.predict_latent_phi(x_tr)
zeta_train =  np.argmax(zeta_probs_train, axis=1)
print(f'Distribution of individuals in each treatement phenotype in the training data: \
{np.unique(zeta_train, return_counts=True)[1]}')

max_treat_idx_CMHE = find_max_treatment_effect_phenotype(
    g=2, zeta_probs=zeta_probs_train, factual_outcomes=(outcomes_tr, interventions_tr))
print(f'\nGroup {max_treat_idx_CMHE} has the maximum restricted mean survival time on the training data!')
```

    Treatment Effect for the 2 groups: tensor([-0.0384,  0.3049])
    Distribution of individuals in each treatement phenotype in the training data: [3888   11]

    Group 0 has the maximum restricted mean survival time on the training data!

    C:\Users\Willa Potosnak\miniconda3\envs\localenv\lib\site-packages\lifelines\fitters\__init__.py:204: ApproximationWarning: Approximating using linear interpolation`.

      warnings.warn("Approximating using linear interpolation`.\n", exceptions.ApproximationWarning)
    C:\Users\Willa Potosnak\miniconda3\envs\localenv\lib\site-packages\lifelines\fitters\__init__.py:204: ApproximationWarning: Approximating using linear interpolation`.

      warnings.warn("Approximating using linear interpolation`.\n", exceptions.ApproximationWarning)

### Evaluate CMHE on Test Data

```python
# Now for each individual in the test data, let's find the probability that
# they belong to the max treatment effect group

zeta_probs_test_CMHE = model.predict_latent_phi(x_te)
zeta_test =  np.argmax(zeta_probs_test_CMHE, axis=1)
print(f'Distribution of individuals in each treatement phenotype in the test data: \
{np.unique(zeta_test, return_counts=True)[1]}')

# Now let us evaluate our performance
plot_phenotypes_roc(outcomes_te, zeta_probs_test_CMHE[:, max_treat_idx_CMHE])
```

### 3.3 Comparison with the Clustering phenotyper

We compare the ability of CMHE against dimensionality reduction followed by clustering for counterfactual phenotyping. Specifically, we first perform dimensionality reduction of the input confounders, $\mathbf{x}$, followed by clustering. Due to a small number of confounders in the synthetic data, in the following experiment, we directly perform clustering using a Gaussian Mixture Model (GMM) with 2 components and diagonal covariance matrices.

```python
from phenotyping import ClusteringPhenotyper
from sklearn.metrics import auc

clustering_method = 'gmm'
dim_red_method = None # We would not perform dimensionality reduction for the synthetic dataset
n_components = None
n_clusters = 2 # Number of underlying treatment effect phenotypes

# Running the phenotyper
phenotyper = ClusteringPhenotyper(clustering_method=clustering_method,
                                  dim_red_method=dim_red_method,
                                  n_components=n_components,
                                  n_clusters=n_clusters)
```

```python
zeta_probs_train = phenotyper.fit_phenotype(features_tr.values)
zeta_train = np.argmax(zeta_probs_train, axis=1)
print(f'Distribution of individuals in each treatement phenotype in the training data: \
{np.unique(zeta_train, return_counts=True)[1]}')

max_treat_idx_CP = find_max_treatment_effect_phenotype(
    g=2, zeta_probs=zeta_probs_train, factual_outcomes=(outcomes_tr, interventions_tr))
print(f'\nGroup {max_treat_idx_CP} has the maximum restricted mean survival time on the training data!')
```

    No Dimensionaity reduction specified...
     Proceeding to learn clusters with the raw features...
    Fitting the following Clustering Model:
     GaussianMixture(covariance_type='diag', n_components=3, random_state=0)
    Distribution of individuals in each treatement phenotype in the training data: [1306 1162 1431]

    Group 2 has the maximum restricted mean survival time on the training data!

    C:\Users\Willa Potosnak\miniconda3\envs\localenv\lib\site-packages\lifelines\fitters\__init__.py:204: ApproximationWarning: Approximating using linear interpolation`.

      warnings.warn("Approximating using linear interpolation`.\n", exceptions.ApproximationWarning)
    C:\Users\Willa Potosnak\miniconda3\envs\localenv\lib\site-packages\lifelines\fitters\__init__.py:204: ApproximationWarning: Approximating using linear interpolation`.

      warnings.warn("Approximating using linear interpolation`.\n", exceptions.ApproximationWarning)

### Evaluate Clustering Phenotyper on Test Data

```python
# Now for each individual in the test data, let's find the probability that
# they belong to the max treatment effect group

# Use the phenotyper trained on training data to phenotype on testing data
zeta_probs_test_CP = phenotyper.phenotype(x_te)
zeta_test_CP =  np.argmax(zeta_probs_test_CP, axis=1)
print(f'Distribution of individuals in each treatement phenotype in the test data: \
{np.unique(zeta_test_CP, return_counts=True)[1]}')

# Now let us evaluate our performance
plot_phenotypes_roc(outcomes_te, zeta_probs_test_CP[:, max_treat_idx_CP])
```

## 4. CMHE for Factual Regression

For completeness, we further evaluate the performance of CMHE in estimating factual risk over multiple time horizons using the standard survival analysis metrics, including:

1. $\textbf{Brier Score} \ (\textrm{BS})$: Defined as the Mean Squared Error (MSE) around the probabilistic prediction at a certain time horizon.
\begin{align}
\text{BS}(t) = \mathop{\mathbf{E}}_{x\sim\mathcal{D}}\big[ ||\mathbf{1}\{ T > t \} - \widehat{\mathbf{P}}(T>t|X)\big)||_{_\textbf{2}}^\textbf{2}  \big]
\end{align}
2. $ \textbf{Time Dependent Concordance Index} \ (C^{\text{td}}$): A rank order statistic that computes model performance in ranking patients based on their estimated risk at a specfic time horizon.
\begin{align}
C^{td }(t) = \mathbf{P}\big( \hat{F}(t| \mathbf{x}_i) > \hat{F}(t| \mathbf{x}_j)  | \delta_i=1, T_i<T_j, T_i \leq t \big)
\end{align}

We compute the censoring adjusted estimates of the Time Dependent Concordance Index (Antolini et al., 2005; Gerds et al., 2013) and the Integrated Brier Score (i.e. Brier Score integrated over 1, 3 and 5 years. $\text{IBS} = \mathop{\sum}_t  \frac{t}{t_\text{max}}  \cdot \text{BS}(t)$) (Gerds and Schumacher, 2006; Graf et al., 1999) to assess both discriminative performance and model calibration at multiple time horizons.

*We find that CMHE had similar or better discriminative performance than a simple Cox Model with MLP hazard functions. CMHE was also better calibrated as evidenced by overall lower Integrated Brier Score, suggesting utility for factual risk estimation.*

### 4.1 Factual Regression Performance of  CMHE

```python
horizons = [1, 3, 5]

# Now let us predict survival using CMHE
predictions_test_CMHE = model.predict_survival(x_te, a_te, t=horizons)

CI1, CI3, CI5, IBS = factual_evaluate((x_tr, t_tr, e_tr, a_tr), (x_te, t_te, e_te, a_te),
                                      horizons, predictions_test_CMHE)
print(f'Concordance Index (1 Year): {np.around(CI1, 4)} (3 Year) {np.around(CI3, 4)}: (5 Year): {np.around(CI5, 4)}')
print(f'Integrated Brier Score: {np.around(IBS, 4)}')
```

    Concordance Index (1 Year): 0.6548 (3 Year) 0.6656: (5 Year): 0.6665
    Integrated Brier Score: 0.1597

```python
x_te
```

    array([[ 0.1487453 ,  1.8924836 ,  0.19525401, ...,  0.4836966 ,
             0.33955073,  0.37479353],
           [ 2.2802675 , -0.73033774, -1.7158557 , ...,  0.47515342,
             0.8169348 ,  0.59493804],
           [ 1.4902165 , -0.91186345,  0.7905248 , ...,  0.08489922,
             0.37587634,  0.62569857],
           ...,
           [ 2.0787652 , -2.0501418 , -0.36273366, ...,  0.63403505,
             0.97710544,  0.81890947],
           [-1.5852283 , -0.79666543,  0.9420089 , ...,  0.6343607 ,
             0.5449456 ,  0.01124444],
           [ 0.25414062,  1.5835027 , -0.41139466, ...,  0.369376  ,
             0.05497523,  0.5677395 ]], dtype=float32)

### 4.2 Comparison with Deep Cox-Proportional Hazards Model

```python
from auton_survival.estimators import SurvivalModel

# Now let us train a Deep Cox-proportional Hazard model with two linear layers and tanh activations
dcph_model = SurvivalModel('dcph', random_seed=0, bs=100, learning_rate=1e-3, layers=[50, 50])

interventions_tr.name, interventions_te.name = 'treat', 'treat'
features_tr_dcph = pd.concat([features_tr, interventions_tr.astype('float64')], axis=1)
features_te_dcph = pd.concat([features_te, interventions_te.astype('float64')], axis=1)
outcomes_tr_dcph = pd.DataFrame(outcomes_tr, columns=['event', 'time']).astype('float64')

# Train the DCPH model
dcph_model = dcph_model.fit(features_tr_dcph, outcomes_tr_dcph)
```

     94%|████████████████████████████████████████████████████████████████████████████████████████████████████████▎      | 47/50 [00:03<00:00, 14.81it/s]

### Evaluate DCPH on Test Data

```python
# Find suvival scores in the test data
predictions_test_DCPH = dcph_model.predict_survival(features_te_dcph, horizons)

CI1, CI3, CI5, IBS = factual_evaluate((x_tr, t_tr, e_tr, a_tr), (x_te, t_te, e_te, a_te),
                                      horizons, predictions_test_DCPH)
print(f'Concordance Index (1 Year): {np.around(CI1, 4)} (3 Year) {np.around(CI3, 4)}: (5 Year): {np.around(CI5, 4)}')
print(f'Integrated Brier Score: {np.around(IBS, 4)}')
```

    Concordance Index (1 Year): 0.6919 (3 Year) 0.6947: (5 Year): 0.6981
    Integrated Brier Score: 0.153

```python

```

```python

```

examples/Demo of CMHE on Synthetic Data.ipynb


 Exemplos práticos de otimização de portfólio com computação clássica e quântica

Esse notebook é um suporte ao nosso artigo na RBEF. "Link do artigo aqui"

# Portfólio ou Carteira

A **carteira** ou **portfólio** de um investidor é basicamente o seu investimento em diferentes tipos de ativos.

Por exemplo, se você tem investimentos (ações) em 5 empresas, digamos: Google, Amazon, Tesla, Petrobras e Vale. Essas 5 empresas compõem sua carteira de investimentos.

# Ativos, Retorno e Risco

**Ativos** pode ser de vários tipos. São exemplos de ativos (lista não euxastiva):

- Ações
- Criptomoedas (e. g. Bitcoin - BTC)
- Dinheiro
- Cota de fundos de investimento
- Títulos do Tesouro

Normalmente, quando você constrói um portfólio, é aconselhável diversificar seus ativos ou comprar diferentes tipos de ativos de diferentes empresas. Para todos os ativos, você obterá lucro após um período de tempo especificado. No entanto, o lucro pode não ser o mesmo para cada investimento feito.

Esse lucro, ou prejuízo, é o que chamamos de **retorno**. Por exemplo, você obterá retornos de ações quando o valor de mercado subir e/ou com a distribuição de lucros "em dinheiro" na forma de dividendos.

Há diversos aspectos de **risco**, como falência das empresas, recuperação judicial, risco regulatório etc. Para o contexto de gestão de portfólio interessa o risco associado às flutuações no valor do ativo, ou volatilidade do ativo. Para certos ativos, seu valor é altamente volátil, ou seja, o valor flutua bastante, e. g. Bitcoin. Enquanto alguns outros ativos, como títulos do governo e certas ações são mais estáveis, sendo relativamente mais resistentes às condições de mercado, mas podem dar retornos menores em comparação com aqueles de alto risco.

# Otimização de portfólio

Um bom portfólio é aquele que nos dá o máximo retorno com o mínimo de risco. Porém, como decidimos, entre as infinitas combinações possíveis de carteiras, aquela que é ótima? Ou seja, como é feita a otimização de portfólio?

A Teoria Moderna do Portfólio, formulada por H. Markowitz, também conhecida como análise de variância média, é um processo matemático que permite ao usuário maximizar os retornos para um determinado nível de risco.

A teoria assume que todos os investidores são avessos ao risco, ou seja, se houver uma escolha entre carteiras de baixo e alto riscos com os mesmos retornos, um investidor escolherá uma com baixo risco.

De um modo geral, a teoria incentiva a diversificação de ativos. Ele diz que um ativo A de alta variação, se combinado com diversos ativos B e C, onde A, B e C têm pouca ou nenhuma correlação, pode nos dar uma carteira com baixa variação de retornos. Este é um ponto crucial da Teoria. Outro ponto crucial é mostrar que buscar maiores retornos significa se expor a mais risco (volatilidade).

# Fronteira Eficiente

Sabemos que cada ativo em uma carteira tem sua própria taxa de retorno e riscos esperados. É possível criar várias combinações de ativos que podem fornecer retornos elevados para um nível de risco predefinido. Da mesma forma, pode haver várias carteiras que oferecem o menor risco para um retorno esperado predefinido.

Fronteira eficiente é determinada através do gráfico com 'retornos' (Y) versus 'volatilidade' (X). Mostra o conjunto de carteiras ideais que oferecem o maior retorno esperado para um determinado nível de risco ou o menor risco para um determinado nível de retorno esperado.

As carteiras que estão abaixo da fronteira eficiente são sub-ótimas porque não fornecem retorno suficiente para o nível de risco ou têm um risco maior para a taxa de retorno definida.

Veja um gráfico ilustrativo que iremos recuperar na sequência.

# Conceitos Fundamentais em Otimização de Portfólio

- **Variância**

A Variância nos preços das ações de uma empresa é um indicador importante de quão volátil esse investimento será, ou seja, como os retornos podem flutuar.

$\sigma^2 =  \frac{1}{N-1} \sum_{i = 1}^N (x_i - \bar{x})^2$

- **Volatilidade**

A volatilidade é medida como o desvio padrão das ações de uma empresa:

$\sigma = \sqrt{  \frac{1}{N-1} \sum_{i = 1}^N (x_i - \bar{x})^2 }$

- **Volatilidade Anual**

O que obtemos da raiz quadrada da variância é o desvio padrão diário (os dados são diários). Para convertê-lo em desvio padrão anual, multiplicamos a variância por $250$ (dias de negociação em um ano).

- **Covariância**

A covariância mede a relação direcional entre os retornos de dois ativos.

Uma covariância positiva significa que os **retornos** dos dois ativos se movem juntos (ambos caem ou sobem), enquanto uma covariância negativa significa que eles se movem inversamente (quando um sobe ou outro tende a cair e vice versa). O risco (assumido como a volatilidade) pode ser reduzido em uma carteira combinando ativos que têm uma covariância negativa.

- **Correlação**

A correlação, nos setores de finanças e investimento, é uma estatística que mede o grau em que dois títulos se movem em relação um ao outro. As correlações são usadas no gerenciamento avançado de portfólio, calculado como o coeficiente de correlação, que tem um valor entre $-1$ e $+1$. Você pode pensar na correlação como uma versão normalizada da covariância, em que os valores são restritos a estar entre $[-1,+1]$.

Uma correlação de $-1$ significa relação negativa, ou seja, se a correlação entre o Ativo A e o Ativo B for $-1$, se o Ativo A aumentar, o Ativo B diminuirá.

Uma correlação de $+1$ significa relação positiva, ou seja, se a correlação entre o Ativo A e o Ativo B for $+1$, se o Ativo A aumentar, o Ativo B aumenta.

Uma correlação nula, $0$, significa nenhuma relação, ou seja, se a correlação entre o Ativo A e o Ativo B for $0$, eles não têm qualquer correlação um sobre o outro.

- **Retornos esperados**

Os retornos esperados de um ativo são simplesmente a média da variação percentual nos preços de suas ações. Portanto, o valor do retorno esperado que obtemos aqui são os retornos esperados diários.

Para um valor de retorno anual esperado, você precisará reamostrar os dados a cada ano, como verá mais adiante.

Para retornos esperados, você precisa definir pesos para os ativos escolhidos.

Em termos mais simples, isso significa que você precisa decidir qual porcentagem de seu dinheiro total deseja manter nas ações de cada empresa.

- **Pesos**

Os pesos representamm a alocação percentual dos investimentos entre os ativos da carteia. Eles devem somar 1 (100% do montante investido). Adiante vamos definir uma matriz de pesos aleatórios para achar a carteira ótima.

Portanto, o problema de otimização de portfólio nada mais é do que encontrar os valores ótimos dos pesos que maximizam os retornos esperados enquanto minimizam o risco (desvio padrão == volatilidade).

- **Sharpe Ratio (SR)**

Esse índice é o retorno médio obtido em excesso da taxa livre de risco por unidade de volatilidade ou risco total. Como já visto, a volatilidade é uma medida das flutuações de preço de um ativo ou portfólio.

A taxa de retorno livre de risco é o retorno de um investimento com risco zero, o que significa que é o retorno que os investidores poderiam esperar por não correr risco.

A carteira de risco ideal é aquela com o maior Sharpe Ratio.

$ SR = \frac{R_p - R_f}{\sigma_o}, $

onde:

$R_p$ é o retorno do portfólio, $R_f$ é taxa de lucro livre de risco e $\sigma_p$ é o desvio padrão do portfólio (volatilidade).

No código abaixo, para descobrir o portfólio com Sharpe Ratio máximo, usamos três taxas de retorno livre de risco (três cenários qualitativamente distintos): a taxa de poupança, a CDI  e o IPCA acumulado dos últimos 12 meses.

# Otimização Clássica Irrestrita

```python
#Install some packges
!pip install yfinance
```

    Requirement already satisfied: yfinance in /usr/local/lib/python3.7/dist-packages (0.1.63)
    Requirement already satisfied: requests>=2.20 in /usr/local/lib/python3.7/dist-packages (from yfinance) (2.23.0)
    Requirement already satisfied: pandas>=0.24 in /usr/local/lib/python3.7/dist-packages (from yfinance) (1.1.5)
    Requirement already satisfied: lxml>=4.5.1 in /usr/local/lib/python3.7/dist-packages (from yfinance) (4.6.3)
    Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.7/dist-packages (from yfinance) (0.0.9)
    Requirement already satisfied: numpy>=1.15 in /usr/local/lib/python3.7/dist-packages (from yfinance) (1.19.5)
    Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas>=0.24->yfinance) (2.8.2)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas>=0.24->yfinance) (2018.9)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.7.3->pandas>=0.24->yfinance) (1.15.0)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests>=2.20->yfinance) (2021.5.30)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests>=2.20->yfinance) (2.10)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests>=2.20->yfinance) (3.0.4)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests>=2.20->yfinance) (1.24.3)

```python
# Useful packages
import numpy as np
import pandas as pd
from pandas_datareader import data as pdr
import yfinance as yfin
yfin.pdr_override()
#
import matplotlib.pyplot as plt
```

```python
# Read Data
# Tesla (TSLA), Facebook (FB), Magazine Luiza e Vale do Rio Doce
# Obs: o símbolo das ações brasileiras precisam de um ".SA" para a API funcionar
listOfstocks =  ['TSLA', 'FB', 'MGLU3.SA', 'VALE3.SA']
test = pdr.get_data_yahoo(listOfstocks, start="2020-01-01", end="2021-09-20")
test.head()
```

    [*********************100%***********************]  4 of 4 completed

      Adj Close
      Close
      High
      Low
      Open
      Volume

      FB
      MGLU3.SA
      TSLA
      VALE3.SA
      FB
      MGLU3.SA
      TSLA
      VALE3.SA
      FB
      MGLU3.SA
      TSLA
      VALE3.SA
      FB
      MGLU3.SA
      TSLA
      VALE3.SA
      FB
      MGLU3.SA
      TSLA
      VALE3.SA
      FB
      MGLU3.SA
      TSLA
      VALE3.SA

      Date

      2020-01-02
      209.779999
      12.029735
      86.052002
      44.330608
      209.779999
      12.3325
      86.052002
      54.330002
      209.789993
      12.3325
      86.139999
      54.439999
      206.270004
      11.9275
      84.342003
      53.750000
      206.750000
      12.075
      84.900002
      53.980000
      12077100.0
      29600800.0
      47660500.0
      17509700.0

      2020-01-03
      208.669998
      11.922436
      88.601997
      44.004223
      208.669998
      12.2225
      88.601997
      53.930000
      210.399994
      12.3325
      90.800003
      54.500000
      206.949997
      12.0750
      87.384003
      53.580002
      207.210007
      12.125
      88.099998
      53.680000
      11188400.0
      26737600.0
      88892500.0
      17284800.0

      2020-01-06
      212.600006
      11.932189
      90.307999
      43.743118
      212.600006
      12.2325
      90.307999
      53.610001
      212.779999
      12.3275
      90.311996
      53.799999
      206.520004
      11.9500
      88.000000
      53.150002
      206.699997
      12.100
      88.094002
      53.799999
      17058900.0
      27359200.0
      50665000.0
      32787800.0

      2020-01-07
      213.059998
      11.898048
      93.811996
      44.061340
      213.059998
      12.1975
      93.811996
      54.000000
      214.580002
      12.3000
      94.325996
      54.189999
      211.750000
      12.1250
      90.671997
      53.400002
      212.820007
      12.235
      92.279999
      53.500000
      14912400.0
      26818800.0
      89410500.0
      16326400.0

      2020-01-08
      215.220001
      12.263843
      98.428001
      44.069496
      215.220001
      12.5725
      98.428001
      54.009998
      216.240005
      12.5875
      99.697998
      54.369999
      212.610001
      12.1250
      93.646004
      53.619999
      213.000000
      12.155
      94.739998
      54.200001
      13475000.0
      40007200.0
      155721500.0
      15298500.0

```python
# Escolhendo apenas o preço de fechamento (Close price)
test = test['Close']
test.head()
```

      FB
      MGLU3.SA
      TSLA
      VALE3.SA

      Date

      2020-01-02
      209.779999
      12.3325
      86.052002
      54.330002

      2020-01-03
      208.669998
      12.2225
      88.601997
      53.930000

      2020-01-06
      212.600006
      12.2325
      90.307999
      53.610001

      2020-01-07
      213.059998
      12.1975
      93.811996
      54.000000

      2020-01-08
      215.220001
      12.5725
      98.428001
      54.009998

```python
# Plot
# Note a diferença de escala dos valores das ações. Uma prática corriqueira é tomar o "log".
# Note ainda que as ações brasileiras estão cotadas em Reais e as americanas em Dólar.
test.plot();
```

```python
# log
testLog = np.log(test)
```

```python
testLog.plot();
```

```python
# Vamos escolher apenas ações brasileiras por simplicidade e nos últimos 5 anos
#ativos =  ['BRKM5.SA', 'ITUB4.SA', 'PETR3.SA', 'VALE3.SA', 'MGLU3.SA', 'KLBN4.SA']
ativos =  ['BRKM5.SA', 'ITUB4.SA', 'VALE3.SA', 'KLBN4.SA'] # Braskem, Itaú, Vale e Kablin -> cotação em Reais (R$)
#ativos =  ['BRKM5.SA', 'ITUB4.SA', 'VALE3.SA', 'CIEL3.SA'] # Braskem, Itaú, Vale e Kablin -> cotação em Reais (R$)
#ativos =  ['AAPL', 'NKE', 'GOOGL', 'AMZN'] # Apple, Nike, Google e Amazon -> cotação em dólar
#
df     = pdr.get_data_yahoo(ativos, start="2016-01-01", end="2021-09-20")
df     = df['Close'] # só preço de fechamento
#
#df.head()
```

    [*********************100%***********************]  4 of 4 completed

```python
#df.plot(title = 'Preço de Fechameto dos Ativos');
df.plot(figsize=[10,10])
plt.xlabel('Data',  fontsize=25)
plt.ylabel('Preço de Fechamento',fontsize=25)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.legend(loc = 'best', fontsize=18);
plt.show()
```

```python
np.log(df+1).plot(title = 'Log (Preço)');
```

```python
# Matriz de covariança
#cov_matrix = df.pct_change().apply(lambda x: np.log(1+x)).cov() # Log of percentage change
cov_matrix = df.pct_change().cov() # Sem log of percentage change
cov_matrix = cov_matrix.dropna()
cov_matrix
```

      BRKM5.SA
      ITUB4.SA
      KLBN4.SA
      VALE3.SA

      BRKM5.SA
      0.001088
      0.000260
      0.000180
      0.000322

      ITUB4.SA
      0.000260
      0.000443
      0.000074
      0.000228

      KLBN4.SA
      0.000180
      0.000074
      0.000389
      0.000138

      VALE3.SA
      0.000322
      0.000228
      0.000138
      0.000875

```python
# plot da Matriz de covariança
plt.imshow(cov_matrix, interpolation='nearest')
plt.colorbar()
plt.show()
```

```python
# Matriz de correlação
#corr_matrix = df.pct_change().apply(lambda x: np.log(1+x)).corr()
corr_matrix = df.pct_change().corr()
corr_matrix = corr_matrix.dropna()
corr_matrix
```

      BRKM5.SA
      ITUB4.SA
      KLBN4.SA
      VALE3.SA

      BRKM5.SA
      1.000000
      0.373794
      0.276938
      0.329692

      ITUB4.SA
      0.373794
      1.000000
      0.178893
      0.365896

      KLBN4.SA
      0.276938
      0.178893
      1.000000
      0.236199

      VALE3.SA
      0.329692
      0.365896
      0.236199
      1.000000

```python
# plot da Matriz de correlação
plt.imshow(corr_matrix, interpolation='nearest')
plt.colorbar()
plt.show()
```

```python
# Retorno anual
ind_er = df.resample('Y').last().pct_change().mean()
ind_er
```

    BRKM5.SA    0.315420
    ITUB4.SA    0.057133
    KLBN4.SA    0.114300
    VALE3.SA    0.301094
    dtype: float64

```python
# Volatilidade
# A volatilidade é dada pelo desvio padrão anual. Multiplicamos a volatilidade diária por 250 porque há 250 dias de negociação / ano.
#ann_sd = df.pct_change().apply(lambda x: np.log(1+x)).std().apply(lambda x: x*np.sqrt(250))
ann_sd = df.pct_change().std().apply(lambda x: x*np.sqrt(250))
ann_sd
vol    = df.pct_change().std().apply(lambda x: x*np.sqrt(250))
vol
```

    BRKM5.SA    0.521475
    ITUB4.SA    0.332864
    KLBN4.SA    0.312028
    VALE3.SA    0.467824
    dtype: float64

```python
assets = pd.concat([ind_er, ann_sd], axis=1) # Creating a table for visualising returns and volatility of assets
assets.columns = ['Returno', 'Risco']
assets
```

      Returno
      Risco

      BRKM5.SA
      0.315420
      0.521475

      ITUB4.SA
      0.057133
      0.332864

      KLBN4.SA
      0.114300
      0.312028

      VALE3.SA
      0.301094
      0.467824

```python
p_ret      = [] # Define uma "empty list" para os retornos do portfolio
p_vol      = [] # Define uma "empty list" para a volatilidade do portfolio
p_weights  = [] # Define uma "empty list" para os pesos dos ativos no portfolio

num_assets = len(df.columns)
num_portfolios = 20000
```

```python
for portfolio in range(num_portfolios):
    weights = np.random.random(num_assets) #Escolhendo pesos aleatórios para os ativos
    weights = weights/np.sum(weights)
    p_weights.append(weights)
    returns = np.dot(weights, ind_er)      #Retornos: produto entre os retornos esperados individuais dos ativos e seus pesos
    p_ret.append(returns)
    var     = cov_matrix.mul(weights, axis=0).mul(weights, axis=1).sum().sum() #Variância do portfolio
    sd      = np.sqrt(var)                 #Desvio padrão diário
    ann_sd  = sd*np.sqrt(250)              #Desvio padrão anual  == volatilidade
    p_vol.append(ann_sd)
```

```python
data = {'Retorno':p_ret, 'Risco':p_vol}

for counter, symbol in enumerate(df.columns.tolist()):
    #print(counter, symbol)
    data[symbol+' weight'] = [w[counter] for w in p_weights]
```

```python
portfolios  = pd.DataFrame(data)
portfolios.head(n = 10) # Visualizar os n primeiros portfólio dos 10000 portfolios criados com pesos aleatórios
```

      Retorno
      Risco
      BRKM5.SA weight
      ITUB4.SA weight
      KLBN4.SA weight
      VALE3.SA weight

      0
      0.229454
      0.321139
      0.377112
      0.201188
      0.149686
      0.272013

      1
      0.155840
      0.262860
      0.134388
      0.393959
      0.273395
      0.198259

      2
      0.229662
      0.314625
      0.120484
      0.053245
      0.322111
      0.504160

      3
      0.215389
      0.324680
      0.256980
      0.353068
      0.017408
      0.372544

      4
      0.191479
      0.324746
      0.450181
      0.353052
      0.160248
      0.036519

      5
      0.169317
      0.276797
      0.186908
      0.402178
      0.194539
      0.216375

      6
      0.216277
      0.298350
      0.253335
      0.186561
      0.229842
      0.330262

      7
      0.196192
      0.280855
      0.260221
      0.193590
      0.328712
      0.217476

      8
      0.177749
      0.278537
      0.278856
      0.127531
      0.515153
      0.078460

      9
      0.209141
      0.305157
      0.168771
      0.277299
      0.143050
      0.410880

```python
# Plot da Fronteira Eficiente
portfolios.plot.scatter(x='Risco', y='Retorno', c='green', marker='o', s=10, alpha=0.15, grid=True, figsize=[10,10]);
```

```python
min_vol_port = portfolios.iloc[portfolios['Risco'].idxmin()]
# idxmin() retorna o valor mínimo na coluna especificada
min_vol_port
```

    Retorno            0.108153
    Risco              0.244218
    BRKM5.SA weight    0.010554
    ITUB4.SA weight    0.418262
    KLBN4.SA weight    0.487447
    VALE3.SA weight    0.083737
    Name: 5468, dtype: float64

```python
max_ret_port = portfolios.iloc[portfolios['Retorno'].idxmax()]
# idxmax() retorna o valor máximo na coluna especificada
max_ret_port
```

    Retorno            0.309987
    Risco              0.472700
    BRKM5.SA weight    0.861802
    ITUB4.SA weight    0.001805
    KLBN4.SA weight    0.016130
    VALE3.SA weight    0.120264
    Name: 17288, dtype: float64

```python
# Portfólio ótimo (melhor Sharpe Ratio)
# lucro livre de risco (risk factor). Poupança últimos 12 meses ~ 1.56%
rf0 = 0.0156
opt_ris_por0 = portfolios.iloc[((portfolios['Retorno']-rf0)/portfolios['Risco']).idxmax()]
opt_ris_por0
```

    Retorno            0.273460
    Risco              0.352108
    BRKM5.SA weight    0.334557
    ITUB4.SA weight    0.001105
    KLBN4.SA weight    0.172152
    VALE3.SA weight    0.492186
    Name: 12392, dtype: float64

```python
# Portfólio ótimo (melhor Sharpe Ratio)
# lucro livre de risco (risk factor). CDI/SELIC últimos 12 meses ~ 2.86%
rf1 = 0.0286
opt_ris_por1 = portfolios.iloc[((portfolios['Retorno']-rf1)/portfolios['Risco']).idxmax()]
opt_ris_por1
```

    Retorno            0.286734
    Risco              0.370427
    BRKM5.SA weight    0.403250
    ITUB4.SA weight    0.003339
    KLBN4.SA weight    0.103440
    VALE3.SA weight    0.489970
    Name: 5889, dtype: float64

```python
# Portfólio ótimo (melhor Sharpe Ratio)
# lucro livre de risco (risk factor). IPCA últimos 12 meses ~ 10.06%
# pode ser pensado como CBDs de 10% de retorno anual protegido pelo fundo garantidor de crédito (FGC)
rf2 = 0.1006
opt_ris_por2 = portfolios.iloc[((portfolios['Retorno']-rf2)/portfolios['Risco']).idxmax()]
opt_ris_por2
```

    Retorno            0.306223
    Risco              0.399605
    BRKM5.SA weight    0.419858
    ITUB4.SA weight    0.000464
    KLBN4.SA weight    0.004136
    VALE3.SA weight    0.575542
    Name: 15901, dtype: float64

```python
# Plotando o portfólio com volatilidade mínima e o com lucro máximo
plt.subplots(figsize=[10,10])
xx = portfolios['Risco']
yy = portfolios['Retorno']
plt.scatter(xx, yy ,                          c='g',     marker='o', s=15,  alpha=0.2, label = 'Portfólios');
plt.scatter(min_vol_port[1], min_vol_port[0], c='b',     marker='v', s=150, alpha=0.75, label = 'Risco Mínimo');
plt.scatter(max_ret_port[1], max_ret_port[0], c='r',     marker='^', s=150, alpha=0.75, label = 'Retorno Máximo');
plt.scatter(opt_ris_por0[1], opt_ris_por0[0], c='m',     marker='o', s=150, alpha=0.75, label = 'Fator de Risco (Poupança)');
plt.scatter(opt_ris_por1[1], opt_ris_por1[0], c='0',     marker='s', s=150, alpha=0.75, label = 'Fator de Risco (CDI)');
plt.scatter(opt_ris_por2[1], opt_ris_por2[0], c='y',     marker='*', s=150, alpha=0.75, label = 'Fator de Risco (IPCA)');
plt.xlabel('Risco',  fontsize=25)
plt.ylabel('Retorno',fontsize=25)
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.legend(loc = 'best', fontsize=18);
```

```python
new_cov = cov_matrix.to_numpy()
new_cov
```

    array([[1.08774352e-03, 2.59532811e-04, 1.80247155e-04, 3.21724369e-04],
           [2.59532811e-04, 4.43192629e-04, 7.43211072e-05, 2.27911525e-04],
           [1.80247155e-04, 7.43211072e-05, 3.89444953e-04, 1.37915422e-04],
           [3.21724369e-04, 2.27911525e-04, 1.37915422e-04, 8.75437564e-04]])

```python
ind_er.to_numpy()
```

    array([0.31542042, 0.0571331 , 0.11430001, 0.30109367])

```python
mu = np.divide(ind_er.to_numpy() - rf0, vol.values)
mu
```

    array([0.57494719, 0.12477514, 0.31631817, 0.61025857])

# Métodos de Computação Quântica - Qiskit

```python
%%capture
!pip install qiskit
```

```python
from qiskit import Aer
from qiskit.circuit.library import TwoLocal
from qiskit.aqua import QuantumInstance
from qiskit.finance.applications.ising import portfolio
from qiskit.optimization.applications.ising.common import sample_most_likely
from qiskit.finance.data_providers import RandomDataProvider
from qiskit.aqua.algorithms import VQE, QAOA, NumPyMinimumEigensolver
from qiskit.aqua.components.optimizers import COBYLA
#import numpy as np
#import matplotlib.pyplot as plt
#import datetime
```

# Otimização Revisitada - Abordagem restrita binária
Resolver o seguinte problema de otimização: \begin{split}\begin{aligned} \min_{x \in \{0, 1\}^n} q x^T \sigma x - \mu^T x\\ \text{com a restrição: } 1^T x = B \end{aligned}\end{split}
onde:
- $x \in \{0,1\}^n$ denota o vetor de variáveis de decisão binárias, que indicam quais ativos escolher ($x = 1$) e quais não escolher ($x =0$),
- $\mu \in \mathbf{R}^n$ define os retornos esperados para os ativos,
- $\sigma \in \mathbf{R}^{n\times n}$ especifica a matriz de covariância entre os ativos,
- $q>0$ controla o apetite de risco do gestor do portfólio,
- e $B$ denota o orçamento, neste caso, o número de ativos a serem selecionados dentre $n$ disponíveis.

Observações:

1 - simplificações assumidas: i) todos os ativos tem o mesmo preço normalizado como $1$ (pensando no log dos preços, não parece uma simplificação tão absurda), ii) o orçamento total deve ser gasto, ou seja, é necessário selecionar exatamente $B$ ativos.

2- $q$ pode ser visto como uma medida do apetite ao risco pois, ao aumentar $q$, precisamos de maiores retornos para o alcançar o mínimo da função objetiva, o que, pela Fronteira Eficiente, exige maiores riscos!

3- A restrição $1^T x = B$ pode ser imaginada como oriunda de um termo de penalidade do tipo $\alpha (1^T x - B)^2$ incluído explicitamente na função objetiva. Ou seja, o problema com a restrição acima é idêntico à otimização do seguinte:
\begin{split}\begin{aligned} \min_{x \in \{0, 1\}^n} q x^T \sigma x - \mu^T x + \alpha (1^T x - B)^2  \end{aligned}\end{split} pois considerando $\alpha >0$, a menor contribuição do termo de regularização ocorre quando $1^T x = B$.

4- O problema resultante pode ser mapeado para um hamiltoniano cujo estado fundamental corresponde à solução ótima.

Reference: https://qiskit.org/documentation/finance/tutorials/01_portfolio_optimization.html

Essa secção mostra como usar o Variational Quantum Eigensolver (VQE) e o Quantum Approximate Optimization Algorithm (QAOA) para encontrar a solução ideal para um determinado conjunto de parâmetros.

```python
mu     = ind_er.to_numpy()      # retorno
sigma  = cov_matrix.to_numpy()  # covariança
```

```python
q       = 0.75            # set risk factor
budget  = num_assets // 2 # set budget
penalty = num_assets      # set parameter to scale the budget penalty term

#
qubitOp, offset = portfolio.get_operator(mu, sigma, q, budget, penalty)

```

```python
# Imprimir resultado de forma elegante
def index_to_selection(i, num_assets):
    s = "{0:b}".format(i).rjust(num_assets)
    x = np.array([1 if s[i]=='1' else 0 for i in reversed(range(num_assets))])
    return x

def print_result(result):
    selection = sample_most_likely(result.eigenstate)
    value = portfolio.portfolio_value(selection, mu, sigma, q, budget, penalty)
    print('Optimal: selection {}, value {:.4f}'.format(selection, value))

    eigenvector = result.eigenstate if isinstance(result.eigenstate, np.ndarray) else result.eigenstate.to_matrix()
    probabilities = np.abs(eigenvector)**2
    i_sorted = reversed(np.argsort(probabilities))
    print('\n----------------- Full result ---------------------')
    print('selection\tvalue\t\tprobability')
    print('---------------------------------------------------')
    for i in i_sorted:
        x = index_to_selection(i, num_assets)
        value = portfolio.portfolio_value(x, mu, sigma, q, budget, penalty)
        probability = probabilities[i]
        print('%10s\t%.4f\t\t%.4f' %(x, value, probability))
```

```python
# Solução "clássica" com Numpy
exact_eigensolver = NumPyMinimumEigensolver(qubitOp)
result = exact_eigensolver.run()

print_result(result)
```

    Optimal: selection [1 0 0 1], value -0.6146

    ----------------- Full result ---------------------
    selection	value		probability
    ---------------------------------------------------
     [1 0 0 1]	-0.6146		1.0000
     [1 1 1 1]	15.2160		0.0000
     [0 1 1 1]	3.5294		0.0000
     [1 0 1 1]	3.2719		0.0000
     [0 0 1 1]	-0.4142		0.0000
     [1 1 0 1]	3.3294		0.0000
     [0 1 0 1]	-0.3569		0.0000
     [0 0 0 1]	3.6996		0.0000
     [1 1 1 0]	3.5154		0.0000
     [0 1 1 0]	-0.1707		0.0000
     [1 0 1 0]	-0.4283		0.0000
     [0 0 1 0]	3.8860		0.0000
     [1 1 0 0]	-0.3710		0.0000
     [0 1 0 0]	3.9432		0.0000
     [1 0 0 0]	3.6854		0.0000
     [0 0 0 0]	16.0000		0.0000

# QAOA - Qiskit

```python
backend = Aer.get_backend('statevector_simulator')
seed = 50

cobyla = COBYLA()
cobyla.set_options(maxiter=250)
qaoa = QAOA(qubitOp, cobyla, 3)

qaoa.random_seed = seed

quantum_instance = QuantumInstance(backend=backend, seed_simulator=seed, seed_transpiler=seed)

result = qaoa.run(quantum_instance)

print_result(result)
```

    /usr/local/lib/python3.7/dist-packages/qiskit/utils/deprecation.py:62: DeprecationWarning: Using a qobj for run() is deprecated as of qiskit-aer 0.9.0 and will be removed no sooner than 3 months from that release date. Transpiled circuits should now be passed directly using `backend.run(circuits, **run_options).
      return func(*args, **kwargs)

    Optimal: selection [1. 0. 0. 1.], value -0.6146

    ----------------- Full result ---------------------
    selection	value		probability
    ---------------------------------------------------
     [1 0 0 1]	-0.6146		0.3861
     [1 0 1 0]	-0.4283		0.1399
     [0 0 1 1]	-0.4142		0.1329
     [1 1 0 0]	-0.3710		0.1146
     [0 1 0 1]	-0.3569		0.1081
     [1 0 1 1]	3.2719		0.0533
     [0 1 1 0]	-0.1707		0.0155
     [0 1 1 1]	3.5294		0.0113
     [1 1 0 1]	3.3294		0.0110
     [1 1 1 0]	3.5154		0.0079
     [1 0 0 0]	3.6854		0.0067
     [1 1 1 1]	15.2160		0.0065
     [0 0 0 1]	3.6996		0.0050
     [0 1 0 0]	3.9432		0.0005
     [0 0 0 0]	16.0000		0.0004
     [0 0 1 0]	3.8860		0.0002

Observe como o "solver" penaliza carteiras com número de ações diferentes do orçamento == 2. Por exemplo, veja o valor da função objetivo para [0 0 0 0] e [1 1 1 1].

# VQE - Qiskit

```python
backend = Aer.get_backend('statevector_simulator')
seed = 50

cobyla = COBYLA()
cobyla.set_options(maxiter=500)
ry = TwoLocal(qubitOp.num_qubits, 'ry', 'cz', reps=3, entanglement='full')
vqe = VQE(qubitOp, ry, cobyla)
vqe.random_seed = seed

quantum_instance = QuantumInstance(backend=backend, seed_simulator=seed, seed_transpiler=seed)

result = vqe.run(quantum_instance)

print_result(result)
```

    /usr/local/lib/python3.7/dist-packages/qiskit/utils/deprecation.py:62: DeprecationWarning: Using a qobj for run() is deprecated as of qiskit-aer 0.9.0 and will be removed no sooner than 3 months from that release date. Transpiled circuits should now be passed directly using `backend.run(circuits, **run_options).
      return func(*args, **kwargs)

    Optimal: selection [1. 0. 0. 1.], value -0.6146

    ----------------- Full result ---------------------
    selection	value		probability
    ---------------------------------------------------
     [1 0 0 1]	-0.6146		0.8917
     [0 1 0 1]	-0.3569		0.0675
     [1 1 0 0]	-0.3710		0.0322
     [1 0 1 0]	-0.4283		0.0068
     [0 0 1 1]	-0.4142		0.0011
     [0 1 1 0]	-0.1707		0.0004
     [1 1 1 0]	3.5154		0.0002
     [0 1 1 1]	3.5294		0.0001
     [0 0 0 0]	16.0000		0.0000
     [1 0 1 1]	3.2719		0.0000
     [1 0 0 0]	3.6854		0.0000
     [0 0 0 1]	3.6996		0.0000
     [0 1 0 0]	3.9432		0.0000
     [0 0 1 0]	3.8860		0.0000
     [1 1 0 1]	3.3294		0.0000
     [1 1 1 1]	15.2160		0.0000

Observe como o "solver" também penaliza carteiras com número de ações diferentes do orçamento == 2. Por exemplo, verifique o valor da função objetivo para [0 0 0 0] e [1 1 1 1].

# Métodos de Otimização Quânticos - Cimatec

# QAOA

O QAOA é um algoritmo variacional da computação quântica, desenvolvido por \cite{faihi2014}, adequado para resolver problemas de otimização combinatória. Nesta seção, apresentaremos um problema de otimização combinatória que pode ser modelado como um modelo ferromagnético popular na mecânica estatística: o \textit{Modelo de Ising} (ou \textit{Hamiltoniano de Ising}). O algoritmo consiste em modelar a função objetivo (F.O.) como um \textit{Hamiltoniano de Ising} e transformá-la em operadores unitários, aplicar tais operadores em um estado de entrada, medir os valores esperados, atualizar os parâmetros e repetir o processo até que a função custo seja minimizada. Quando a energia da F.O. é minimizada, a distribuição de probabilidades (obtida após a medição da função de onda $|\psi\rangle$ com os parâmetros ótimos) apresenta uma ocorrência maior do autoestado de $|\psi\rangle$ que fornece a melhor configuração para o problema.

### Modelo de Ising

O modelo do comportamento de materiais ferromagnéticos pode ser descrito pelo \textit{Hamiltoniano de Ising}. O modelo representa uma cadeia de partículas de spin 1/2, que interagem em pares, sendo que cada constituinte está sujeito à um campo magnético de magnitude $h_i$. Como partículas de spin 1/2 podem assumir apenas duas direções de momento de dipolo ("up" e "down") a interação entre cada par de partículas fornece uma energia (positiva ou negativa) de acordo com as orientações dos spins. Para encontrar a configuração dos momentos de dipolo que fornece o menor valor de energia possível do sistema, a seguinte função objetivo deve ser minimizada:

\begin{equation}
    C(z) = - \sum_{<ij>}^{}z_iz_j - \sum_{i}^{}h_iz_i .
\end{equation}

As variáveis $z_i$ da função objetivo podem assumir apenas valores unitários, sejam eles positivos ou negativos, ou seja, $z_i \in \{-1,+1\}$. As variáveis podem ser escritas como operadores de Pauli-Z ($\sigma_z$), já que $\pm 1$ são autovalores de $\sigma_z$, associados às autofunções de spin das partículas.

### Operador de Fase

Para que seja possível mapear um problema de variáveis binárias $z_i$ para um problema de otimização de autovalores, uma estratégia é substituir as variáveis $z_i \in \{-1,+1\}$ por operadores unitários (que possuem autovalores $\lambda \in \{-1,+1\}$). Diante disso, é possível criar um Operador de Fase que representa as interações entre as partículas adjacentes, bem como a resposta das partículas ao campo magnético aplicado. Trata-se de escrever a função objetivo do problema como um operador unitário para que possa ser aplicada diretamente no circuito. Como visto, o operador descrito na Equação \ref{fo_ising} é não-unitário, visto que não satisfaz o critério da Equação 7. Contudo, cada termo da soma (negligenciando os coeficientes) é um operador unitário. Para descrever este Hamiltoniano define-se o Operador Gamma através da relação

\begin{equation}
    U(C,\gamma)=e^{-i\gamma C(Z)}=e^{i\gamma\sum_{<ij>}^{}Z_iZ_j + i\gamma\sum_{i}^{}h_iZ_i} .
\end{equation}

Como $C(z)$ é expressa em termos dos operadores Pauli-Z e o produto tensorial entre eles geram matrizes diagonais, $U(C,\gamma)$ também é uma matriz diagonal (porém unitária), onde o parâmetro $\gamma$ é um dos parâmetros variacionais usados no QAOA. Desse modo, o operador $U(C,\gamma)$ pode ser escrito como a operação unitária

\begin{equation}
    U(C,\gamma)=\prod_{<ij>}^{}e^{i\gamma Z_iZ_j}\prod_{i=0}^{N}e^{i\gamma h_iZ_i} .
\end{equation}

Esse operador (assim como produtos tensoriais entre matrizes de Pauli-Z) apresenta a forma de  uma matriz diagonal, onde os elementos da diagonal são exponenciais complexas que adicionam fases no estado quântico no qual é aplicado.

### Operador de Mistura

O operador $U(B,\beta)$ é aplicado com o objetivo de misturar as amplitudes (gerando interferências destrutivas e construtivas) possibilitando explorar melhor o espaço de busca e aumentando as chances de encontrar, com maior precisão, o ponto ótimo global da função objetivo. O Operador de Mistura pode ser descrito, matematicamente, como

\begin{equation}
    U(B,\beta) = e^{-i\beta \sum_{j=0}^{N} X_j} \equiv \prod_{j=0}^{N}e^{-i\beta X_j}.
\end{equation}

Tal operador pode ser construído utilizando apenas operações de rotação em \textit{qubits} individuais do tipo RX. Contudo,a eficiência desse operador está diretamente ligada ao tempo de convergência e à acurácia do algoritmo. A Equação \ref{mix} descreve o operador em sua forma mais simples, representado apenas como rotações em torno do eixo $x$ da \textit{Esfera de Bloch}.

### Função objetivo do QAOA: Medindo Valores Esperados
As operações unitárias apresentadas nas subseções anteriores podem ser representadas como diagramas de circuitos quânticos. No QAOA, cada conjunto das operações em questão são conhecidas como \textit{layers}. A $i$-ésima \textit{layer} do circuito pode ser escrita como

\begin{equation}
    U(\gamma_i,\beta_i) = U(B,\beta_i)U(C,\gamma_i),
\end{equation}

e, portanto, a sequência desses operadores representa o operador geral do QAOA. Quanto maior o número de \textit{layers}, maior o número de parâmetros do circuito e, consequentemente, maior a acurácia do resultado. Assim como no VQE, a função objetivo do QAOA também é um valor esperado. A diferença entre eles é que, no VQE, utiliza-se um ansatz genérico e no QAOA, por sua vez, o ansatz é construído à partir da função objetivo. A atuação do operador geral do QAOA sobre o estado de entrada $|s\rangle$ pode ser considerada da seguinte maneira:
\begin{equation}
    |\vec{\gamma},\vec{\beta} \rangle = (U(B,\beta_n)U(C,\gamma_n))...(U(B, \beta_1)U(C,\gamma_1))(U(B,\beta_0)U(C,\gamma_0))|s\rangle.
\end{equation}
Como é de interesse encontrar a configuração de menor energia para o hamiltoniano, a função objetivo do problema é, novamente, a função dos valores esperados de energia, que se minimizada, retorna o menor autovalor de energia do sistema. A função objetivo do problema pode ser expressa como

\begin{equation}
    \langle C(Z) \rangle \equiv \langle\vec{\gamma},\vec{\beta}|C(Z)|\vec{\gamma},\vec{\beta} \rangle.
\end{equation}
Como visto nos capitulos anteriores, o valor esperado de energia também pode ser escrito como um valor médio. Quando o estado quântico é preparado e medido uma quantidade suficiente de vezes, a distribuição de probabilidades obtida nos possibilita calcular o valor esperado como a soma da quantidade de vezes que cada autoestado da função de onda foi medido, multiplicada por suas respectivas energias associadas. Portanto, a função objetivo também pode ser escrita como
\begin{equation}
    \langle\vec{\gamma},\vec{\beta}|C(Z)|\vec{\gamma},\vec{\beta} \rangle = E_0\alpha_0\langle\vec{\gamma},\vec{\beta}|0\rangle + E_1\alpha_1\langle\vec{\gamma},\vec{\beta}|1\rangle + E_2\alpha_2\langle\vec{\gamma},\vec{\beta}|2\rangle + ... + E_n\alpha_n\langle\vec{\gamma},\vec{\beta}|n\rangle =  \sum_{i=0}^{n}E_ip_i.
\end{equation}

Os parâmetros variacionais, $\gamma$ e $\beta$, são atualizados iterativamente através de métodos clássicos de otimização até que o menor autovalor de energia seja encontrado. Por se tratar de problemas de otimização combinatória, o interesse do problema é no autoestado da base computacional associado ao menor autovalor, que é composto pela \textit{bitstring} formada pela configuração ótima do problema.

Como visto, o QAOA pode ser usado para encontrar a configuração que minimiza a energia do \textit{Hamiltoniano de Ising}. Por se tratar de um problema de otimização combinatória, percebe-se que o algoritmo é um bom candidato a resolver problemas dessa natureza.

### O Método Variacional da Mecânica Quântica
Como discutido no Capítulo 2, informações sobre um estado quântico podem ser obtidas por meio de um conjunto de medidas sobre o sistema. Embora nosso objetivo seja encontrar o menor autovalor de uma matriz, a função objetivo do problema é calculada através valor esperado da matriz em questão, visto que se uma função de onda parametrizada por um conjunto de parâmetros $\theta$ é criada, o princípio variacional nos garante que
\begin{equation}
    \langle \mathcal{H}_\theta \rangle \equiv \langle \psi_\theta | \mathcal{H}|\psi_\theta \rangle \geq \lambda_{min}.
\end{equation}

Portanto, se o espaço de busca é suficientemente grande e o objetivo do problema é minimizar o valor esperado da matriz $ \mathcal{H}$, existe um conjunto de parâmetros $\theta'$ que minimiza a função objetivo. Diante disso, podemos afirmar que uma boa e confiável aproximação para o menor autovalor $\lambda{min}$ da matriz $\mathcal{H}$ pode ser dada por

\begin{equation}
    \langle \mathcal{H}_{\theta'}\rangle_{min} \approx \lambda_{min}.
\end{equation}

Uma explicação intuitiva do método variacional pode ser apresentada da seguinte maneira: O valor esperado (ou valor médio) de uma grandeza, pode assumir infinitos valores entre os respectivos máximo e mínimo. Logo, o valor médio de um observável físico nunca será menor que seu autovalor mínimo e nunca será maior que seu autovalor máximo.

# Otimização de Portfólio usando o QAOA

O problema da otimização de portfólio pode ser modelado por meio de uma \textit{Quadratic Unconstrained Binary Optimization} (QUBO) e transformada para um Modelo de Ising através da transformação de variáveis $\{0,+1\} \rightarrow \{-1,+1\}$.

Explicar (Gleydson/Anton)
- O que é
- Tipos
- Exemplos
- QAOA

- Aplicar à otimização de portfólio

# New Section

```python

```

```python

```