# Full Configuration Interaction theory

 **Morten Hjorth-Jensen**, [National Superconducting Cyclotron Laboratory](http://www.nscl.msu.edu/) and [Department of Physics and Astronomy](https://www.pa.msu.edu/), [Michigan State University](http://www.msu.edu/), East Lansing, MI 48824, USA and Department of Physics, University of Oslo, N-0316 Oslo, Norway

Date: **28th Indian-Summer School on Ab Initio Methods in Nuclear Physics**

## Slater determinants as basis states, short reminder
The simplest possible choice for many-body wavefunctions are **product** wavefunctions.
That is

$$
\Psi(x_1, x_2, x_3, \ldots, x_A) \approx \phi_1(x_1) \phi_2(x_2) \phi_3(x_3) \ldots
$$

because we are really only good  at thinking about one particle at a time. Such
product wavefunctions, without correlations, are easy to
work with; for example, if the single-particle states $\phi_i(x)$ are orthonormal, then
the product wavefunctions are easy to orthonormalize.

Similarly, computing matrix elements of operators are relatively easy, because the
integrals factorize.

The price we pay is the lack of correlations, which we must build up by using many, many product
wavefunctions. (Thus we have a trade-off: compact representation of correlations but
difficult integrals versus easy integrals but many states required.)

## Slater determinants as basis states, repetition
Because we have fermions, we are required to have antisymmetric wavefunctions, e.g.

$$
\Psi(x_1, x_2, x_3, \ldots, x_A) = - \Psi(x_2, x_1, x_3, \ldots, x_A)
$$

etc. This is accomplished formally by using the determinantal formalism

$$
\Psi(x_1, x_2, \ldots, x_A)
= \frac{1}{\sqrt{A!}}
\det \left |
\begin{array}{cccc}
\phi_1(x_1) & \phi_1(x_2) & \ldots & \phi_1(x_A) \\
\phi_2(x_1) & \phi_2(x_2) & \ldots & \phi_2(x_A) \\
 \vdots & & &  \\
\phi_A(x_1) & \phi_A(x_2) & \ldots & \phi_A(x_A)
\end{array}
\right |
$$

Product wavefunction + antisymmetry = Slater determinant.

## Slater determinants as basis states

$$
\Psi(x_1, x_2, \ldots, x_A)
= \frac{1}{\sqrt{N!}}
\det \left |
\begin{array}{cccc}
\phi_1(x_1) & \phi_1(x_2) & \ldots & \phi_1(x_A) \\
\phi_2(x_1) & \phi_2(x_2) & \ldots & \phi_2(x_A) \\
 \vdots & & &  \\
\phi_A(x_1) & \phi_A(x_2) & \ldots & \phi_A(x_A)
\end{array}
\right |
$$

Properties of the determinant (interchange of any two rows or
any two columns yields a change in sign; thus no two rows and no
two columns can be the same) lead to the Pauli principle:

* No two particles can be at the same place (two columns the same); and

* No two particles can be in the same state (two rows the same).

## Slater determinants as basis states
As a practical matter, however, Slater determinants beyond $N=4$ quickly become
unwieldy. Thus we turn to the **occupation representation** or **second quantization** to simplify calculations.

The occupation representation, using fermion **creation** and **annihilation**
operators, is compact and efficient. It is also abstract and, at first encounter, not easy to
internalize. It is inspired by other operator formalism, such as the ladder operators for
the harmonic oscillator or for angular momentum, but unlike those cases, the operators **do not have coordinate space representations**.

Instead, one can think of fermion creation/annihilation operators as a game of symbols that
compactly reproduces what one would do, albeit clumsily, with full coordinate-space Slater
determinants.

## Quick repetition of the occupation representation
We start with a set of orthonormal single-particle states $\{ \phi_i(x) \}$.
(Note: this requirement, and others, can be relaxed, but leads to a
more involved formalism.) **Any** orthonormal set will do.

To each single-particle state $\phi_i(x)$ we associate a creation operator
$\hat{a}^\dagger_i$ and an annihilation operator $\hat{a}_i$.

When acting on the vacuum state $| 0 \rangle$, the creation operator $\hat{a}^\dagger_i$ causes
a particle to occupy the single-particle state $\phi_i(x)$:

$$
\phi_i(x) \rightarrow \hat{a}^\dagger_i |0 \rangle
$$

## Quick repetition  of the occupation representation
But with multiple creation operators we can occupy multiple states:

$$
\phi_i(x) \phi_j(x^\prime) \phi_k(x^{\prime \prime})
\rightarrow \hat{a}^\dagger_i \hat{a}^\dagger_j \hat{a}^\dagger_k |0 \rangle.
$$

Now we impose antisymmetry, by having the fermion operators satisfy  **anticommutation relations**:

$$
\hat{a}^\dagger_i \hat{a}^\dagger_j + \hat{a}^\dagger_j \hat{a}^\dagger_i
= [ \hat{a}^\dagger_i ,\hat{a}^\dagger_j ]_+
= \{ \hat{a}^\dagger_i ,\hat{a}^\dagger_j \} = 0
$$

so that

$$
\hat{a}^\dagger_i \hat{a}^\dagger_j = - \hat{a}^\dagger_j \hat{a}^\dagger_i
$$

## Quick repetition  of the occupation representation
Because of this property, automatically $\hat{a}^\dagger_i \hat{a}^\dagger_i = 0$,
enforcing the Pauli exclusion principle.  Thus when writing a Slater determinant
using creation operators,

$$
\hat{a}^\dagger_i \hat{a}^\dagger_j \hat{a}^\dagger_k \ldots |0 \rangle
$$

each index $i,j,k, \ldots$ must be unique.

## Full Configuration Interaction Theory
We have defined the ansatz for the ground state as

$$
|\Phi_0\rangle = \left(\prod_{i\le F}\hat{a}_{i}^{\dagger}\right)|0\rangle,
$$

where the index $i$ defines different single-particle states up to the Fermi level. We have assumed that we have $N$ fermions.
A given one-particle-one-hole ($1p1h$) state can be written as

$$
|\Phi_i^a\rangle = \hat{a}_{a}^{\dagger}\hat{a}_i|\Phi_0\rangle,
$$

while a $2p2h$ state can be written as

$$
|\Phi_{ij}^{ab}\rangle = \hat{a}_{a}^{\dagger}\hat{a}_{b}^{\dagger}\hat{a}_j\hat{a}_i|\Phi_0\rangle,
$$

and a general $NpNh$ state as

$$
|\Phi_{ijk\dots}^{abc\dots}\rangle = \hat{a}_{a}^{\dagger}\hat{a}_{b}^{\dagger}\hat{a}_{c}^{\dagger}\dots\hat{a}_k\hat{a}_j\hat{a}_i|\Phi_0\rangle.
$$

## Full Configuration Interaction Theory
We can then expand our exact state function for the ground state
as

$$
|\Psi_0\rangle=C_0|\Phi_0\rangle+\sum_{ai}C_i^a|\Phi_i^a\rangle+\sum_{abij}C_{ij}^{ab}|\Phi_{ij}^{ab}\rangle+\dots
=(C_0+\hat{C})|\Phi_0\rangle,
$$

where we have introduced the so-called correlation operator

$$
\hat{C}=\sum_{ai}C_i^a\hat{a}_{a}^{\dagger}\hat{a}_i  +\sum_{abij}C_{ij}^{ab}\hat{a}_{a}^{\dagger}\hat{a}_{b}^{\dagger}\hat{a}_j\hat{a}_i+\dots
$$

Since the normalization of $\Psi_0$ is at our disposal and since $C_0$ is by hypothesis non-zero, we may arbitrarily set $C_0=1$ with
corresponding proportional changes in all other coefficients. Using this so-called intermediate normalization we have

$$
\langle \Psi_0 | \Phi_0 \rangle = \langle \Phi_0 | \Phi_0 \rangle = 1,
$$

resulting in

$$
|\Psi_0\rangle=(1+\hat{C})|\Phi_0\rangle.
$$

## Full Configuration Interaction Theory
We rewrite

$$
|\Psi_0\rangle=C_0|\Phi_0\rangle+\sum_{ai}C_i^a|\Phi_i^a\rangle+\sum_{abij}C_{ij}^{ab}|\Phi_{ij}^{ab}\rangle+\dots,
$$

in a more compact form as

$$
|\Psi_0\rangle=\sum_{PH}C_H^P\Phi_H^P=\left(\sum_{PH}C_H^P\hat{A}_H^P\right)|\Phi_0\rangle,
$$

where $H$ stands for $0,1,\dots,n$ hole states and $P$ for $0,1,\dots,n$ particle states.
Our requirement of unit normalization gives

$$
\langle \Psi_0 | \Phi_0 \rangle = \sum_{PH}|C_H^P|^2= 1,
$$

and the energy can be written as

$$
E= \langle \Psi_0 | \hat{H} |\Phi_0 \rangle= \sum_{PP'HH'}C_H^{*P}\langle \Phi_H^P | \hat{H} |\Phi_{H'}^{P'} \rangle C_{H'}^{P'}.
$$

## Full Configuration Interaction Theory
Normally

$$
E= \langle \Psi_0 | \hat{H} |\Phi_0 \rangle= \sum_{PP'HH'}C_H^{*P}\langle \Phi_H^P | \hat{H} |\Phi_{H'}^{P'} \rangle C_{H'}^{P'},
$$

is solved by diagonalization setting up the Hamiltonian matrix defined by the basis of all possible Slater determinants. A diagonalization

is equivalent to finding the variational minimum   of

$$
\langle \Psi_0 | \hat{H} |\Phi_0 \rangle-\lambda \langle \Psi_0 |\Phi_0 \rangle,
$$

where $\lambda$ is a variational multiplier to be identified with the energy of the system.
The minimization process results in

2
3

<
<
<
!
!
M
A
T
H
_
B
L
O
C
K

$$
\sum_{P'H'}\left\{\delta[C_H^{*P}]\langle \Phi_H^P | \hat{H} |\Phi_{H'}^{P'} \rangle C_{H'}^{P'}+
C_H^{*P}\langle \Phi_H^P | \hat{H} |\Phi_{H'}^{P'} \rangle \delta[C_{H'}^{P'}]-
\lambda( \delta[C_H^{*P}]C_{H'}^{P'}+C_H^{*P}\delta[C_{H'}^{P'}]\right\} = 0.
$$

Since the coefficients $\delta[C_H^{*P}]$ and $\delta[C_{H'}^{P'}]$ are complex conjugates it is necessary and sufficient to require the quantities that multiply with $\delta[C_H^{*P}]$ to vanish.

## Full Configuration Interaction Theory

This leads to

$$
\sum_{P'H'}\langle \Phi_H^P | \hat{H} |\Phi_{H'}^{P'} \rangle C_{H'}^{P'}-\lambda C_H^{P}=0,
$$

for all sets of $P$ and $H$.

If we then multiply by the corresponding $C_H^{*P}$ and sum over $PH$ we obtain

$$
\sum_{PP'HH'}C_H^{*P}\langle \Phi_H^P | \hat{H} |\Phi_{H'}^{P'} \rangle C_{H'}^{P'}-\lambda\sum_{PH}|C_H^P|^2=0,
$$

leading to the identification $\lambda = E$. This means that we have for all $PH$ sets

$$
\begin{equation}
\sum_{P'H'}\langle \Phi_H^P | \hat{H} -E|\Phi_{H'}^{P'} \rangle = 0. \label{eq:fullci} \tag{1}
\end{equation}
$$

## Full Configuration Interaction Theory
An alternative way to derive the last equation is to start from

$$
(\hat{H} -E)|\Psi_0\rangle = (\hat{H} -E)\sum_{P'H'}C_{H'}^{P'}|\Phi_{H'}^{P'} \rangle=0,
$$

and if this equation is successively projected against all $\Phi_H^P$ in the expansion of $\Psi$, then the last equation on the previous slide
results.   As stated previously, one solves this equation normally by diagonalization. If we are able to solve this equation exactly (that is
numerically exactly) in a large Hilbert space (it will be truncated in terms of the number of single-particle states included in the definition
of Slater determinants), it can then serve as a benchmark for other many-body methods which approximate the correlation operator
$\hat{C}$.

## Example of a Hamiltonian matrix
Suppose, as an example, that we have six fermions below the Fermi level.
This means that we can make at most $6p-6h$ excitations. If we have an infinity of single particle states above the Fermi level, we will obviously have an infinity of say $2p-2h$ excitations. Each such way to configure the particles is called a **configuration**. We will always have to truncate in the basis of single-particle states.
This gives us a finite number of possible Slater determinants. Our Hamiltonian matrix would then look like (where each block can have a large dimensionalities):

       	 $0p-0h$	 $1p-1h$	 $2p-2h$	 $3p-3h$	 $4p-4h$	 $5p-5h$	 $6p-6h$

   $0p-0h$    	    x          	    x          	    x          	    0          	    0          	    0          	    0

   $1p-1h$    	    x          	    x          	    x          	    x          	    0          	    0          	    0

   $2p-2h$    	    x          	    x          	    x          	    x          	    x          	    0          	    0

   $3p-3h$    	    0          	    x          	    x          	    x          	    x          	    x          	    0

   $4p-4h$    	    0          	    0          	    x          	    x          	    x          	    x          	    x

   $5p-5h$    	    0          	    0          	    0          	    x          	    x          	    x          	    x

   $6p-6h$    	    0          	    0          	    0          	    0          	    x          	    x          	    x

with a two-body force. Why are there non-zero blocks of elements?

## Example of a Hamiltonian matrix with a Hartree-Fock basis
If we use a Hartree-Fock basis, this corresponds to a particular unitary transformation where matrix elements of the type $\langle 0p-0h \vert \hat{H} \vert 1p-1h\rangle =\langle \Phi_0 | \hat{H}|\Phi_{i}^{a}\rangle=0$ and our Hamiltonian matrix becomes

       	   $0p-0h$  	   $1p-1h$  	   $2p-2h$  	   $3p-3h$  	   $4p-4h$  	   $5p-5h$  	   $6p-6h$

   $0p-0h$    	    $\tilde{x}$    	    0              	    $\tilde{x}$    	    0              	    0              	    0              	    0

   $1p-1h$    	    0              	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    0              	    0              	    0

   $2p-2h$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    0              	    0

   $3p-3h$    	    0              	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    0

   $4p-4h$    	    0              	    0              	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$

   $5p-5h$    	    0              	    0              	    0              	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$

   $6p-6h$    	    0              	    0              	    0              	    0              	    $\tilde{x}$    	    $\tilde{x}$    	    $\tilde{x}$

## Shell-model jargon
If we do not make any truncations in the possible sets of Slater determinants (many-body states) we can make by distributing $A$ nucleons among $n$ single-particle states, we call such a calculation for **Full configuration interaction theory**

If we make truncations, we have different possibilities

* The standard nuclear shell-model. Here we define an effective Hilbert space with respect to a given core. The calculations are normally then performed for all many-body states that can be constructed from the effective Hilbert spaces. This approach requires a properly defined effective Hamiltonian

* We can truncate in the number of excitations. For example, we can limit the possible Slater determinants to only $1p-1h$ and $2p-2h$ excitations. This is called a configuration interaction calculation at the level of singles and doubles excitations, or just CISD.

* We can limit the number of excitations in terms of the excitation energies. If we do not define a core, this defines normally what is called the no-core shell-model approach.

What happens if we have a three-body interaction and a Hartree-Fock basis?

## FCI and the exponential growth
Full configuration interaction theory calculations provide in principle, if we can diagonalize numerically, all states of interest. The dimensionality of the problem explodes however quickly.

The total number of Slater determinants which can be built with say $N$ neutrons distributed among $n$ single particle states is

$$
\left (\begin{array}{c} n \\ N\end{array} \right) =\frac{n!}{(n-N)!N!}.
$$

As an example, for a model space which comprises the first four
  major harmonic oscillator shells only, that is the $0s$, $0p$,
  $1s0d$ and $1p0f$ shells we have $40$ single particle states for
  neutrons and protons.  For the eight neutrons of oxygen-16 we would
  then have

$$
\left (\begin{array}{c} 40 \\ 8\end{array} \right)
    =\frac{40!}{(32)!8!}\sim 8\times 10^{7},
$$

possible Slater determinants. Multiplying this with the number of
  proton Slater determinants we end up with approximately $d\sim 10^{15}$
  possible Slater determinants and a Hamiltonian matrix of
  dimension $10^{15}\times 10^{15}$, an intractable problem if we wish to diagonalize the Hamiltonian matrix.

## Exponential wall
This number can be reduced if we look at specific symmetries only. However, the dimensionality explodes quickly!

* For Hamiltonian matrices of dimensionalities  which are smaller than $d\sim 10^5$, we would use so-called direct methods for diagonalizing the Hamiltonian matrix

* For larger dimensionalities iterative eigenvalue solvers like Lanczos' method are used. The most efficient codes at present can handle matrices of $d\sim 10^{10}$.

## A non-practical way of solving the eigenvalue problem
To see this, we look at the contributions arising from

$$
\langle \Phi_H^P | = \langle \Phi_0|
$$

in  Eq. ([eq:fullci](#eq:fullci)), that is we multiply with $\langle \Phi_0 |$
from the left in

$$
(\hat{H} -E)\sum_{P'H'}C_{H'}^{P'}|\Phi_{H'}^{P'} \rangle=0.
$$

If we assume that we have a two-body operator at most, Slater's rule gives then an equation for the
correlation energy in terms of $C_i^a$ and $C_{ij}^{ab}$ only.  We get then

$$
\langle \Phi_0 | \hat{H} -E| \Phi_0\rangle + \sum_{ai}\langle \Phi_0 | \hat{H} -E|\Phi_{i}^{a} \rangle C_{i}^{a}+
\sum_{abij}\langle \Phi_0 | \hat{H} -E|\Phi_{ij}^{ab} \rangle C_{ij}^{ab}=0,
$$

or

$$
E-E_0 =\Delta E=\sum_{ai}\langle \Phi_0 | \hat{H}|\Phi_{i}^{a} \rangle C_{i}^{a}+
\sum_{abij}\langle \Phi_0 | \hat{H}|\Phi_{ij}^{ab} \rangle C_{ij}^{ab},
$$

where the energy $E_0$ is the reference energy and $\Delta E$ defines the so-called correlation energy.
The single-particle basis functions  could be the results of a Hartree-Fock calculation or just the eigenstates of the non-interacting part of the Hamiltonian.

## A non-practical way of solving the eigenvalue problem
To see this, we look at the contributions arising from

$$
\langle \Phi_H^P | = \langle \Phi_0|
$$

in  Eq. ([eq:fullci](#eq:fullci)), that is we multiply with $\langle \Phi_0 |$
from the left in

$$
(\hat{H} -E)\sum_{P'H'}C_{H'}^{P'}|\Phi_{H'}^{P'} \rangle=0.
$$

## A non-practical way of solving the eigenvalue problem
If we assume that we have a two-body operator at most, Slater's rule gives then an equation for the
correlation energy in terms of $C_i^a$ and $C_{ij}^{ab}$ only.  We get then

$$
\langle \Phi_0 | \hat{H} -E| \Phi_0\rangle + \sum_{ai}\langle \Phi_0 | \hat{H} -E|\Phi_{i}^{a} \rangle C_{i}^{a}+
\sum_{abij}\langle \Phi_0 | \hat{H} -E|\Phi_{ij}^{ab} \rangle C_{ij}^{ab}=0,
$$

or

$$
E-E_0 =\Delta E=\sum_{ai}\langle \Phi_0 | \hat{H}|\Phi_{i}^{a} \rangle C_{i}^{a}+
\sum_{abij}\langle \Phi_0 | \hat{H}|\Phi_{ij}^{ab} \rangle C_{ij}^{ab},
$$

where the energy $E_0$ is the reference energy and $\Delta E$ defines the so-called correlation energy.
The single-particle basis functions  could be the results of a Hartree-Fock calculation or just the eigenstates of the non-interacting part of the Hamiltonian.

## Rewriting the FCI equation
In our notes on Hartree-Fock calculations,
we have already computed the matrix $\langle \Phi_0 | \hat{H}|\Phi_{i}^{a}\rangle $ and $\langle \Phi_0 | \hat{H}|\Phi_{ij}^{ab}\rangle$.  If we are using a Hartree-Fock basis, then the matrix elements
$\langle \Phi_0 | \hat{H}|\Phi_{i}^{a}\rangle=0$ and we are left with a *correlation energy* given by

$$
E-E_0 =\Delta E^{HF}=\sum_{abij}\langle \Phi_0 | \hat{H}|\Phi_{ij}^{ab} \rangle C_{ij}^{ab}.
$$

## Rewriting the FCI equation
Inserting the various matrix elements we can rewrite the previous equation as

$$
\Delta E=\sum_{ai}\langle i| \hat{f}|a \rangle C_{i}^{a}+
\sum_{abij}\langle ij | \hat{v}| ab \rangle C_{ij}^{ab}.
$$

This equation determines the correlation energy but not the coefficients $C$.

## Rewriting the FCI equation, does not stop here
We need more equations. Our next step is to set up

$$
\langle \Phi_i^a | \hat{H} -E| \Phi_0\rangle + \sum_{bj}\langle \Phi_i^a | \hat{H} -E|\Phi_{j}^{b} \rangle C_{j}^{b}+
\sum_{bcjk}\langle \Phi_i^a | \hat{H} -E|\Phi_{jk}^{bc} \rangle C_{jk}^{bc}+
\sum_{bcdjkl}\langle \Phi_i^a | \hat{H} -E|\Phi_{jkl}^{bcd} \rangle C_{jkl}^{bcd}=0,
$$

as this equation will allow us to find an expression for the coefficents $C_i^a$ since we can rewrite this equation as

$$
\langle i | \hat{f}| a\rangle +\langle \Phi_i^a | \hat{H}|\Phi_{i}^{a} \rangle C_{i}^{a}+ \sum_{bj\ne ai}\langle \Phi_i^a | \hat{H}|\Phi_{j}^{b} \rangle C_{j}^{b}+
\sum_{bcjk}\langle \Phi_i^a | \hat{H}|\Phi_{jk}^{bc} \rangle C_{jk}^{bc}+
\sum_{bcdjkl}\langle \Phi_i^a | \hat{H}|\Phi_{jkl}^{bcd} \rangle C_{jkl}^{bcd}=EC_i^a.
$$

## Rewriting the FCI equation, please stop here
We see that on the right-hand side we have the energy $E$. This leads to a non-linear equation in the unknown coefficients.
These equations are normally solved iteratively ( that is we can start with a guess for the coefficients $C_i^a$). A common choice is to use perturbation theory for the first guess, setting thereby

$$
C_{i}^{a}=\frac{\langle i | \hat{f}| a\rangle}{\epsilon_i-\epsilon_a}.
$$

## Rewriting the FCI equation, more to add
The observant reader will however see that we need an equation for $C_{jk}^{bc}$ and $C_{jkl}^{bcd}$ as well.
To find equations for these coefficients we need then to continue our multiplications from the left with the various
$\Phi_{H}^P$ terms.

For $C_{jk}^{bc}$ we need then

4
4

<
<
<
!
!
M
A
T
H
_
B
L
O
C
K

$$
\sum_{cdkl}\langle \Phi_{ij}^{ab} | \hat{H} -E|\Phi_{kl}^{cd} \rangle C_{kl}^{cd}+\sum_{cdeklm}\langle \Phi_{ij}^{ab} | \hat{H} -E|\Phi_{klm}^{cde} \rangle C_{klm}^{cde}+\sum_{cdefklmn}\langle \Phi_{ij}^{ab} | \hat{H} -E|\Phi_{klmn}^{cdef} \rangle C_{klmn}^{cdef}=0,
$$

and we can isolate the coefficients $C_{kl}^{cd}$ in a similar way as we did for the coefficients $C_{i}^{a}$.

## Rewriting the FCI equation, more to add
A standard choice for the first iteration is to set

$$
C_{ij}^{ab} =\frac{\langle ij \vert \hat{v} \vert ab \rangle}{\epsilon_i+\epsilon_j-\epsilon_a-\epsilon_b}.
$$

At the end we can rewrite our solution of the Schroedinger equation in terms of $n$ coupled equations for the coefficients $C_H^P$.
This is a very cumbersome way of solving the equation. However, by using this iterative scheme we can illustrate how we can compute the
various terms in the wave operator or correlation operator $\hat{C}$. We will later identify the calculation of the various terms $C_H^P$
as parts of different many-body approximations to full CI. In particular, we can  relate this non-linear scheme with Coupled Cluster theory and
many-body perturbation theory.

## Summarizing FCI and bringing in approximative methods

If we can diagonalize large matrices, FCI is the method of choice since:
* It gives all eigenvalues, ground state and excited states

* The eigenvectors are obtained directly from the coefficients $C_H^P$ which result from the diagonalization

* We can easily compute expectation values of other operators, as well as transition probabilities

* Correlations are easy to understand in terms of contributions to a given operator beyond the Hartree-Fock contribution. This is the standard approach in  many-body theory.

## Definition of the correlation energy
The correlation energy is defined as, with a two-body Hamiltonian,

$$
\Delta E=\sum_{ai}\langle i| \hat{f}|a \rangle C_{i}^{a}+
\sum_{abij}\langle ij | \hat{v}| ab \rangle C_{ij}^{ab}.
$$

The coefficients $C$ result from the solution of the eigenvalue problem.
The energy of say the ground state is then

$$
E=E_{ref}+\Delta E,
$$

where the so-called reference energy is the energy we obtain from a Hartree-Fock calculation, that is

$$
E_{ref}=\langle \Phi_0 \vert \hat{H} \vert \Phi_0 \rangle.
$$

## FCI equation and the coefficients

However, as we have seen, even for a small case like the four first major shells and a nucleus like oxygen-16, the dimensionality becomes quickly intractable. If we wish to include single-particle states that reflect weakly bound systems, we need a much larger single-particle basis. We need thus approximative methods that sum specific correlations to infinite order.

Popular methods are
* [Many-body perturbation theory (in essence a Taylor expansion)](http://www.sciencedirect.com/science/article/pii/0370157395000126)

* [Coupled cluster theory (coupled non-linear equations)](http://iopscience.iop.org/article/10.1088/0034-4885/77/9/096302/meta)

* Green's function approaches (matrix inversion)

* [Similarity group transformation methods (coupled ordinary differential equations)](http://journals.aps.org/prl/abstract/10.1103/PhysRevLett.106.222502)

All these methods start normally with a Hartree-Fock basis as the calculational basis.

## Building a many-body basis
Here we will discuss how we can set up a single-particle basis which we can use in the various parts of our projects, from the simple pairing model to infinite nuclear matter. We will use here the simple pairing model to illustrate in particular how to set up a single-particle basis. We will also use this do discuss standard FCI approaches like:
 1. Standard shell-model basis in one or two major shells

 2. Full CI in a given basis and no truncations

 3. CISD and CISDT approximations

 4. No-core shell model and truncation in excitation energy

## Building a many-body basis
An important step in an FCI code  is to construct the many-body basis.

While the formalism is independent of the choice of basis, the **effectiveness** of a calculation
will certainly be basis dependent.

Furthermore there are common conventions useful to know.

First, the single-particle basis has angular momentum as a good quantum number.  You can
imagine the single-particle wavefunctions being generated by a one-body Hamiltonian,
for example a harmonic oscillator.  Modifications include harmonic oscillator plus
spin-orbit splitting, or self-consistent mean-field potentials, or the Woods-Saxon potential which mocks
up the self-consistent mean-field.
For nuclei, the harmonic oscillator, modified by spin-orbit splitting, provides a useful language
for describing single-particle states.

## Building a many-body basis
Each single-particle state is labeled by the following quantum numbers:

* Orbital angular momentum $l$

* Intrinsic spin $s$ = 1/2 for protons and neutrons

* Angular momentum $j = l \pm 1/2$

* $z$-component $j_z$ (or $m$)

* Some labeling of the radial wavefunction, typically $n$ the number of nodes in  the radial wavefunction, but in the case of harmonic oscillator one can also use the principal quantum number $N$, where the harmonic oscillator energy is $(N+3/2)\hbar \omega$.

In this format one labels states by $n(l)_j$, with $(l)$ replaced by a letter:
$s$ for $l=0$, $p$ for $l=1$, $d$ for $l=2$, $f$ for $l=3$, and thenceforth alphabetical.

## Building a many-body basis
 In practice the single-particle space has to be severely truncated.  This truncation is
typically based upon the single-particle energies, which is the effective energy
from a mean-field potential.

Sometimes we freeze the core and only consider a valence space. For example, one
may assume a frozen ${}^{4}\mbox{He}$ core, with two protons and two neutrons in the $0s_{1/2}$
shell, and then only allow active particles in the $0p_{1/2}$ and $0p_{3/2}$ orbits.

Another example is a frozen ${}^{16}\mbox{O}$ core, with eight protons and eight neutrons filling the
$0s_{1/2}$,  $0p_{1/2}$ and $0p_{3/2}$ orbits, with valence particles in the
$0d_{5/2}, 1s_{1/2}$ and $0d_{3/2}$ orbits.

Sometimes we refer to nuclei by the valence space where their last nucleons go.
So, for example, we call ${}^{12}\mbox{C}$ a $p$-shell nucleus, while ${}^{26}\mbox{Al}$ is an
$sd$-shell nucleus and ${}^{56}\mbox{Fe}$ is a $pf$-shell nucleus.

## Building a many-body basis
There are different kinds of truncations.

* For example, one can start with `filled' orbits (almost always the lowest), and then  allow one, two, three... particles excited out of those filled orbits. These are called  1p-1h, 2p-2h, 3p-3h excitations.

* Alternately, one can state a maximal orbit and allow all possible configurations with  particles occupying states up to that maximum. This is called *full configuration*.

* Finally, for particular use in nuclear physics, there is the *energy* truncation, also  called the $N\hbar\Omega$ or $N_{max}$ truncation.

## Building a many-body basis
Here one works in a harmonic oscillator basis, with each major oscillator shell assigned  a principal quantum number $N=0,1,2,3,...$.
The $N\hbar\Omega$ or $N_{max}$ truncation: Any configuration is given an noninteracting energy, which is the sum
of the single-particle harmonic oscillator energies. (Thus this ignores
spin-orbit splitting.)

Excited state are labeled relative to the lowest configuration by the
number of harmonic oscillator quanta.

This truncation is useful because if one includes *all* configuration up to
some $N_{max}$, and has a translationally invariant interaction, then the intrinsic
motion and the center-of-mass motion factor. In other words, we can know exactly
the center-of-mass wavefunction.

In almost all cases, the many-body Hamiltonian is rotationally invariant. This means
it commutes with the operators $\hat{J}^2, \hat{J}_z$ and so eigenstates will have
good $J,M$. Furthermore, the eigenenergies do not depend upon the orientation $M$.

Therefore we can choose to construct a many-body basis which has fixed $M$; this is
called an $M$-scheme basis.

Alternately, one can construct a many-body basis which has fixed $J$, or a $J$-scheme
basis.

## Building a many-body basis
The Hamiltonian matrix will have smaller dimensions (a factor of 10 or more) in the $J$-scheme than in the $M$-scheme.
On the other hand, as we'll show in the next slide, the $M$-scheme is very easy to
construct with Slater determinants, while the $J$-scheme basis states, and thus the
matrix elements, are more complicated, almost always being linear combinations of
$M$-scheme states. $J$-scheme bases are important and useful, but we'll focus on the
simpler $M$-scheme.

The quantum number $m$ is additive (because the underlying group is Abelian):
if a Slater determinant $\hat{a}_i^\dagger \hat{a}^\dagger_j \hat{a}^\dagger_k \ldots | 0 \rangle$
is built from single-particle states all with good $m$, then the total

$$
M = m_i + m_j + m_k + \ldots
$$

This is *not* true of $J$, because the angular momentum group SU(2) is not Abelian.

## Building a many-body basis

The upshot is that
* It is easy to construct a Slater determinant with good total $M$;

* It is trivial to calculate $M$ for each Slater determinant;

* So it is easy to construct an $M$-scheme basis with fixed total $M$.

Note that the individual $M$-scheme basis states will *not*, in general,
have good total $J$.
Because the Hamiltonian is rotationally invariant, however, the eigenstates will
have good $J$. (The situation is muddied when one has states of different $J$ that are
nonetheless degenerate.)

## Building a many-body basis
Example: two $j=1/2$ orbits

Index	 $n$	 $l$	 $j$	 $m_j$

   1        	    0      	    0      	    1/2    	    -1/2

   2        	    0      	    0      	    1/2    	    1/2

   3        	    1      	    0      	    1/2    	    -1/2

   4        	    1      	    0      	    1/2    	    1/2

Note that the order is arbitrary.

## Building a many-body basis
There are $\left ( \begin{array}{c} 4 \\ 2 \end{array} \right) = 6$ two-particle states,
which we list with the total $M$:

Occupied	 $M$

   1,2         	    0

   1,3         	    -1

   1,4         	    0

   2,3         	    0

   2,4         	    1

   3,4         	    0

There are 4 states with $M= 0$,
and 1 each with $M = \pm 1$.

## Building a many-body basis
As another example, consider using only single particle states from the $0d_{5/2}$ space.
They have the following quantum numbers

Index	 $n$	 $l$	 $j$	 $m_j$

   1        	    0      	    2      	    5/2    	    -5/2

   2        	    0      	    2      	    5/2    	    -3/2

   3        	    0      	    2      	    5/2    	    -1/2

   4        	    0      	    2      	    5/2    	    1/2

   5        	    0      	    2      	    5/2    	    3/2

   6        	    0      	    2      	    5/2    	    5/2

## Building a many-body basis
There are $\left ( \begin{array}{c} 6 \\ 2 \end{array} \right) = 15$ two-particle states,
which we list with the total $M$:

Occupied	 $M$	 Occupied	 $M$	 Occupied	 $M$

   1,2         	    -4     	    2,3         	    -2     	    3,5         	    1

   1,3         	    -3     	    2,4         	    -1     	    3,6         	    2

   1,4         	    -2     	    2,5         	    0      	    4,5         	    2

   1,5         	    -1     	    2,6         	    1      	    4,6         	    3

   1,6         	    0      	    3,4         	    0      	    5,6         	    4

There are 3 states with $M= 0$, 2 with $M = 1$, and so on.


# **[HW6] DCGAN**
1. DataLoader
2. Model
3. Inception Score
4. Trainer
5. Train

이번 실습에서는 Convolution기반의 Generative Adversarial Network를 구현해서 이미지를 직접 생성해보는 실습을 진행해보겠습니다.

- dataset: CIFAR-10 (https://www.cs.toronto.edu/~kriz/cifar.html)
- model: DCGAN (https://arxiv.org/abs/1511.06434)
- evaluation: Inception Score (https://arxiv.org/abs/1801.01973)

## Import packages

런타임의 유형을 변경해줍니다.

상단 메뉴에서 [런타임]->[런타임유형변경]->[하드웨어가속기]->[GPU]

변경 이후 아래의 cell을 실행 시켰을 때, torch.cuda.is_avialable()이 True가 나와야 합니다.

```python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torch.optim as optim
print(torch.__version__)
print(torch.cuda.is_available())
```

    1.10.0+cu111
    True

```python
import matplotlib.pyplot as plt
import numpy as np
import scipy as sp
import tqdm
import os
import random
import time
import datetime
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split

# for reproducibility
np.set_printoptions(precision=3)
np.set_printoptions(suppress=True)
random.seed(1234)
torch.manual_seed(1234)
np.random.seed(1234)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
```

# 1. DataLoader

이전의 실습들에서 사용한것과 마찬가지로, pre-defined된 CIFAR-10 dataset을 활용해서 dataloader를 만들어 두겠습니다.

```python
from PIL import Image
from torch.utils import data
import torchvision
import torchvision.transforms as transforms

def create_dataloader(batch_size=64, num_workers=1):
    transform = transforms.Compose([transforms.ToTensor(),
                                    transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))])

    trainset = torchvision.datasets.CIFAR10(root='./data/', train=True, transform=transform, download=True)
    testset = torchvision.datasets.CIFAR10(root='./data/', train=False, transform=transform, download=True)

    trainloader = data.DataLoader(dataset=trainset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    testloader = data.DataLoader(dataset=testset, batch_size=batch_size, shuffle=False, num_workers=num_workers)

    return trainloader, testloader
```

# 2. Model

이번 section에서는 DCGAN의 모델구조를 직접 구현해보도록 하겠습니다.

우선 본격적인 모델 구현에 앞서 GAN의 전체적인 구조에 대해 살펴보겠습니다.

GAN은 Generator와 Discriminator로 구성되어, Generator는 random latent vector를 받아 Discriminator를 속일 수 있는 fake image를 만들고, Discriminator는 real image와 fake image를 구분하는 형태로 학습이 진행되게 됩니다.

DCGAN은 image 데이터 처리에 효과적인 convolution layer를 활용하여 Generator와 Discriminator의 구조를 변형한 모델입니다.

DCGAN의 Generator와 Discriminator의 구조는 아래와 같습니다.

이 때, Generator는 output의 width와 height를 키우는 convolution을 진행해주어야 하기 때문에, standard한 convolution operation이 아닌 deconvolution 혹은 transpose convolution이라고 불리는 연산을 통해 output의 size를 키워주는 연산을 진행하게 됩니다.

반대로, Discriminator는 Generator와 대칭되는 구조를 통해 standard한 convolution을 사용하여 classification을 진행해주게 됩니다.

Transpose Convolution:(https://pytorch.org/docs/stable/generated/torch.nn.ConvTranspose2d.html)

## Convolution Block

우선, 모델을 쉽게 구현할 수 있도록, Generator와 Discriminator에서 반복적으로 사용할 convolution block고 deconvolution block을 정의해두도록 하겠습니다.

```python
def conv(c_in, c_out, k_size, stride=2, pad=1, bias=False, norm='bn', activation=None):
    layers = []

    # Conv.
    layers.append(nn.Conv2d(c_in, c_out, k_size, stride, pad, bias=bias))

    # Normalization
    if norm == 'bn':
        layers.append(nn.BatchNorm2d(c_out))
    elif norm == None:
        pass

    # Activation
    if activation == 'lrelu':
        layers.append(nn.LeakyReLU(0.2))
    elif activation == 'relu':
        layers.append(nn.ReLU())
    elif activation == 'tanh':
        layers.append(nn.Tanh())
    elif activation == 'sigmoid':
        layers.append(nn.Sigmoid())
    elif activation == None:
        pass

    return nn.Sequential(*layers)

def deconv(c_in, c_out, k_size, stride=2, pad=1, output_padding=0, bias=False, norm='bn', activation=None):
    layers = []

    # Deconv.
    layers.append(nn.ConvTranspose2d(c_in, c_out, k_size, stride, pad, output_padding, bias=bias))

    # Normalization
    if norm == 'bn':
        layers.append(nn.BatchNorm2d(c_out))
    elif norm == None:
        pass

    # Activation
    if activation == 'lrelu':
        layers.append(nn.LeakyReLU(0.2))
    elif activation == 'relu':
        layers.append(nn.ReLU())
    elif activation == 'tanh':
        layers.append(nn.Tanh())
    elif activation == 'sigmoid':
        layers.append(nn.Sigmoid())
    elif activation == None:
        pass

    return nn.Sequential(*layers)
```

## Generator

이제, 위에서 정의한 deconv block을 활용해서 DCGAN의 Generator를 구현해보도록 하겠습니다.

```python
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        model = []

        ### DCGAN Generator
        # You have to implement 4-layers generator.
        # Note: Recommend to use 'deconv' function
        ### YOUR CODE HERE (~ 4 lines)
        model += [deconv(256, 256, 4, 1, 0, norm='bn', activation='relu'),
                  deconv(256, 128, 4, norm='bn', activation='relu'),
                  deconv(128, 64, 4, norm='bn', activation='relu'),
                  deconv(64, 3, 4, norm=None, activation='tanh')]
        ### END YOUR CODE
        self.model = nn.Sequential(*model)

    def forward(self, z):
        # Input (z) size : [Batch, 256, 1, 1]
        # Output (Image) size : [Batch, 3, 32, 32]
        z = z.view(z.size(0), z.size(1), 1, 1)
        output = self.model(z)

        return output
```

## Discriminator

이제, 위에서 정의한 conv block을 활용해서 DCGAN의 Discriminator를 구현해보도록 하겠습니다.

```python
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        model = []

        ### DCGAN Discriminator
        # You have to implement 4-layers discriminator.
        # Note: Recommend to use 'conv' function
        ### YOUR CODE HERE (~ 4 lines)
        model += [conv(3, 64, 4, norm='bn', activation='lrelu'),
                  conv(64, 128, 4, norm='bn', activation='lrelu'),
                  conv(128, 256, 4, norm='bn', activation='lrelu'),
                  conv(256, 1, 4, 1, 0, norm=None, activation='sigmoid')]
        ### END YOUR CODE
        self.model = nn.Sequential(*model)

    def forward(self, x: torch.Tensor):
        # Input (z) size : [Batch, 3, 32, 32]
        # Output (probability) size : [Batch, 1]
        output = self.model(x).squeeze()

        return output
```

## Implementation Test

이제 Generator와 Discriminator를 맞게 구현했는지 test해보도록 하겠습니다.

체크를 위해서 코드와 함께 주어졌던 두개의 파일
- sanity_check_dcgan_netG.pth
- sanity_check_dcgan_netD.pth

를 왼쪽 상단에 [파일]->[세션 저장소에 업로드]를 눌러 업로드 하고, \\
아래의 코드를 실행시켜 코드가 통과되면 성공입니다.

```python
def test_model():
    print("=====Model Initializer Test Case======")
    netG = Generator()
    # the first test
    try:
        netG.load_state_dict(torch.load("/content/drive/MyDrive/Colab_CS/sanity_check_dcgan_netG.pth", map_location='cpu'))
    except Exception as e:
        print("Your DCGAN generator initializer is wrong. Check the comments in details and implement the model precisely.")
        raise e
    print("The first test passed!")

    # the second test
    netD = Discriminator()
    try:
        netD.load_state_dict(torch.load("/content/drive/MyDrive/Colab_CS/sanity_check_dcgan_netD.pth", map_location='cpu'))
    except Exception as e:
        print("Your DCGAN discriminator initializer is wrong. Check the comments in details and implement the model precisely.")
        raise e
    print("The second test passed!")
    print("All 2 tests passed!")

test_model()
```

    =====Model Initializer Test Case======
    The first test passed!
    The second test passed!
    All 2 tests passed!

```python
from google.colab import drive
drive.mount('/content/drive')
```

    Mounted at /content/drive

# 3. Inception Score

비록 이제 dataloader와 model을 구현하였지만, 본격적으로 학습을 진행하기전 지도학습과 다르게 한가지 추가적으로 필요한 것이 있습니다.

기존의 지도학습 세팅에서는 loss나 validation accuracy를 통해서 학습이 원활히 진행되고 있는지 모니터링이 가능했지만, GAN에서는 generator가 비록 discriminator를 잘 속이고 있을지라도 (i.e., 낮은 loss) discriminator가 학습이 충분히 되지 못했다면 낮은 퀄리티의 이미지가 생성되게 됩니다.

이미지의 퀄리티를 측정하는 방법은 크게 2가지 입니다.
1. Fidelity(충실도): 얼마나 고품질의 이미지를 생성하는가?.
2. Diversity(다양성): 생성된 이미지들이 얼마나 다양한가? (e.g., 고양이만 생성하지 않음)

보통 Fidelity를 측정하기 위해서는 **Frechet Inception Distance**라는 metric이, Diversity를 측정하기 위해서는 **Inception Score**라는 evaluation metric이 사용되곤 합니다.

이번 실습에서는 이미지의 다양성을 측정하는 Inception Score를 통해 학습이 원활히 진행되고 있는지 모니터링 하도록 하겠습니다.

Inception score를 측정하는 방법은 아래와 같습니다.
1. Generator를 통해 이미지를 N개 생성한다.
2. 생성된 이미지들을 pre-trained된 inception network (=googleNet)에 통과시킨다.
3. inception network가 예측한 생성된 image의 label별 probability의 평균이 얼마나 diverse한지 측정한다.

Inception score에 대한 자세한 내용이 궁금하신 분은 아래를 참조해주세요
- https://arxiv.org/abs/1801.01973
- https://cyc1am3n.github.io/2020/03/01/is_fid.html

```python
from torchvision.models.inception import inception_v3
from scipy.stats import entropy

class Inception_Score():
    def __init__(self, dataset):
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

        # Dataset & DataLoader
        self.N = len(dataset)
        self.batch_size = 64

        self.dataset = dataset
        self.dataloader = data.DataLoader(dataset=dataset, batch_size=self.batch_size, num_workers=1)
        self.transform = nn.Upsample(size=(299, 299), mode='bilinear').to(self.device)

        # Inception Model
        self.inception_model = inception_v3(pretrained=True, transform_input=False).to(self.device)
        self.inception_model.eval()

    def get_pred(self, x):
        with torch.no_grad():
            x = self.transform(x)
            x = self.inception_model(x)
            return F.softmax(x, dim=1).data.cpu().numpy()

    def compute_score(self, splits=1):
        preds = np.zeros((self.N, 1000))

        for i, batch in tqdm.tqdm(enumerate(self.dataloader)):
            batch = batch.to(self.device)
            batch_size_i = batch.size(0)
            preds[i * self.batch_size : i * self.batch_size + batch_size_i] = self.get_pred(batch)

        # Compute the mean KL-divergence
        # You have to calculate the inception score.
        # The logit values from inception model are already stored in 'preds'.
        inception_score = 0.0
        split_scores = []
        for k in tqdm.tqdm(range(splits)):
            part = preds[k * (self.N // splits): (k + 1) * (self.N // splits), :]
            py = np.mean(part, axis=0)
            scores = []
            for i in range(part.shape[0]):
                pyx = part[i, :]
                scores.append(entropy(pyx, py))
            split_scores.append(np.exp(np.mean(scores)))
        inception_score = np.mean(split_scores)

        return inception_score
```

```python
def test_inception_score():
    print("======Inception Score Test Case======")

    # CIFAR10 Datset without Label
    class CIFAR10woLabel():
        def __init__(self):
            transform = transforms.Compose([transforms.ToTensor(),
                                            transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))])
            self.dataset = torchvision.datasets.CIFAR10(root='./data/', download=True, transform=transform)

        def __getitem__(self, index):
            return self.dataset[index][0]

        def __len__(self):
            return len(self.dataset)

    print("Calculating Inception Score...")

    Inception = Inception_Score(CIFAR10woLabel())
    score = Inception.compute_score(splits=1)

    assert np.allclose(score, 9.719672, atol=1e-3), \
        "Your inception score does not match expected result."

    print("All test passed!")

test_inception_score()
```

    ======Inception Score Test Case======
    Calculating Inception Score...
    Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data/cifar-10-python.tar.gz

      0%|          | 0/170498071 [00:00<?, ?it/s]

    Extracting ./data/cifar-10-python.tar.gz to ./data/

    Downloading: "https://download.pytorch.org/models/inception_v3_google-0cc3c7bd.pth" to /root/.cache/torch/hub/checkpoints/inception_v3_google-0cc3c7bd.pth

      0%|          | 0.00/104M [00:00<?, ?B/s]

    0it [00:00, ?it/s]/usr/local/lib/python3.7/dist-packages/torch/nn/functional.py:3635: UserWarning: Default upsampling behavior when mode=bilinear is changed to align_corners=False since 0.4.0. Please specify align_corners=True if the old behavior is desired. See the documentation of nn.Upsample for details.
      "See the documentation of nn.Upsample for details.".format(mode)
    782it [10:14,  1.27it/s]
    100%|██████████| 1/1 [00:03<00:00,  3.88s/it]

    All test passed!

# 4. Trainer

이제 앞서 선언한 dataloader, model, evaluator를 모두 활용해서 GAN을 학습시키는 Trainer를 구현해보도록 하겠습니다.

## Preliminary

\begin{equation}
D_{\theta}: \text{Discriminator network}\\
G_{\phi}: \text{Generator network}\\
x: \text{real_image} \\
z: \text{latent_vector} \\
\end{equation}

## Discriminator Loss

\begin{equation}
\mathcal{L}_{D_{\theta}} = -E_{x \sim p_{data}}[logD_{\theta}(x) + E_{z}[\log(1 - D_{\theta}(G_{\phi}(z)))]]
\end{equation}

Discriminator loss는 위와 같이 real_image는 1으로, generated_image는 0으로 판별하는 방식으로 학습을 진행하게 됩니다.

## Generator Loss

Generator network는 이론적으로는 discriminator의 loss에서 generator가 해당되는 부분에 -1을 곱해서 표현할 수 있습니다.

\begin{equation}
\mathcal{L}_{G_{\phi}} = E_{z}[\log(1-D_{\theta}(G_{\phi}(z))]  \tag{1}
\end{equation}

하지만, 위의 식으로 학습을 진행할 경우 Generator의 학습이 원활히 이루어지지 않게되는 문제점이 있습니다.

```python
plt.title('log(1-D(G(z))')
x = np.arange(0, 1.0, 0.01)
y = np.log(1-x)
plt.xlabel('D(G(z))')
plt.plot(x,y)
```

위의 loss plot에서 볼 수 있듯이, Generator는 Discriminator를 속이는 것에 성공할 수록 $D_{\theta}(G_{\phi}(z)) \approx 1$ 낮은 loss를 갖게 됩니다.

하지만, 이미지 생성의 난이도를 생각하면, 학습 초반에 Discriminator에 비해 Generator가 못하는 일은 자명한 일입니다. 이 때, D(G(z))가 0에 가까운 지점 $D_{\theta}(G_{\phi}(z)) \approx 0$ 에서의 함수의 기울기가 너무 작기 때문에 학습 초반에 Generator가 충분한 양의 학습 시그널을 받지 못하게 되는 문제점이 발생하게 됩니다.

따라서, 위의 식과 직관적으로 유사한 의미를 가지는 다른 loss function을 정의해보도록 하겠습니다.

\begin{equation}
\mathcal{L}_{G_{\phi}} = -E_{z}[\log(D_{\theta}(G_{\phi}(z))]  \tag{2}
\end{equation}

```python
plt.title('-log(D(G(z))')
x = np.arange(0, 1.0, 0.01)
y = -np.log(x)
plt.xlabel('D(G(z))')
plt.plot(x,y)
```

위의 loss plot에서 볼 수 있듯이, Generator는 여전히 Discriminator를 속이는 것에 성공할 수록 $D_{\theta}(G_{\phi}(z)) \approx 1$ 낮은 loss를 갖게 됩니다.

하지만, 이전과는 달리 $D_{\theta}(G_{\phi}(z)) \approx 0$에서의 gradient가 크기 때문에 학습 초반에 이미지를 생성하지 못할 때 오히려 충분한 양의 학습 시그널을 받을 수 있게 됩니다.

따라서, 이번 과제에서는 Generator의 Loss로 두번째 식을 사용하도록 하겠습니다.

```python
# Utility Functions
def denorm(x):
    out = (x + 1) / 2
    return out.clamp(0, 1)

def save_checkpoint(model, save_path, device):
    if not os.path.exists(os.path.dirname(save_path)):
        os.makedirs(os.path.dirname(save_path))
    torch.save(model.cpu().state_dict(), save_path)
    model.to(device)

def load_checkpoint(model, checkpoint_path, device):
    if not os.path.exists(checkpoint_path):
        print("Invalid path!")
        return
    model.load_state_dict(torch.load(checkpoint_path))
    model.to(device)

class FolderDataset(data.Dataset):
    def __init__(self, folder):
        self.folder = folder
        self.image_list = os.listdir(folder)
        self.transform = transforms.Compose([transforms.ToTensor(),
                                             transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))])

    def __getitem__(self, index):
        image = Image.open(os.path.join(self.folder, self.image_list[index]))
        return self.transform(image)

    def __len__(self):
        return len(self.image_list)

# Trainer
class Trainer():
    def __init__(self,
                 trainloader,
                 testloader,
                 generator,
                 discriminator,
                 criterion,
                 g_optimizer,
                 d_optimizer,
                 device):
        """
        trainloader: train data's loader
        testloader: test data's loader
        generator: generator
        discriminator: discriminator
        criterion: loss function to evaluate the model (e.g., BCE Loss)
        g_optimizer: optimizer for generator
        d_optimizer: optimizer for discriminator
        """
        self.trainloader = trainloader
        self.testloader = testloader
        self.G = generator
        self.D = discriminator
        self.criterion = criterion
        self.g_optimizer = g_optimizer
        self.d_optimizer = d_optimizer
        self.device = device

        # Make directory to save the images & models for a specific checkpoint
        os.makedirs(os.path.join('./results/', 'images'), exist_ok=True)
        os.makedirs(os.path.join('./results/', 'checkpoints'), exist_ok=True)
        os.makedirs(os.path.join('./results/', 'evaluation'), exist_ok=True)

    def train(self, epochs = 1):
        self.G.to(self.device)
        self.D.to(self.device)

        start_time = time.time()
        for epoch in range(epochs):
            for iter, (real_img, _) in enumerate(self.trainloader):
                self.G.train()
                self.D.train()

                batch_size = real_img.size(0)
                real_label = torch.ones(batch_size).to(self.device)
                fake_label = torch.zeros(batch_size).to(self.device)
                # get real CIFAR-10 image
                real_img = real_img.to(self.device)
                # initialize latent_vector to feed into the Generator
                z = torch.randn(real_img.size(0), 256).to(self.device)

                ##########################################################################################
                # Discriminator Loss 구현                                                                #
                # Note : Discriminator Loss는 Generator network의 parameter에 영향을 주지 않아야 합니다. #
                #        detach() function을 참고하세요.                                                 #
                #        https://pytorch.org/docs/stable/generated/torch.Tensor.detach.html              #
                ##########################################################################################
                D_loss: torch.Tensor = None
                ### YOUR CODE HERE (~ 4 lines)
                real_out = self.D(real_img)
                fake_img = self.G(z)
                fake_out = self.D(fake_img.detach())
                D_loss = self.criterion(real_out, real_label) + self.criterion(fake_out, fake_label)
                ### END YOUR CODE

                # TEST CODE
                # (TEST의 통과가 맞는 구현을 보장하지는 못합니다. 일반적으로는 loss가 1.38~1.45 사이의 값이 나와야 합니다.)
                if epoch == 0 and iter == 0:
                    assert D_loss.detach().allclose(torch.tensor(1.4000), atol=2e-1), \
                    f"Discriminator Loss of the model does not match expected result."
                    print("==Discriminator loss function test passed!==")

                self.D.zero_grad()
                D_loss.backward()
                self.d_optimizer.step()

                #######################################################
                # Generator Loss 구현                                 #
                # Note : 위의 정의된 두번 째 식을 사용해서 구현하세요 #
                #######################################################
                G_loss: torch.Tensor = None
                ### YOUR CODE HERE (~ 3 lines)
                fake_img = self.G(z)
                fake_out = self.D(fake_img)
                G_loss = self.criterion(fake_out, real_label)
                ### END YOUR CODE

                # Test code
                # (TEST의 통과가 맞는 구현을 보장하지는 못합니다. 일반적으로는 loss가 1.35~1.52 사이의 값이 나와야 합니다.)
                if epoch == 0 and iter == 0:
                    assert G_loss.detach().allclose(torch.tensor(1.5), atol=2e-1), \
                    f"Generator Loss of the model does not match expected result."
                    print("==Generator loss function test passed!==")

                self.G.zero_grad()
                G_loss.backward()
                self.g_optimizer.step()

            # verbose
            end_time = time.time() - start_time
            end_time = str(datetime.timedelta(seconds=end_time))[:-7]
            print('Time [%s], Epoch [%d/%d], lossD: %.4f, lossG: %.4f'
                  % (end_time, epoch+1, epochs, D_loss.item(), G_loss.item()))

            # Save Images
            fake_img = fake_img.reshape(fake_img.size(0), 3, 32, 32)
            torchvision.utils.save_image(denorm(fake_img), os.path.join('./results/', 'images', 'fake_image-{:03d}.png'.format(epoch+1)))
            if epoch % 10 == 0:
                self.test()

        # Save Checkpoints
        save_checkpoint(self.G, os.path.join('./results', 'checkpoints', 'G_final.pth'), self.device)
        save_checkpoint(self.D, os.path.join('./results', 'checkpoints', 'D_final.pth'), self.device)

    def test(self):
        print('Start computing Inception Score')
        self.G.eval()
        with torch.no_grad():
            for iter in tqdm.tqdm(range(5000)):
                z = torch.randn(1, 256).to(self.device)
                fake_img = self.G(z)
                torchvision.utils.save_image(denorm(fake_img), os.path.join('./results/', 'evaluation', 'fake_image-{:03d}.png'.format(iter)))

        # Compute the Inception score
        dataset = FolderDataset(folder = os.path.join('./results/', 'evaluation'))
        Inception = Inception_Score(dataset)
        score = Inception.compute_score(splits=1)
        print('Inception Score : ', score)
```

### Train

자, 이제 학습을 진행해 보겠습니다.
학습이 진행됨에 따라 generator가 생성하는 image는 \\
[파일]->[results]->[images]에서 각 epoch별로 확인해보실 수 있습니다.

```python
lr = 2e-4

trainloader, testloader = create_dataloader()
G = Generator()
D = Discriminator()
criterion = nn.BCELoss()
g_optimizer = optim.Adam(G.parameters(), lr=lr, betas=(0.5, 0.999))
d_optimizer = optim.Adam(D.parameters(), lr=lr, betas=(0.5, 0.999))
device = torch.device('cuda')

trainer = Trainer(trainloader=trainloader,
                  testloader=testloader,
                  generator=G,
                  discriminator=D,
                  criterion=criterion,
                  g_optimizer=g_optimizer,
                  d_optimizer=d_optimizer,
                  device=device)

trainer.train(epochs=50)
```

    Files already downloaded and verified
    Files already downloaded and verified
    ==Discriminator loss function test passed!==
    ==Generator loss function test passed!==
    Time [0:00:47], Epoch [1/50], lossD: 0.2177, lossG: 4.4834
    Start computing Inception Score

    100%|██████████| 5000/5000 [00:08<00:00, 571.05it/s]
    0it [00:00, ?it/s]/usr/local/lib/python3.7/dist-packages/torch/nn/functional.py:3635: UserWarning: Default upsampling behavior when mode=bilinear is changed to align_corners=False since 0.4.0. Please specify align_corners=True if the old behavior is desired. See the documentation of nn.Upsample for details.
      "See the documentation of nn.Upsample for details.".format(mode)
    79it [01:01,  1.28it/s]
    100%|██████████| 1/1 [00:00<00:00,  2.44it/s]

    Inception Score :  2.0722152604102377

    Time [0:02:47], Epoch [2/50], lossD: 0.2580, lossG: 2.2199
    Time [0:03:35], Epoch [3/50], lossD: 0.4077, lossG: 2.5191
    Time [0:04:23], Epoch [4/50], lossD: 0.4489, lossG: 2.7645
    Time [0:05:11], Epoch [5/50], lossD: 0.5359, lossG: 5.5390
    Time [0:06:00], Epoch [6/50], lossD: 0.4076, lossG: 3.6671
    Time [0:06:48], Epoch [7/50], lossD: 0.2684, lossG: 4.1791
    Time [0:07:36], Epoch [8/50], lossD: 0.1034, lossG: 4.0335
    Time [0:08:24], Epoch [9/50], lossD: 0.0918, lossG: 3.4361
    Time [0:09:12], Epoch [10/50], lossD: 0.2844, lossG: 4.5256
    Time [0:09:59], Epoch [11/50], lossD: 0.3611, lossG: 4.4674
    Start computing Inception Score

    100%|██████████| 5000/5000 [00:09<00:00, 550.04it/s]
    79it [01:00,  1.30it/s]
    100%|██████████| 1/1 [00:00<00:00,  2.56it/s]

    Inception Score :  3.7484425122708998

    Time [0:11:58], Epoch [12/50], lossD: 0.0975, lossG: 4.1358
    Time [0:12:45], Epoch [13/50], lossD: 0.5864, lossG: 1.1035
    Time [0:13:32], Epoch [14/50], lossD: 0.8942, lossG: 2.6309
    Time [0:14:20], Epoch [15/50], lossD: 0.0431, lossG: 4.4464
    Time [0:15:07], Epoch [16/50], lossD: 0.1853, lossG: 4.6425
    Time [0:15:54], Epoch [17/50], lossD: 0.0772, lossG: 4.7077
    Time [0:16:41], Epoch [18/50], lossD: 0.1442, lossG: 3.6719
    Time [0:17:29], Epoch [19/50], lossD: 0.1623, lossG: 4.0677
    Time [0:18:16], Epoch [20/50], lossD: 0.1256, lossG: 5.1543
    Time [0:19:03], Epoch [21/50], lossD: 0.1338, lossG: 4.3656
    Start computing Inception Score

    100%|██████████| 5000/5000 [00:08<00:00, 564.61it/s]
    79it [01:00,  1.30it/s]
    100%|██████████| 1/1 [00:00<00:00,  2.63it/s]

    Inception Score :  3.950156360103655

    Time [0:21:01], Epoch [22/50], lossD: 0.2091, lossG: 3.8985
    Time [0:21:48], Epoch [23/50], lossD: 0.0860, lossG: 5.2119
    Time [0:22:35], Epoch [24/50], lossD: 0.2178, lossG: 4.0088
    Time [0:23:22], Epoch [25/50], lossD: 0.0957, lossG: 4.8595
    Time [0:24:10], Epoch [26/50], lossD: 0.1863, lossG: 3.6236
    Time [0:24:57], Epoch [27/50], lossD: 0.0279, lossG: 4.5394
    Time [0:25:45], Epoch [28/50], lossD: 0.1260, lossG: 4.4608
    Time [0:26:32], Epoch [29/50], lossD: 0.0249, lossG: 5.4409
    Time [0:27:19], Epoch [30/50], lossD: 0.1852, lossG: 3.5242
    Time [0:28:06], Epoch [31/50], lossD: 0.1917, lossG: 2.5745
    Start computing Inception Score

    100%|██████████| 5000/5000 [00:08<00:00, 564.74it/s]
    79it [01:00,  1.30it/s]
    100%|██████████| 1/1 [00:00<00:00,  2.49it/s]

    Inception Score :  4.212877004134691

    Time [0:30:04], Epoch [32/50], lossD: 0.0743, lossG: 5.2845
    Time [0:30:51], Epoch [33/50], lossD: 0.1738, lossG: 4.6336
    Time [0:31:38], Epoch [34/50], lossD: 0.6574, lossG: 2.5570
    Time [0:32:25], Epoch [35/50], lossD: 0.0857, lossG: 3.9621
    Time [0:33:13], Epoch [36/50], lossD: 0.0477, lossG: 4.4689
    Time [0:34:00], Epoch [37/50], lossD: 0.2681, lossG: 6.3673
    Time [0:34:47], Epoch [38/50], lossD: 0.0640, lossG: 5.4857
    Time [0:35:34], Epoch [39/50], lossD: 0.3815, lossG: 2.1046
    Time [0:36:21], Epoch [40/50], lossD: 0.1141, lossG: 3.5499
    Time [0:37:08], Epoch [41/50], lossD: 0.0660, lossG: 6.5942
    Start computing Inception Score

    100%|██████████| 5000/5000 [00:09<00:00, 542.02it/s]
    79it [01:00,  1.30it/s]
    100%|██████████| 1/1 [00:00<00:00,  2.45it/s]

    Inception Score :  3.9950488695627064

    Time [0:39:06], Epoch [42/50], lossD: 0.0250, lossG: 6.8565
    Time [0:39:54], Epoch [43/50], lossD: 0.1399, lossG: 5.1342
    Time [0:40:41], Epoch [44/50], lossD: 0.3743, lossG: 2.3584
    Time [0:41:28], Epoch [45/50], lossD: 0.1101, lossG: 6.0654
    Time [0:42:15], Epoch [46/50], lossD: 0.0939, lossG: 5.3856
    Time [0:43:03], Epoch [47/50], lossD: 0.0796, lossG: 4.6636
    Time [0:43:50], Epoch [48/50], lossD: 0.0948, lossG: 5.2483
    Time [0:44:37], Epoch [49/50], lossD: 0.0683, lossG: 5.4519
    Time [0:45:24], Epoch [50/50], lossD: 0.0130, lossG: 6.7613

```python

```


# Selección óptima de portafolios I

En la clase pasada vimos que:
- La LAC describe las posibles selecciones de riesgo-rendimiento entre un activo libre de riesgo y un activo riesgoso.
- Su pendiente es igual al radio de Sharpe del activo riesgoso.
- La asignación óptima de capital para cualquier inversionista es el punto tangente de la curva de indiferencia del inversionista con la LAC (depende de las preferencias particulares - aversión al riesgo).

Para todo lo anterior, supusimos que ya teníamos el portafolio óptimo (activo riesgoso).

En el siguiente análisis:

**Objetivos:**
- ¿Cuál es el portafolio óptimo de activos riesgosos?
 - ¿Cuál es el mejor portafolio de activos riesgosos?
 - Es un portafolio eficiente en media-varianza.
- Problema: dado un conjunto de activos riesgosos, ¿cómo construimos la mejor combinación?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Maximizando el radio de Sharpe

### ¿Qué pasa si tenemos dos activos riesgosos?

Cuando tenemos dos o más activos riesgosos, tenemos disponibles diferentes LAC. ¿Qué significan sus pendientes?

 Ver en el tablero.

Pregunta:
- ¿Qué es lo que se quiere?

**Conclusión:**
- El mejor portafolio de activos no depende de las preferencias individuales, y por tanto va a ser igual para todos.
- Dicho mejor portafolio maximiza el radio de Sharpe.
- A este portafolio lo llamaremos el portafolio eficiente en media-varianza (EMV)

**Idea principal: el portafolio óptimo de activos riesgosos es independiente de las preferencias del inversionista.**
- El portafolio EMV determina el portafolio óptimo de activos riesgosos.
- Todos tendremos el mismo portafolio de activos riesgosos (EMV), y lo combinaremos con el activo libre de reisgo de acuerdo con las preferencias de cada uno de nosotros (aversión al riesgo).
- La LAC combinando el activo libre de riesgo y el portafolio EMV, se vuelve el conjunto de portafolios eficientes.

Entonces, se deben seguir los siguientes pasos:
1. Crear la frontera media-varianza.
2. Encontrar el portafolio que maximize el radio de Sharpe (portafolio EMV).
3. Construir la frontera eficiente (LAC) del punto $(0,r_f)$ al punto $(\sigma_s,E[r_s])$ del portafolio EMV.
4. Combinar de acuerdo a sus preferencias.
___

## 2. Solución analítica del portafolio EMV: caso con dos activos.

Queremos solucionar el siguiente problema:

\begin{align}
\max_{w_1,w_2} &\quad \frac{E[r_p]-r_f}{\sigma_p}\\
\text{s.a.}    &\quad E[r_p]=w_1E[r_1]+w_2E[r_2]\\
               &\quad \sigma_p=\sqrt{w_1^2\sigma_1^2+w_2^2\sigma_2^2+2w_1w_2\rho_{12}\sigma_1\sigma_2}\\
               &\quad w_1+w_2=1, \quad w_1,w_2\geq0
\end{align}

el cual es equivalente a

\begin{align}
\max_{w_1} &\quad \frac{w_1E[r_1]+(1-w_1)E[r_2]-r_f}{\sqrt{w_1^2\sigma_1^2+(1-w_1)^2\sigma_2^2+2w_1(1-w_1)\rho_{12}\sigma_1\sigma_2}}\\
\text{s.a.}    &\quad 0\leq w_1\leq1
\end{align}

**Actividad.**
El anterior es un problema de maximizar una función de una variable en un dominio cerrado. No debaría representar dificultad.

Encontrar la solución analítica a este problema.

Quien primero lo haga, y salga a explicarlo al tablero, le subo alguna tarea o quiz a 100.

Deben llegar a:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

Si nadie lo ha hecho en 30 min., procederé a hacerlo yo.

**Nota:**
- así como obtuvimos una expresión para el peso del portafolio de mínima varianza con dos activos, obtenemos una expresión para el peso del portafolio Eficiente en Media-Varianza.
- Estas actividades son sin duda un buen ejercicio, y se pueden replicar usando técnicas de varias variables (multiplicadores de Lagrange) cuando se tengan más de dos activos.
- Sin embargo, la complejidad del problema crece considerablemente con el número de variables, y la solución analítica deja de ser viable cuando mencionamos que un portafolio bien diversificado consta aproximadamente de 50-60 activos.
- En esos casos, este problema se soluciona con rutinas numéricas que hagan la optimización por nosotros.
- Por eso, les enseño cómo resolver este problema con optimizadores numéricos, porque son una solución viable y escalable a más variables.

## 3. Ejemplo ilustrativo.

Retomamos el ejemplo de mercados de acciones en los países integrantes del $G5$: EU, RU, Francia, Alemania y Japón.

```python
# Importamos pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Resumen en base anual de rendimientos esperados y volatilidades
annual_ret_summ = pd.DataFrame(columns=['EU', 'RU', 'Francia', 'Alemania', 'Japon'], index=['Media', 'Volatilidad'])
annual_ret_summ.loc['Media'] = np.array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497])
annual_ret_summ.loc['Volatilidad'] = np.array([0.1535, 0.2430, 0.2324, 0.2038, 0.2298])

annual_ret_summ.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      Media
      0.1355
      0.1589
      0.1519
      0.1435
      0.1497

      Volatilidad
      0.1535
      0.243
      0.2324
      0.2038
      0.2298

```python
# Matriz de correlación
corr = pd.DataFrame(data= np.array([[1.0000, 0.5003, 0.4398, 0.3681, 0.2663],
                                    [0.5003, 1.0000, 0.5420, 0.4265, 0.3581],
                                    [0.4398, 0.5420, 1.0000, 0.6032, 0.3923],
                                    [0.3681, 0.4265, 0.6032, 1.0000, 0.3663],
                                    [0.2663, 0.3581, 0.3923, 0.3663, 1.0000]]),
                    columns=annual_ret_summ.columns, index=annual_ret_summ.columns)
corr.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      EU
      1.0000
      0.5003
      0.4398
      0.3681
      0.2663

      RU
      0.5003
      1.0000
      0.5420
      0.4265
      0.3581

      Francia
      0.4398
      0.5420
      1.0000
      0.6032
      0.3923

      Alemania
      0.3681
      0.4265
      0.6032
      1.0000
      0.3663

      Japon
      0.2663
      0.3581
      0.3923
      0.3663
      1.0000

Supondremos, además, que la tasa libre de riesgo es $r_f=5\%$.

```python
# Tasa libre de riesgo
rf = 0.05
```

Entonces, supondremos que tenemos disponibles los activos correspondientes a los mercados de acciones de EU y Japón, y en adición el activo libre de riesgo.

#### 1. Construir la frontera de mínima varianza

```python
# Vector de w variando entre 0 y 1 con n pasos
N = 101
w = np.linspace(0,1,N)
# Rendimientos esperados individuales
# Activo1: EU, Activo2:Japon
E1 = annual_ret_summ.loc['Media','EU']
E2 = annual_ret_summ.loc['Media','Japon']
# Volatilidades individuales
s1 = annual_ret_summ.loc['Volatilidad','EU']
s2 = annual_ret_summ.loc['Volatilidad','Japon']
# Correlacion
r12 = corr.loc['EU','Japon']
# Covarianza
s12 = s1 * s2 * r12
```

```python
# DataFrame de portafolios:
# 1. Índice: i
# 2. Columnas 1-2: w, 1-w
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
portafolios = pd.DataFrame({'w': w,
                           '1-w': 1-w,
                           'Media': w*E1 + (1-w)*E2,
                           'Volatilidad': ((w * s1)**2 + ((1-w)*s2)**2 + 2*w*(1-w)*s12)**0.5})
portafolios['RS'] = (portafolios['Media'] - rf)/portafolios['Volatilidad']
portafolios.head()
```

      w
      1-w
      Media
      Volatilidad
      RS

      0
      0.00
      1.00
      0.149700
      0.229800
      0.433856

      1
      0.01
      0.99
      0.149558
      0.227916
      0.436820

      2
      0.02
      0.98
      0.149416
      0.226041
      0.439814

      3
      0.03
      0.97
      0.149274
      0.224176
      0.442839

      4
      0.04
      0.96
      0.149132
      0.222322
      0.445894

```python
# Importar librerías de gráficos
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# Gráfica de dispersión de puntos coloreando de acuerdo a SR
plt.figure(figsize = (12,8))
plt.scatter(portafolios['Volatilidad'],portafolios['Media'], c=portafolios['RS'], cmap='inferno')
plt.plot(s1,E1,'bo', ms=10, label='EU')
plt.plot(s2,E2,'ro', ms=10, label='Japón')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.grid()
plt.legend(loc='best')
plt.colorbar()
```

#### 2. Encontrar el portafolio que maximiza el radio de Sharpe (EMV)

Primero, encontramos este portafolio con la fórmula que obtuvimos:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

```python
# Fórmula que obtuvimos
w_EMV = ((E1-rf)*s2**2 - (E2-rf)*s12) / ((E2-rf)*s1**2 + (E1-rf)*s2**2 - (E1-rf + E2-rf)*s12)
w_EMV
```

    0.6983139170512034

Ahora sí, con la función scipy.optimize.minimize

```python
# Importar el módulo optimize
from scipy.optimize import minimize
```

```python
# Función objetivo (-SR)
def menos_RS(w, rf, E1, E2, s1, s2, s12):
    Erp = w*E1 + (1-w)*E2
    sp = ((w*s1)**2 + ((1-w)*s2)**2 + 2*w*(1-w)*s12)**0.5
    RS = (Erp - rf) / sp
    return -RS
```

```python
# Dato inicial
w0 = .5
# Cotas de las variables
bnd = ((0,1),)
```

```python
# Optimización numérica
res = minimize(fun=menos_RS,
              x0 = w0,
              args = (rf, E1, E2, s1, s2, s12),
               bounds = bnd)
# Resultado
res
```

          fun: array([-0.63087253])
     hess_inv: <1x1 LbfgsInvHessProduct with dtype=float64>
          jac: array([3.39728246e-06])
      message: b'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL'
         nfev: 10
          nit: 4
       status: 0
      success: True
            x: array([0.69831582])

```python
w_EMV, res.x
```

    (0.6983139170512034, array([0.69831582]))

Con lo anterior, podemos obtener datos de rendimiento esperado y volatilidad del portafolio EMV

```python
# Rendimiento esperado y volatilidad del portafolio EMV
E_EMV = w_EMV*E1+(1-w_EMV)*E2
s_EMV = ((w_EMV*s1)**2 + ((1-w_EMV)*s2)**2 + 2*w_EMV*(1-w_EMV)*s12)**0.5
```

```python
E_EMV, s_EMV
```

    (0.13978394237787292, 0.14231708951606933)

```python
# Gráfica de dispersión de puntos coloreando de acuerdo a SR, y portafolio EMV
plt.figure(figsize = (12,8))
plt.scatter(portafolios['Volatilidad'],portafolios['Media'], c=portafolios['RS'], cmap='inferno')
plt.plot(s_EMV, E_EMV, 'g*', ms=10, label='Port.EMV')
plt.plot(s1,E1,'bo', ms=10, label='EU')
plt.plot(s2,E2,'ro', ms=10, label='Japón')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.grid()
plt.legend(loc='best')
plt.colorbar()
```

#### 3. Construir LAC

Ahora, dibujamos la LAC, combinando el portafolio EMV con el activo libre de riesgo:

```python
# Vector de wp variando entre 0 y 1.5 con n pasos
wp = np.linspace(0,1.5)
```

```python
# DataFrame de CAL:
# 1. Índice: i
# 2. Columnas 1-2: wp, wrf
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
LAC = pd.DataFrame({'w_EMV': wp,
                   'w(rf)': 1-wp,
                   'Media': wp*E_EMV + (1-wp)*rf,
                   'Vol': wp*s_EMV})
LAC['RS'] = (LAC['Media']-rf)/LAC['Vol']
LAC
```

      w_EMV
      w(rf)
      Media
      Vol
      RS

      0
      0.000000
      1.000000
      0.050000
      0.000000
      NaN

      1
      0.030612
      0.969388
      0.052748
      0.004357
      0.630873

      2
      0.061224
      0.938776
      0.055497
      0.008713
      0.630873

      3
      0.091837
      0.908163
      0.058245
      0.013070
      0.630873

      4
      0.122449
      0.877551
      0.060994
      0.017427
      0.630873

      5
      0.153061
      0.846939
      0.063742
      0.021783
      0.630873

      6
      0.183673
      0.816327
      0.066491
      0.026140
      0.630873

      7
      0.214286
      0.785714
      0.069239
      0.030497
      0.630873

      8
      0.244898
      0.755102
      0.071988
      0.034853
      0.630873

      9
      0.275510
      0.724490
      0.074736
      0.039210
      0.630873

      10
      0.306122
      0.693878
      0.077485
      0.043566
      0.630873

      11
      0.336735
      0.663265
      0.080233
      0.047923
      0.630873

      12
      0.367347
      0.632653
      0.082982
      0.052280
      0.630873

      13
      0.397959
      0.602041
      0.085730
      0.056636
      0.630873

      14
      0.428571
      0.571429
      0.088479
      0.060993
      0.630873

      15
      0.459184
      0.540816
      0.091227
      0.065350
      0.630873

      16
      0.489796
      0.510204
      0.093976
      0.069706
      0.630873

      17
      0.520408
      0.479592
      0.096724
      0.074063
      0.630873

      18
      0.551020
      0.448980
      0.099473
      0.078420
      0.630873

      19
      0.581633
      0.418367
      0.102221
      0.082776
      0.630873

      20
      0.612245
      0.387755
      0.104970
      0.087133
      0.630873

      21
      0.642857
      0.357143
      0.107718
      0.091490
      0.630873

      22
      0.673469
      0.326531
      0.110467
      0.095846
      0.630873

      23
      0.704082
      0.295918
      0.113215
      0.100203
      0.630873

      24
      0.734694
      0.265306
      0.115964
      0.104559
      0.630873

      25
      0.765306
      0.234694
      0.118712
      0.108916
      0.630873

      26
      0.795918
      0.204082
      0.121461
      0.113273
      0.630873

      27
      0.826531
      0.173469
      0.124209
      0.117629
      0.630873

      28
      0.857143
      0.142857
      0.126958
      0.121986
      0.630873

      29
      0.887755
      0.112245
      0.129706
      0.126343
      0.630873

      30
      0.918367
      0.081633
      0.132455
      0.130699
      0.630873

      31
      0.948980
      0.051020
      0.135203
      0.135056
      0.630873

      32
      0.979592
      0.020408
      0.137952
      0.139413
      0.630873

      33
      1.010204
      -0.010204
      0.140700
      0.143769
      0.630873

      34
      1.040816
      -0.040816
      0.143449
      0.148126
      0.630873

      35
      1.071429
      -0.071429
      0.146197
      0.152483
      0.630873

      36
      1.102041
      -0.102041
      0.148946
      0.156839
      0.630873

      37
      1.132653
      -0.132653
      0.151694
      0.161196
      0.630873

      38
      1.163265
      -0.163265
      0.154443
      0.165553
      0.630873

      39
      1.193878
      -0.193878
      0.157191
      0.169909
      0.630873

      40
      1.224490
      -0.224490
      0.159940
      0.174266
      0.630873

      41
      1.255102
      -0.255102
      0.162688
      0.178622
      0.630873

      42
      1.285714
      -0.285714
      0.165436
      0.182979
      0.630873

      43
      1.316327
      -0.316327
      0.168185
      0.187336
      0.630873

      44
      1.346939
      -0.346939
      0.170933
      0.191692
      0.630873

      45
      1.377551
      -0.377551
      0.173682
      0.196049
      0.630873

      46
      1.408163
      -0.408163
      0.176430
      0.200406
      0.630873

      47
      1.438776
      -0.438776
      0.179179
      0.204762
      0.630873

      48
      1.469388
      -0.469388
      0.181927
      0.209119
      0.630873

      49
      1.500000
      -0.500000
      0.184676
      0.213476
      0.630873

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, portafolio EMV y LAC
plt.figure(figsize = (12,8))
plt.scatter(portafolios['Volatilidad'],portafolios['Media'], c=portafolios['RS'], cmap='inferno')
plt.plot(s1,E1,'bo', ms=10, label='EU')
plt.plot(s2,E2,'ro', ms=10, label='Japón')
plt.plot(LAC['Vol'],LAC['Media'], '--k', lw= 2, label = 'LAC')
plt.plot(s_EMV, E_EMV, 'g*', ms=10, label='Port.EMV')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.grid()
plt.legend(loc='best')
plt.colorbar()
plt.axis([.13,.16,.13,.15])
```

#### 4. Combinación óptima de acuerdo a preferencias

Con los datos anteriores, y la caracterización de aversión al riesgo, se escoge la combinación óptima entre el portafolio EMV y el activo libre de riesgo de acuerdo a:

$$w^\ast=\frac{E[r_s-r_f]}{\gamma\sigma_s^2}.$$

```python
# Para gamma=7
g = 7
wcap = (E_EMV-rf)/(g*s_EMV**2)
wcap
```

    0.6332665142475

```python
wcap*w_EMV, wcap*(1-w_EMV), 1-wcap
```

    (0.44221882010153346, 0.1910476941459666, 0.3667334857525)

Created with Jupyter by Esteban Jiménez Rodríguez.


```python
import numpy as np
import xarray as xr
import pandas as pd
import numba
```

# Continuous Equation

$$
    \begin{align}
        \frac{\partial h}{\partial t}+\frac{\partial q_x}{\partial x} +\frac{\partial q_y}{\partial y} = 0
    \end{align}
$$

```python
@numba.jit(nopython=True, parallel=True)
def conEq(dep, qx, qy, dzb, dt, dx, dy, ibx, hmin, hbuf, hdown, periodic=True):
#     Qind = range(154,159)

    imax, jmax = len(dep), len(dep[0])
    depn = np.zeros_like(dep, dtype=np.float64)
    fluxx = np.zeros((imax+1, jmax), dtype=np.float64)
    fluxy = np.zeros((imax, jmax+1), dtype=np.float64)
    modflux = np.full( (imax, jmax), False)

    gravity = float( 9.8 )

    f = lambda Qp, Qm : Qm if Qp >= 0.0 and Qm >= 0.0 else (Qp if Qp <= 0.0 and Qm <= 0.0 else 0.5*Qp+0.5*Qm )

    def flux(Qp, Qm, depp, depm, zbp, zbm, ib, delta) :
        r = f(Qp, Qm)
#         if ( (depm + zbm) < zbp - ib*delta) and (depp <= hbuf) : r = 0.0
#         if ( (depp + zbp) < zbm + ib*delta) and (depm <= hbuf) : r = 0.0
        if ( (depm + zbm) <= zbp + hbuf - ib*delta) and (depp <= hbuf) : r = 0.0
        if ( (depp + zbp) <= zbm + hbuf + ib*delta) and (depm <= hbuf) : r = 0.0

        return r

    for i in numba.prange( imax ):
        for j in range( jmax ):
            c, xm = (i,j), (i-1,j)
            fluxx[c] = flux(qx[c], qx[xm], dep[c], dep[xm], dzb[c], dzb[xm], ibx, dx)

    if periodic :
# boundary : periodic
        fluxx[-1,:] = fluxx[0,:]
    else:
        for j in numba.prange( jmax ): fluxx[-1,j] = fluxx[-2,j] # qx[-1,j] # if qx[-1,j] > 0.0 else qx[,j]
# normal
#         for j in numba.prange( jmax ): fluxx[-1,j] = qx[-1,j] if qx[-1,j] > 0.0 else 0.0

    for i in numba.prange( imax ):
        for j in range( 1, jmax ):
            c, ym = (i,j), (i,j-1)
            fluxy[c] = flux(qy[c], qy[ym], dep[c], dep[ym], dzb[c], dzb[ym], 0.0, dy)

# wall boundary
#     fluxy[:,0] = 0.0
#     fluxy[:,-1] = 0.0

    for i in numba.prange( imax ):
        fluxy[i,-1] = qy[i,-1] if qy[i,-1] > 0.0 else 0.0
        fluxy[i, 0] = qy[i, 0] if qy[i, 0] < 0.0 else 0.0

    nis = 0 if periodic else 1
# limiter --------------------------------------------------------------
# 水深が負になる際に質量保存を満たすためにフラックスを修正する
    for i in range(nis, imax):
        for j in range(jmax):
            if dep[c] > hmin :
                c, xp, yp = (i, j), (i+1, j), (i, j+1)
                fxp = fluxx[xp] if fluxx[xp] > 0.0 else 0.0
                fxm = -fluxx[c] if fluxx[c] < 0.0 else 0.0
                fyp = fluxy[yp] if fluxy[yp] > 0.0 else 0.0
                fym = -fluxy[c] if fluxy[c] < 0.0 else 0.0
                V =  dep[c]*dx*dy - hmin*dx*dy
                Vq = ( fxp*dy + fxm*dy + fyp*dx + fym*dx )*dt
                if V <  Vq:
                    alfa = V / Vq - 0.001
                    if fluxx[xp] > 0.0 : fluxx[xp] *= alfa
                    if fluxx[c]  < 0.0 : fluxx[c]  *= alfa
                    if fluxy[yp] > 0.0 : fluxy[yp] *= alfa
                    if fluxy[c]  < 0.0 : fluxy[c]  *= alfa

                    modflux[c] = True
# ------------------------------------------------------------------------
    n = 0
    for i in numba.prange(nis, imax):
        for j in range(jmax):
            c, xp, yp = (i, j), (i+1, j), (i, j+1)
            depn[c] = dep[c] - dt*(fluxx[xp] - fluxx[c])/dx - dt*(fluxy[yp] - fluxy[c])/dy
            if depn[c] < hmin :
                n += 1
#                 print('dep-error')
#                 print( modflux[c], depn[c], fluxx[xp], fluxx[c], fluxy[yp], fluxy[c] )
                fxp = fluxx[xp] if fluxx[xp] > 0.0 else 0.0
                fxm = -fluxx[c] if fluxx[c] < 0.0 else 0.0
                fyp = fluxy[yp] if fluxy[yp] > 0.0 else 0.0
                fym = -fluxy[c] if fluxy[c] < 0.0 else 0.0
                V =  dep[c]*dx*dy - hmin*dx*dy
                Vq = ( fxp*dy + fxm*dy + fyp*dx + fym*dx )*dt
#                 print(V,Vq)

                depn[c] = hmin

# upstream boundary
#     if periodic == False: depn[0][:] = depn[1][:]
    if periodic == False:
#         depn[0][:] = dep[0][:]
#         for j in Qind : depn[0,j] = depn[1,j]

        depn[0,:] = depn[1,:]

# downstream boundary
#     depn[-1][:] = hdown
    depn[-1][:] = depn[-2][:]

    return depn, n
```

# Momentum Equation

$$
    \begin{align}
        \frac{\partial q_x}{\partial t}+\frac{\partial u q_x}{\partial x}+\frac{\partial v q_x}{\partial y}+gh\frac{\partial H}{\partial x}+\frac{\tau_{0x}}{\rho}
        - \nu_t h \left(\frac{\partial^2 u}{\partial x^2}+\frac{\partial^2 u}{\partial y^2} \right)= 0 \\
        \frac{\partial q_y}{\partial t}+\frac{\partial u q_y}{\partial x}+\frac{\partial v q_y}{\partial y}+gh\frac{\partial H}{\partial y}+\frac{\tau_{0y}}{\rho}- \nu_t h \left(\frac{\partial^2 v}{\partial x^2}+\frac{\partial^2 v}{\partial y^2} \right)
        = 0
    \end{align}
$$

```python
@numba.jit(nopython=True, parallel=True)
def momentEq(dep, qx, qy, depn, dzb, dt, dx, dy, ibx, hmin, hbuf, direction, Qup, cManning, periodic=True):
    #direction = 1:x, 2:y
    gravity = float( 9.8 )
#     Qind = range(154,159)

    q = qx.copy() if direction == 1 else qy.copy()
    u, v = qx/dep, qy/dep
    Vdir = q/dep

    imax, jmax = len(q), len(q[0])

    qn = np.zeros_like(q, dtype=np.float64)
    fluxx = np.zeros((imax+1, jmax), dtype=np.float64)
    fluxy = np.zeros((imax, jmax+1), dtype=np.float64)

    f = lambda vp,vm,qp,qm : vm*qm if vp >= 0.0 and vm >= 0.0 else \
        (vp*qp if vp <= 0.0 and vm <= 0.0 else (0.5*vp+0.5*vm)*(0.5*qp+0.5*qm) )

    def flux1(vp, vm, qp, qm, depp, depm, zbp, zbm, ib, delta) :
        r = f(vp,vm,qp,qm)
#         if ( (depm + zbm) < zbp - ib*delta) and (depp <= hbuf) : r = 0.0
#         if ( (depp + zbp) < zbm + ib*delta) and (depm <= hbuf) : r = 0.0
        if ( (depm + zbm) <= zbp + hbuf - ib*delta) and (depp <= hbuf) : r = 0.0
        if ( (depp + zbp) <= zbm + hbuf + ib*delta) and (depm <= hbuf) : r = 0.0

        return r

    for i in numba.prange( imax ):
        for j in range( jmax ):
            c, xm = (i,j), (i-1,j)
            fluxx[c] = flux1( u[c], u[xm], q[c], q[xm], dep[c], dep[xm], dzb[c], dzb[xm], ibx, dx )

# boundary : periodic
    if periodic :
        fluxx[-1,:] = fluxx[0,:]
    else:
        fluxx[0,:]  = -9999
        for j in numba.prange( jmax ):
            fluxx[-1,j] = u[-1,j]*q[-1,j] if u[-1,j] > 0.0 else 0.0

    for i in numba.prange( imax ):
        for j in range( 1, jmax ):
            c, ym = (i,j), (i,j-1)
            fluxy[c] = flux1( v[c], v[ym], q[c], q[ym], dep[c], dep[ym], dzb[c], dzb[ym], 0.0, dy )

# wall boundary
#     fluxy[:,0]  = -fluxy[:,1]
#     fluxy[:,-1] = -fluxy[:,-2]

    for i in numba.prange( imax ):
        fluxy[i,-1] = v[i,-1]*q[i,-1] if v[i,-1] > 0.0 else 0.0
        fluxy[i, 0] = v[i, 0]*q[i, 0] if v[i,0 ] < 0.0 else 0.0

    nis = 0 if periodic else 1

    for i in numba.prange(nis, imax):
        for j in range(jmax):
            c = (i, j)
            if periodic :
                xp = (0, j) if i == imax-1 else (i+1, j)
            else:
                xp = (i+1, j)
            xm = (i-1, j)
            yp = (i, j+1)
            ym = (i, j-1)

            if depn[c] <= hbuf :
                qn[c] = 0.0
            else:
            # pressure & gravity term
                if direction == 2 and ((j == 0) or (j == jmax-1)) :
                    dHdx = 0.0
                elif direction == 1 and i == imax-1 :
#                     dHdx = 0.0
                    dHdx =((depn[i,j]+dzb[i,j]) - (depn[i-1,j]+dzb[i-1,j]))/dx
                else :
                    if direction == 1 :
                        dp, dm, delta  = xp, xm, dx
                        dib = ibx
                    else :
                        dp, dm, delta = yp, ym, dy
                        dib = 0.0

                    Vc, Vp, Vm = q[c]/dep[c], q[dp]/dep[dp], q[dm]/dep[dm]
                    Hc, Hp, Hm = depn[c]+dzb[c], depn[dp]+dzb[dp], depn[dm]+dzb[dm]

#                     if(Hc < dzb[dp] - dib*delta) and depn[dp] <= hbuf :
                    if(Hc <= dzb[dp] + hbuf - dib*delta) and depn[dp] <= hbuf :
#                         if(Hc < dzb[dm] + dib*delta) and depn[dm] <= hbuf :
                        if(Hc <= dzb[dm] + hbuf + dib*delta) and depn[dm] <= hbuf :
                            dHdx = 0.0
                        else:
                            dHdx = (Hc-Hm)/delta-dib
#                     elif(Hc < dzb[dm] + dib*delta) and depn[dm] <= hbuf :
                    elif(Hc <= dzb[dm] + hbuf + dib*delta) and depn[dm] <= hbuf :
                        dHdx = (Hp-Hc)/delta-dib
                    else :
                        if Vc > 0.0 and Vp > 0.0 and Vm > 0.0:
                            Cr1, Cr2 = 0.5*(abs(Vc)+abs(Vp))*dt/delta, 0.5*(abs(Vc)+abs(Vm))*dt/delta
                            dHdx1, dHdx2 = (Hp-Hc)/delta-dib, (Hc-Hm)/delta-dib
                        elif Vc < 0.0 and Vp < 0.0 and Vm < 0.0:
                            Cr1, Cr2 = 0.5*(abs(Vc)+abs(Vm))*dt/delta, 0.5*(abs(Vc)+abs(Vp))*dt/delta
                            dHdx1, dHdx2 = (Hc-Hm)/delta-dib, (Hp-Hc)/delta-dib
                        else:
                            Cr1 = Cr2 = 0.5*(abs(0.5*(Vc+Vp))+abs(0.5*(Vc+Vm)))*dt/delta
                            dHdx1 = dHdx2 = (0.5*(Hc+Hp) - 0.5*(Hc+Hm)) / delta - dib

                        w1, w2 = 1-Cr1**0.5, Cr2**0.5
                        dHdx = w1 * dHdx1 + w2 * dHdx2

# viscous sublayer
#                 Cf = 1.0/(1.0/0.4*np.log(11.0*dep[c]/2.0/di))**2
                Cf = gravity*cManning**2.0/dep[c]**(1.0/3.0)
                Vnorm = np.sqrt(u[c]**2.0+v[c]**2.0)
                Vis = Cf * Vnorm * u[c] if direction == 1 else  Cf * Vnorm * v[c]

# turbulence
#                kenergy = 2.07*Cf*Vnorm**2
                nut = 0.4/6.0*dep[c]*np.sqrt(Cf)*np.abs(Vnorm)

# side boundary : non-slip condition
                if (i == imax-1) or (j == 0) or (i == jmax-1):
                    turb = 0.0
                else :
#                     turb = nut * ( Vdir[xp] - 2.0*Vdir[c] + Vdir[xm] )/ dx**2 \
#                          + nut * ( Vdir[yp] - 2.0*Vdir[c] + Vdir[ym] )/ dy**2

                    turb = nut * ( q[xp] - 2.0*q[c] + q[xm] )/ dx**2 \
                         + nut * ( q[yp] - 2.0*q[c] + q[ym] )/ dy**2
               #tmp
#                 turb = 0.0
#                 sourcet = Vis - dep[c] * turb
                sourcet = Vis - turb

                qn[c] = q[c] - dt * ( fluxx[xp] - fluxx[c] ) / dx \
                             - dt * ( fluxy[yp] - fluxy[c] ) / dy \
                             - dt * gravity * depn[c] * dHdx \
                             - dt * sourcet

    if periodic == False :
# upstream boundary
        if direction == 2 :
            qn[0,:] = 0.0
        else :
#             updep = depn[0,:].copy()
#             for j in range(jmax):
#                 updep[j] = 0.0 if updep[j] <= hmin else updep[j]

#             alpha = Qup / dy / np.sum( updep[:]**(5/3) )
#             qn[0,:] = alpha * updep[:]**(5/3)

#             qn[0,:] = 0.0
#             qn[0,117] = 0.001

            updep = np.zeros_like(depn[0,:])
            for j in range(jmax):
                if depn[0,j] > hmin : updep[j] = depn[0,j]

            alpha = Qup / dy / np.sum( updep[:]**(5/3) )
            qn[0,:] = alpha * updep[:]**(5/3)

# downstream boundary
#     qn[-1,:] = qn[-2,:]

    return qn
```

```python
@numba.jit(nopython=True, parallel=False)
def simulation(dep, qx, qy, dzb, dt, dx, dy, ibx, hmin, hbuf, Qup, hdown, cManning):
    depn, count = conEq(dep, qx, qy, dzb, dt, dx, dy, ibx, hmin, hbuf, hdown, False)
    qxn = momentEq(dep, qx, qy, depn, dzb, dt, dx, dy, ibx, hmin, hbuf, 1, Qup, cManning, False)
    qyn = momentEq(dep, qx, qy, depn, dzb, dt, dx, dy, ibx, hmin, hbuf, 2, Qup, cManning, False)
#     CFL = ((np.abs(qxn/depn) + np.sqrt(9.8*depn))/dx + ( np.abs(qyn/depn) + np.sqrt(9.8*depn) )/dy)*dt
    CFL = ((np.abs(qxn/depn))/dx + ( np.abs(qyn/depn))/dy)*dt
    CFLmax = np.max(CFL)
    return depn, qxn, qyn, CFLmax, count
```

# main

```python
ds = xr.open_dataset('zb.nc')

hmin = float(10.0**(-5))
hbuf = float(10.0**(-2))

cManning = 0.025

dx, dy = float(2.5), float(2.5)
dtini = float(0.05)

nxmax, nymax = ds.dims['x'], ds.dims['y']
qx = np.zeros((nxmax,nymax), dtype=np.float64)
qy = np.zeros_like(qx, dtype=np.float64)
dep = np.full_like(qx, hmin, dtype=np.float64)
zb = np.zeros_like(qx, dtype=np.float64)
zb[:,:] = ds['elevation'].values[:,:]

# initial condition
WL = 27.0
dep0 = WL - zb[0,:]
dep0 = np.where(dep0<hmin, hmin, dep0)
dep[0,:] = dep0
dep[1,:] = dep0
```

```python
# %%time
Qt = float(100)
dic = ds.attrs

# CFL = float(0.4)

t = float(0)
dt = 0.025
dtout= float(3600.0)
tmax = float(5.0*3600.+0.1)
nout = 0

while tmax >= t :
    t += dt
    dep, qx, qy, CFLmax, count = simulation(dep, qx, qy, zb, dt, dx, dy, float(0), hmin, hbuf, Qt, float(0), cManning)

# update dt
#     dt = np.round( dt * CFL/CFLmax, 5)

    if t >= nout*dtout :
        print(t, dt, CFLmax, count)
        dic['total_second'] = round(t, 2)
        dss = xr.Dataset({'depth': (['x','y'], dep), 'u': (['x','y'], qx/dep), 'v': (['x','y'], qy/dep)
                          , 'elevation': (['x','y'], zb) }
                          , coords={'xc': (('x', 'y'), ds['xc']), 'yc': (('x', 'y'), ds['yc'])}
                          , attrs=dic )

        out = dss.to_netcdf('out' + str(nout).zfill(8) + '.nc')
        out = dss.close()
        del out
        nout += 1

```