# Optimization of a State-to-State Transfer in a Lambda System in the RWA

```python
# NBVAL_IGNORE_OUTPUT
%load_ext watermark
import os
import numpy as np
import scipy
import matplotlib
import matplotlib.pylab as plt
import krotov
import qutip
from qutip import Qobj
%watermark -v --iversions
```

    Python implementation: CPython
    Python version       : 3.8.1
    IPython version      : 7.24.1

    matplotlib: 3.4.2
    krotov    : 1.2.1+dev
    numpy     : 1.20.3
    qutip     : 4.6.1
    scipy     : 1.6.3

$\newcommand{tr}[0]{\operatorname{tr}}
\newcommand{diag}[0]{\operatorname{diag}}
\newcommand{abs}[0]{\operatorname{abs}}
\newcommand{pop}[0]{\operatorname{pop}}
\newcommand{aux}[0]{\text{aux}}
\newcommand{opt}[0]{\text{opt}}
\newcommand{tgt}[0]{\text{tgt}}
\newcommand{init}[0]{\text{init}}
\newcommand{lab}[0]{\text{lab}}
\newcommand{rwa}[0]{\text{rwa}}
\newcommand{bra}[1]{\langle#1\vert}
\newcommand{ket}[1]{\vert#1\rangle}
\newcommand{Bra}[1]{\left\langle#1\right\vert}
\newcommand{Ket}[1]{\left\vert#1\right\rangle}
\newcommand{Braket}[2]{\left\langle #1\vphantom{#2}\mid{#2}\vphantom{#1}\right\rangle}
\newcommand{ketbra}[2]{\vert#1\rangle\!\langle#2\vert}
\newcommand{op}[1]{\hat{#1}}
\newcommand{Op}[1]{\hat{#1}}
\newcommand{dd}[0]{\,\text{d}}
\newcommand{Liouville}[0]{\mathcal{L}}
\newcommand{DynMap}[0]{\mathcal{E}}
\newcommand{identity}[0]{\mathbf{1}}
\newcommand{Norm}[1]{\lVert#1\rVert}
\newcommand{Abs}[1]{\left\vert#1\right\vert}
\newcommand{avg}[1]{\langle#1\rangle}
\newcommand{Avg}[1]{\left\langle#1\right\rangle}
\newcommand{AbsSq}[1]{\left\vert#1\right\vert^2}
\newcommand{Re}[0]{\operatorname{Re}}
\newcommand{Im}[0]{\operatorname{Im}}$

This example is illustrates the use of complex-valued control fields. This is
accomplished by rewriting the Hamiltonian as the sum of two independent
controls (real and imaginary parts). We consider a 3-level system in a
$\Lambda$ configuration, and seek control pulses that implement a
(phase-sensitive) state-to-state transition $\ket{1} \rightarrow \ket{3}$.

## The rotating wave Hamiltonian

The system consists of three levels $\ket{1}$, $\ket{2}$ and $\ket{3}$ with
energy levels $E_{1}, E_{2}$ and $E_{3}$ which interact with a pair of laser
pulses $\epsilon_{P}(t)$ ("pump laser") and $\epsilon_{S}(t)$ ("Stokes laser"),
respectively, see Chapter 15.4.2 in ["Introduction to Quantum Mechanics: A
Time-Dependent Perspective" by David Tannor][Tannor] for details.

[Tannor]: http://www.weizmann.ac.il/chemphys/tannor/Book/

In the lab frame, the Hamiltonian reads

$$
\Op{H}_{\text{lab}} = \begin{pmatrix}
    E_1                     & -\mu_{12} \epsilon_P(t) & 0                          \\
    -\mu_{12} \epsilon_P(t) & E_2                     & - \mu_{23} \epsilon_S(t) \\
    0                       & -\mu_{23} \epsilon_S(t) &  E_2
\end{pmatrix}\,.
$$

with the dipole values $\mu_{12}$, $\mu_{23}$ describing the coupling to the
(real-valued) control fields $\epsilon_P(t)$, $\epsilon_S(t)$. The "rotating
frame" is defined as

$$\ket{\Psi_{\text{rot}}} = \Op{U}_0^\dagger \ket{\Psi_{\text{lab}}}$$

with the transformation

$$\op{U}_{0} = \ketbra{1}{1}
e^{-i\left(E_{2} - \omega_{P} \right)t} + \ketbra{2}{2} e^{-iE_{2}t} +
\ketbra{3}{3} e^{-i\left(E_{2}-\omega_{S}\right)t}\,,$$

where $\omega_{P}$ and $\omega_{S}$ are the two central frequencies defining
the rotating frame.

The condition of having to fulfill the Schrödinger equation in the rotating
frame implies a rotating frame Hamiltonian defined as

$$\op{H}_{\text{rot}}  = \op{U}_{0}^{\dagger} \op{H}_{\text{lab}} \op{U}_{0}  - i \op{U}_{0}^{\dagger} \dot{\op{U}}_{0}\,.$$

Note that most textbooks use $\Op{U}$ instead of $\Op{U}^\dagger$, and thus the
adjoint of the above equation to define the rotating frame transformation, but
we follow the example of Tannor's book here.

The rotating frame Hamiltonian reads
$$
\Op{H}_\text{rot} = \begin{pmatrix}
    E_1 + \omega_P - E_2                      & -\mu_{12} \epsilon_P(t) e^{-i \omega_P t} & 0                          \\
    -\mu_{12} \epsilon_P(t) e^{+i \omega_P t} &  0                                        & - \mu_{23} \epsilon_S(t) e^{-i \omega_S t}\\
    0                                         & -\mu_{23} \epsilon_S(t) e^{+i \omega_S t} &  E3 + \omega_S -E_2
\end{pmatrix}\,.
$$

We can now write the fields as

$$
\begin{split}
\mu_{12} \epsilon_{P}(t)
    &= \Omega_{P}^{(1)}(t) \cos{(\omega_P t)} - \Omega_{P}^{(2)}(t) \sin{(\omega_P t)} \\
    &= \Omega_{P}^{(1)}(t) \left( e^{i \omega_P t} + e^{-i \omega_P t}\right)
        + i \Omega_{P}^{(2)}(t) \left( e^{i \omega_P t} - e^{-i \omega_P t} \right) \,,
\end{split}
$$

and similarly for $\epsilon_{S}(t)$, where we have split each field into two
arbitrary (real-valued) auxiliary fields $\Omega_{P}^{(1)}(t),
\Omega_{P}^{(2)}(t)$, and $\Omega_{S}^{(1)}(t), \Omega_{S}^{(2)}(t)$. This
rewriting is suggestive of controls being spectrally centered around $\omega_P$
and $\omega_S$, respectively, in which case any oscillations in
$\Omega_{P,S}^{(1,2)}(t)$ are on a much slower time scale than $\omega_{P, S}$.
Mathematically, however, *any* control fields can written in the above form.
Thus, we have not placed any restriction on the controls at this time.

Plugging this into $\Op{H}_\text{rot}$ and invoking the rotating wave
approximation that neglects all fast oscillating terms $\propto e^{\pm i 2
\omega_{P,S} t}$, we find

$$
\Op{H}_\text{RWA} = \begin{pmatrix}
    \Delta_P                    & -\frac{1}{2} \Omega_P(t)   & 0                          \\
    -\frac{1}{2} \Omega_P^*(t)  &  0                         &  -\frac{1}{2} \Omega_S(t)  \\
    0                           & -\frac{1}{2} \Omega_S^*(t) & \Delta_S
\end{pmatrix}\,,
$$

with the detunings $\Delta_P \equiv E_1 + \omega_P - E_2$, $\Delta_S  \equiv E3
+ \omega_S -E_2$ and the complex-valued control fields $\Omega_P(t) \equiv
\Omega_{P}^{(1)}(t) + i \Omega_{P}^{(2)}(t)$ and $\Omega_S(t) \equiv
\Omega_{S}^{(1)}(t) + i \Omega_{S}^{(2)}(t)$, illustrated in the following
diagram:

Most textbooks (including Tannor's) only allow control fields of the form
$\epsilon_{P,S}(t) \propto \Omega_{P,S}(t) \cos{(\omega_{P,S} t)}$ with the
pulse envelopes $\Omega_{P,S}(t) \in \mathbb{R}^+$. This will result in the
same $\Op{H}_\text{RWA}$ as above, but with the positive real-valued envelopes
instead of the complex-valued $\Omega_{P,S}(t)$. However, this restriction is
unnecessary: complex-valued control fields in the RWA are more general and
entirely physical, with the relation to the real-valued field in the lab
frame as defined above. The spectra of the optimized pulses are free to deviate
from the frequencies of the rotating frame, limited only by the numerical
resolution of the time grid and the RWA.

The `krotov` package requires that all control pulses are real-valued.
Therefore, the real and imaginary parts of $\Omega_{P}$ and $\Omega_{S}$ are
treated as independent Hamiltonians, and we write

$$
\Op{H}_\text{RWA}
    = \Op{H_0}
    + \Omega_{P}^{(1)}(t) \Op{H}_{P,\text{re}}
    + \Omega_{P}^{(2)}(t) \Op{H}_{P,\text{im}}
    + \Omega_{S}^{(1)}(t) \Op{H}_{S,\text{re}}
    + \Omega_{S}^{(2)}(t) \Op{H}_{S,\text{im}}
$$

for the purpose of the optimization, with

$$
\begin{align}
\Op{H_0} &= \Delta_P \ketbra{1}{1} + \Delta_S \ketbra{3}{3}\,, \\
\Op{H}_{P,\text{re}} &= -\frac{1}{2} \left(\ketbra{1}{2} + \ketbra{2}{1}\right)\,, \\
\Op{H}_{P,\text{im}} &= -\frac{i}{2} \left(\ketbra{1}{2} - \ketbra{2}{1}\right)\,, \\
\Op{H}_{S,\text{re}} &= -\frac{1}{2} \left(\ketbra{2}{3} + \ketbra{3}{2}\right)\,, \\
\Op{H}_{S,\text{im}} &= -\frac{i}{2} \left(\ketbra{2}{3} - \ketbra{3}{2}\right)\,.
\end{align}
$$

## Guess controls

We choose the initial guess for the four control fields based on the intuition
of the "stimulated Raman adiabatic passage" (STIRAP) scheme. STIRAP allows to
transfer the population in $\ket{1}$ $\ket{3}$ without having to pass through
$\ket{2}$; it requires the Stokes-pulse to precede but overlap the pump-pulse.

Here, we leave it up to Krotov's method to find appropriate pulses for a
STIRAP-like transfer (without requiring that the $\ket{2}$ level remains
unpopulated).  We start from a low intensity real-valued $\Omega_S(t)$ pulse
with a Blackman shape, followed by an overlapping real-valued $\Omega_P(t)$ of
the same shape. The entire scheme is in the time interval [0, 5].

```python
def Omega_P1(t, args):
    """Guess for the real part of the pump pulse"""
    Ω0 = 5.0
    return Ω0 * krotov.shapes.blackman(t, t_start=2.0, t_stop=5.0)

def Omega_P2(t, args):
    """Guess for the imaginary part of the pump pulse"""
    return 0.0

def Omega_S1(t, args):
    """Guess for the real part of the Stokes pulse"""
    Ω0 = 5.0
    return Ω0 * krotov.shapes.blackman(t, t_start=0.0, t_stop=3.0)

def Omega_S2(t, args):
    """Guess for the imaginary part of the Stokes pulse"""
    return 0.0
```

We can now instantiate the Hamiltonian including these guess controls:

```python
def hamiltonian(E1=0.0, E2=10.0, E3=5.0, omega_P=9.5, omega_S=4.5):
    """Lambda-system Hamiltonian in the RWA"""

    # detunings
    ΔP = E1 + omega_P - E2
    ΔS = E3 + omega_S - E2

    H0 = Qobj([[ΔP, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, ΔS]])

    HP_re = -0.5 * Qobj([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
    HP_im = -0.5 * Qobj([[0.0, 1.0j, 0.0], [-1.0j, 0.0, 0.0], [0.0, 0.0, 0.0]])

    HS_re = -0.5 * Qobj([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])
    HS_im = -0.5 * Qobj([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0j], [0.0, -1.0j, 0.0]])

    return [
        H0,
        [HP_re, Omega_P1],
        [HP_im, Omega_P2],
        [HS_re, Omega_S1],
        [HS_im, Omega_S2],
    ]

```

```python
H = hamiltonian()
```

## Target state in the rotating frame

The basis states of the $\Lambda$-system are defined as

```python
ket1 = qutip.Qobj(np.array([1.0, 0.0, 0.0]))
ket2 = qutip.Qobj(np.array([0.0, 1.0, 0.0]))
ket3 = qutip.Qobj(np.array([0.0, 0.0, 1.0]))
```

We would like to implement a phase-sensitive transition $\ket{1} \rightarrow
\ket{3}$ *in the lab frame*. Since we are defining the dynamics in the RWA,
this means we have to adjust the target state to be in the rotating frame as
well (the initial state at $t=0$ is not affected by the RWA).

As defined earlier, the states in the rotating frame are obtained from the
states in the lab frame by the transformation $\ket{\Psi_{\text{rot}}} =
\Op{U}_0^\dagger \ket{\Psi_{\text{lab}}}$. In our case, this means that we get
$\ket{3}$ with and additional phase factor:

```python
def rwa_target_state(ket3, E2=10.0, omega_S=4.5, T=5):
    return np.exp(1j * (E2 - omega_S) * T) * ket3
```

```python
psi_target = rwa_target_state(ket3)
```

We can now instantiate the control objective:

```python
objective = krotov.Objective(initial_state=ket1, target=psi_target, H=H)
objective
```

    Objective[|Ψ₀(3)⟩ to |Ψ₁(3)⟩ via [H₀[3,3], [H₁[3,3], u₁(t)], [H₂[3,3], u₂(t)], [H₃[3,3], u₃(t)], [H₄[3,3], u₄(t)]]]

## Simulate dynamics under the guess field

We use a time grid with 500 steps between $t=0$ and $T=5$:

```python
tlist = np.linspace(0, 5, 500)
```

Before propagating, we visually verify the guess pulses we defined earlier:

```python
def plot_pulse(pulse, tlist, label):
    fig, ax = plt.subplots()
    if callable(pulse):
        pulse = np.array([pulse(t, args=None) for t in tlist])
    ax.plot(tlist, pulse)
    ax.set_xlabel('time')
    ax.set_ylabel('%s pulse amplitude' % label)
    plt.show(fig)
```

```python
plot_pulse(H[1][1], tlist, 'Ωₚ')
plot_pulse(H[3][1], tlist, 'Ωₛ')
```

The imaginary parts are zero:

```python
assert np.all([H[2][1](t, None) == 0 for t in tlist])
assert np.all([H[4][1](t, None) == 0 for t in tlist])
```

We introduce projectors $\op{P}_{i} =
\ketbra{i}{i}$ for each of the three energy levels, allowing use to plot the population dynamics:

```python
proj1 = qutip.ket2dm(ket1)
proj2 = qutip.ket2dm(ket2)
proj3 = qutip.ket2dm(ket3)
```

```python
guess_dynamics = objective.mesolve(tlist, e_ops=[proj1,proj2,proj3])
```

```python
def plot_population(result):
    fig, ax = plt.subplots()
    ax.plot(result.times, result.expect[0], label='1')
    ax.plot(result.times, result.expect[1], label='2')
    ax.plot(result.times, result.expect[2], label='3')
    ax.legend()
    ax.set_xlabel('time')
    ax.set_ylabel('population')
    plt.show(fig)
```

```python
plot_population(guess_dynamics)
```

We find that our guess pulses are too disjoint to implement the STIRAP scheme.
Thus, the Stokes pulse has no effect, whilst the pump pulse merely transfers
population out of $\ket{1}$ into $\ket{2}$ and back again.

## Optimize

In order to invoke `optimize_pulses`, we must define the required parameters
for each control, a pulse shape (used to ensure that the controls remain 0 at
$t=0$ and $t=T$), and the parameter $\lambda_a$ that determines the overall
magnitude of the pulse updates in each iteration.

```python
def S(t):
    """Scales the Krotov methods update of the pulse value at the time t"""
    return krotov.shapes.flattop(
        t, t_start=0.0, t_stop=5.0, t_rise=0.3, func='sinsq'
    )
```

```python
pulse_options = {
    H[1][1]: dict(lambda_a=0.5, update_shape=S),
    H[2][1]: dict(lambda_a=0.5, update_shape=S),
    H[3][1]: dict(lambda_a=0.5, update_shape=S),
    H[4][1]: dict(lambda_a=0.5, update_shape=S)
}
```

We now run the optimization, using the phase-sensitive functional $J_{T,
\text{re}} = 1 - \Re\Braket{\Psi(t)}{\Psi_{\tgt}}$, printing the integrated
pulse update for each control in each iteration. The optimization stops when
$J_T$ falls below $10^{-3}$, changes by less than $10^{-5}$, or after at most
15 iterations. We also check for monotonic convergence.

```python
opt_result = krotov.optimize_pulses(
    [objective],
    pulse_options,
    tlist,
    propagator=krotov.propagators.expm,
    chi_constructor=krotov.functionals.chis_re,
    info_hook=krotov.info_hooks.print_table(
        J_T=krotov.functionals.J_T_re,
        show_g_a_int_per_pulse=True,
        unicode=False,
    ),
    check_convergence=krotov.convergence.Or(
        krotov.convergence.value_below(1e-3, name='J_T'),
        krotov.convergence.delta_below(1e-5),
        krotov.convergence.check_monotonic_error,
    ),
    iter_stop=15,
)
```

    iter.      J_T   g_a_int_1   g_a_int_2   g_a_int_3   g_a_int_4    g_a_int          J  Delta J_T    Delta J  secs
    0     1.01e+00    0.00e+00    0.00e+00    0.00e+00    0.00e+00   0.00e+00   1.01e+00        n/a        n/a     0
    1     6.72e-01    8.60e-02    2.87e-04    8.17e-02    3.72e-04   1.68e-01   8.40e-01  -3.37e-01  -1.68e-01     1
    2     4.02e-01    7.20e-02    4.21e-04    6.22e-02    4.20e-04   1.35e-01   5.37e-01  -2.70e-01  -1.35e-01     1
    3     2.22e-01    4.91e-02    4.64e-04    3.99e-02    3.88e-04   8.98e-02   3.12e-01  -1.80e-01  -8.98e-02     1
    4     1.17e-01    2.89e-02    3.87e-04    2.29e-02    3.01e-04   5.25e-02   1.69e-01  -1.05e-01  -5.25e-02     1
    5     6.00e-02    1.56e-02    2.69e-04    1.23e-02    2.10e-04   2.84e-02   8.84e-02  -5.69e-02  -2.84e-02     1
    6     3.05e-02    8.08e-03    1.71e-04    6.37e-03    1.39e-04   1.48e-02   4.52e-02  -2.95e-02  -1.48e-02     1
    7     1.54e-02    4.08e-03    1.06e-04    3.24e-03    9.10e-05   7.51e-03   2.30e-02  -1.50e-02  -7.51e-03     1
    8     7.85e-03    2.04e-03    6.65e-05    1.63e-03    5.99e-05   3.79e-03   1.16e-02  -7.59e-03  -3.79e-03     1
    9     4.02e-03    1.02e-03    4.31e-05    8.14e-04    4.01e-05   1.91e-03   5.94e-03  -3.83e-03  -1.91e-03     1
    10    2.09e-03    5.05e-04    2.88e-05    4.07e-04    2.73e-05   9.68e-04   3.05e-03  -1.94e-03  -9.68e-04     1
    11    1.10e-03    2.52e-04    1.99e-05    2.03e-04    1.88e-05   4.94e-04   1.59e-03  -9.87e-04  -4.94e-04     1
    12    5.90e-04    1.26e-04    1.40e-05    1.02e-04    1.31e-05   2.54e-04   8.45e-04  -5.09e-04  -2.54e-04     1

```python
opt_result
```

    Krotov Optimization Result
    --------------------------
    - Started at 2021-11-07 05:51:35
    - Number of objectives: 1
    - Number of iterations: 12
    - Reason for termination: Reached convergence: J_T < 0.001
    - Ended at 2021-11-07 05:51:59 (0:00:24)

We dump the result of the optimization to disk for later use in the [Ensemble
Optimization for Robust Pulses](08_example_ensemble.ipynb).

```python
if not os.path.isfile('lambda_rwa_opt_result.dump'):
    opt_result.dump('lambda_rwa_opt_result.dump')
```

The optimized complex pulses look as follows:

```python
def plot_pulse_amplitude_and_phase(pulse_real, pulse_imaginary,tlist):
    ax1 = plt.subplot(211)
    ax2 = plt.subplot(212)
    amplitudes = [np.sqrt(x*x + y*y) for x,y in zip(pulse_real,pulse_imaginary)]
    phases = [np.arctan2(y,x)/np.pi for x,y in zip(pulse_real,pulse_imaginary)]
    ax1.plot(tlist,amplitudes)
    ax1.set_xlabel('time')
    ax1.set_ylabel('pulse amplitude')
    ax2.plot(tlist,phases)
    ax2.set_xlabel('time')
    ax2.set_ylabel('pulse phase (π)')
    plt.show()

print("pump pulse amplitude and phase:")
plot_pulse_amplitude_and_phase(
    opt_result.optimized_controls[0], opt_result.optimized_controls[1], tlist)
print("Stokes pulse amplitude and phase:")
plot_pulse_amplitude_and_phase(
    opt_result.optimized_controls[2], opt_result.optimized_controls[3], tlist)
```

We can convert the complex controls in the rotating frame back into the
real-valued pulses in the lab frame:

```python
def plot_physical_field(pulse_re, pulse_im, tlist, case=None):

    if case == 'pump':
        w = 9.5
    elif case == 'stokes':
        w = 4.5
    else:
        print('Error: selected case is not a valid option')
        return

    ax = plt.subplot(111)
    ax.plot(tlist,pulse_re*np.cos(w*tlist)-pulse_im*np.sin(w*tlist), 'r')
    ax.set_xlabel('time', fontsize = 16)
    if case == 'pump':
        ax.set_ylabel(r'$\mu_{12}\,\epsilon_{P}$')
    elif case == 'stokes':
        ax.set_ylabel(r'$ \mu_{23}\,\epsilon_{S}$')
    plt.show()

print('Physical electric pump pulse in the lab frame:')
plot_physical_field(
    opt_result.optimized_controls[0], opt_result.optimized_controls[1], tlist, case = 'pump')

print('Physical electric Stokes pulse in the lab frame:')
plot_physical_field(
    opt_result.optimized_controls[2], opt_result.optimized_controls[3], tlist, case = 'stokes')
```

Lastly, we check the population dynamics to verify that we indeed implement the
desired state-to-state transfer:

```python
opt_dynamics = opt_result.optimized_objectives[0].mesolve(
    tlist, e_ops=[proj1, proj2, proj3])
```

```python
plot_population(opt_dynamics)
```


```python
from resources.workspace import *
```

$
% START OF MACRO DEF
% DO NOT EDIT IN INDIVIDUAL NOTEBOOKS, BUT IN macros.py
%
\newcommand{\Reals}{\mathbb{R}}
\newcommand{\Expect}[0]{\mathbb{E}}
\newcommand{\NormDist}{\mathcal{N}}
%
\newcommand{\DynMod}[0]{\mathscr{M}}
\newcommand{\ObsMod}[0]{\mathscr{H}}
%
\newcommand{\mat}[1]{{\mathbf{{#1}}}}
%\newcommand{\mat}[1]{{\pmb{\mathsf{#1}}}}
\newcommand{\bvec}[1]{{\mathbf{#1}}}
%
\newcommand{\trsign}{{\mathsf{T}}}
\newcommand{\tr}{^{\trsign}}
\newcommand{\tn}[1]{#1}
\newcommand{\ceq}[0]{\mathrel{≔}}
%
\newcommand{\I}[0]{\mat{I}}
\newcommand{\K}[0]{\mat{K}}
\newcommand{\bP}[0]{\mat{P}}
\newcommand{\bH}[0]{\mat{H}}
\newcommand{\bF}[0]{\mat{F}}
\newcommand{\R}[0]{\mat{R}}
\newcommand{\Q}[0]{\mat{Q}}
\newcommand{\B}[0]{\mat{B}}
\newcommand{\C}[0]{\mat{C}}
\newcommand{\Ri}[0]{\R^{-1}}
\newcommand{\Bi}[0]{\B^{-1}}
\newcommand{\X}[0]{\mat{X}}
\newcommand{\A}[0]{\mat{A}}
\newcommand{\Y}[0]{\mat{Y}}
\newcommand{\E}[0]{\mat{E}}
\newcommand{\U}[0]{\mat{U}}
\newcommand{\V}[0]{\mat{V}}
%
\newcommand{\x}[0]{\bvec{x}}
\newcommand{\y}[0]{\bvec{y}}
\newcommand{\z}[0]{\bvec{z}}
\newcommand{\q}[0]{\bvec{q}}
\newcommand{\br}[0]{\bvec{r}}
\newcommand{\bb}[0]{\bvec{b}}
%
\newcommand{\bx}[0]{\bvec{\bar{x}}}
\newcommand{\by}[0]{\bvec{\bar{y}}}
\newcommand{\barB}[0]{\mat{\bar{B}}}
\newcommand{\barP}[0]{\mat{\bar{P}}}
\newcommand{\barC}[0]{\mat{\bar{C}}}
\newcommand{\barK}[0]{\mat{\bar{K}}}
%
\newcommand{\D}[0]{\mat{D}}
\newcommand{\Dobs}[0]{\mat{D}_{\text{obs}}}
\newcommand{\Dmod}[0]{\mat{D}_{\text{obs}}}
%
\newcommand{\ones}[0]{\bvec{1}}
\newcommand{\AN}[0]{\big( \I_N - \ones \ones\tr / N \big)}
%
% END OF MACRO DEF
$
## Dynamical systems
are systems (sets of equations) whose variables evolve in time (the equations contains time derivatives). As a branch of mathematics, its theory is mainly concerned with understanding the behaviour of solutions (trajectories) of the systems.

## Chaos
is also known as the butterfly effect: "a butterfly that flaps its wings in Brazil can 'cause' a hurricane in Texas".

As opposed to the opinions of Descartes/Newton/Laplace, chaos effectively means that even in a deterministic (non-stochastic) universe, we can only predict "so far" into the future. This will be illustrated below using two toy-model dynamical systems made by Edward Lorenz.

---
## The Lorenz (1963) attractor

The [Lorenz-63 dynamical system](https://en.wikipedia.org/wiki/Lorenz_system) can be derived as an extreme simplification of *Rayleigh-Bénard convection*: fluid circulation in a shallow layer of fluid uniformly heated (cooled) from below (above).
This produces the following 3 *coupled* ordinary differential equations (ODE):

$$
\begin{aligned}
\dot{x} & = \sigma(y-x) \\
\dot{y} & = \rho x - y - xz \\
\dot{z} & = -\beta z + xy
\end{aligned}
$$

where the "dot" represents the time derivative, $\frac{d}{dt}$. The state vector is $\x = (x,y,z)$, and the parameters are typically set to

```python
SIGMA = 10.0
BETA  = 8/3
RHO   = 28.0
```

The ODEs can be coded as follows

```python
def dxdt(xyz, t0, sigma, beta, rho):
    """Compute the time-derivative of the Lorenz-63 system."""
    x, y, z = xyz
    return [
        sigma * (y - x),
        x * (rho - z) - y,
        x * y - beta * z
    ]
```

#### Numerical integration to compute the trajectories

Below is a function to numerically **integrate** the ODEs and **plot** the solutions.

```python
from scipy.integrate import odeint
output_63 = [None]

@interact(         sigma=(0.,50), beta=(0.,5), rho=(0.,50), N=(0,50), eps=(0.01,1), T=(0.,40))
def animate_lorenz(sigma=SIGMA,   beta=BETA,   rho=RHO    , N=2,      eps=0.01,     T=1.0):

    # Initial conditions: perturbations around some "proto" state
    seed(1)
    x0_proto = array([-6.1, 1.2, 32.5])
    x0 = x0_proto + eps*randn((N, 3))

    # Compute trajectories
    tt = linspace(0, T, int(100*T)+1)             # Time sequence for trajectory
    dd = lambda x,t: dxdt(x,t, sigma,beta,rho)    # Define dxdt(x,t) with fixed params.
    xx = array([odeint(dd, xn, tt) for xn in x0]) # Integrate
    output_63[0] = xx

    # PLOTTING
    ax = plt.figure(figsize=(10,5)).add_subplot(111, projection='3d')
    ax.axis('off')
    colors = plt.cm.jet(linspace(0,1,N))
    for n in range(N):
        ax.plot(*(xx[n,:,:].T),'-'  , color=colors[n])
        ax.scatter3D(*xx[n,-1,:],s=40,color=colors[n])
```

**Exc 2**:
* Move `T` (use your arrow keys). What does it control?
* Set `T` to something small; move the sliders for `N` and `eps`. What do they control?
* Visually investigate the system's (i.e. the trajectories') sensitivity to initial conditions by moving `T`, `N` and `eps`. How long do you think it takes (on average) for two trajectories (or the estimation error) to grow twice as far apart as they started (alternatives: 0.03, 0.3, 3, 30)?

### Averages

Slide `N` and `T` to their upper bounds. Execute the code cell below.

```python
# Compute the average location of the $m$-th component of the state in TWO ways.

m  = 0 # state component index (must be 0,1,2)
nB = 20
xx = output_63[0][:,:,m]

plt.hist(xx[:,-1]   ,density=1,bins=nB, label="ensemble dist.",alpha=1.0) # -1: last time
plt.hist(xx[-1,:]   ,density=1,bins=nB, label="temporal dist.",alpha=0.5) # -1: last ensemble member
#plt.hist(xx.ravel(),density=1,bins=nB, label="total    distribution",alpha=0.5)
plt.legend();
```

**Exc 6*:** Answer the questions below.
 * (a) Do you think the samples behind the histograms are drawn from the same distribution?
 * (b) The answer to the above question means that this dynamical system is [ergodic](https://en.wikipedia.org/wiki/Ergodic_theory#Ergodic_theorems).
 Now, suppose we want to investigate which (DA) method is better at estimating the true state (trajectory) for this system, on average. Should we run several short experiments or one long one?

```python
#show_answer("Ergodicity a")
#show_answer("Ergodicity b")
```

---

## The "Lorenz-95" model

The Lorenz-96 system
is a "1D" model, designed to resemble atmospheric convection.  Each state variable $\x_m$ can be considered some atmospheric quantity at grid point at a fixed latitude of the earth.  The system
is given by the coupled set of ODEs,
$$
\frac{d \x_m}{dt} = (\x_{m+1} − \x_{m-2}) \x_{m-1} − \x_m + F
\, ,
\quad \quad m \in \{1,\ldots,M\}
\, ,
$$
where the subscript indices apply periodically.

This model is not derived from physics but has similar characteristics, such as

 there is external forcing, determined by a parameter $F$;

 there is internal dissipation, emulated by the linear term;

 there is energy-conserving advection, emulated by quadratic terms.

[Further description in the very readable original article](http://eaps4.mit.edu/research/Lorenz/Predicability_a_Problem_2006.pdf).

**Exc 10:** Show that the "total energy" $\sum_{m=1}^{M} \x_m^2$ is preserved by the quadratic terms in the ODE.

```python
#show_answer("Hint: Lorenz energy")
#show_answer("Lorenz energy")
```

The model is animated below.

```python
# For all m, any n: s(x,n) := x[m+n], circularly.
def s(x,n):
    return np.roll(x,-n)

output_95 = [None]

@interact(            M=(5,60,1), Force=(0,40,1), eps=(0.01,3,0.1), T=(0.05,40,0.05))
def animate_lorenz_95(M=40,       Force=8.0,      eps=0.01,T=0):
    # Initial conditions: perturbations
    x0 = zeros(M)
    x0[0] = eps

    def dxdt(x,t):
        return (s(x,1)-s(x,-2))*s(x,-1) - x + Force

    tt = linspace(0, T, int(40*T)+1)
    xx = odeint(lambda x,t: dxdt(x,t), x0, tt)
    output_95[0] = xx

    plt.figure(figsize=(7,4))

    # Plot last only
    #plt.plot(xx[-1],'b')

    # Plot multiple
    Lag = 8
    colors = plt.cm.cubehelix(0.1+0.6*linspace(0,1,Lag))
    for k in range(Lag,0,-1):
        plt.plot(xx[max(0,len(xx)-k)],color=colors[Lag-k])
    plt.ylim(-10,20)
```

**Exc 12:** Investigate by moving the sliders: Under which settings of the force `F` is the system chaotic (is the predictability horizon finite)?

---
## Error/perturbation dynamics

**Exc 14*:** Suppose $x(t)$ and $z(t)$ are "twins": they evolve according to the same law $f$:
$$
\begin{align}
\frac{dx}{dt} &= f(x) \\
\frac{dz}{dt} &= f(z) \, .
\end{align}
$$

Define the "error": $\varepsilon(t) = x(t) - z(t)$.
Suppose $z(0)$ is close to $x(0)$.
Let $F = \frac{df}{dx}(x(t))$.

* a) Show that the error evolves according to the ordinary differential equation (ODE)
$$\frac{d \varepsilon}{dt} \approx F \varepsilon \, .$$

```python
#show_answer("error evolution")
```

* b) Suppose $F$ is constant. Show that the error grows exponentially: $\varepsilon(t) = \varepsilon(0) e^{F t} $.

```python
#show_answer("anti-deriv")
```

* c)
 * 1) Suppose $F<1$.
 What happens to the error?
 What does this mean for predictability?
 * 2) Now suppose $F>1$.
 Given that all observations are uncertain (i.e. $R_t>0$, if only ever so slightly),
 can we ever hope to estimate $x(t)$ with 0 uncertainty?

```python
#show_answer("predictability cases")
```

* d) Consider the ODE derived above.
How might we change it in order to model (i.e. emulate) a saturation of the error at some level?
Can you solve this equation?

```python
#show_answer("saturation term")
```

* e) Now suppose $z(t)$ evolves according to $\frac{dz}{dt} = g(z)$, with $g \neq f$.
What is now the differential equation governing the evolution of the error, $\varepsilon$?

```python
#show_answer("liner growth")
```

**Exc 16*:** Recall the Lorenz-63 system. What is its doubling time (i.e. estimate how long does it take for two trajectories to grow twice as far apart as they were to start with) ?
*Hint: Set `N=50, eps=0.01, T=1,` and compute the spread of the particles now as compared to how they started*

```python
xx = output_63[0][:,-1] # Ensemble of particles at the end of integration
### compute your answer here ###
```

```python
#show_answer("doubling time")
```

The answer actually depends on where in "phase space" the particles started.
To get a universal answer one must average these experiments for many different initial conditions.

## In summary:
Prediction (forecasting) with these systems is challenging because they are chaotic: small errors grow exponentially.

In other words, chaos means that there is a limit to how far into the future we can make predictions (skillfully).

It is therefore crucial to minimize the initial error as much as possible. This is a task for DA.

### Next: [Ensemble representation](T7%20-%20Ensemble%20representation.ipynb)

notebooks/T6 - Dynamical systems, chaos, Lorenz.ipynb


# Modelo de Sovova

En el modelo de Sovova se incluye un efecto de difusión adicinal que no es tenido en cuenta en el modelo de Lack, resultando en un modelo con una ventaja que permite modelar la pendiente de la primera fase de extracción de forma independiente de las otras 2 fases de extracción.

\begin{equation}
z_k = \frac{t_s}{t_r} \frac{\gamma y_r}{x_o} ln \left( \frac{x_o exp(\frac{t-t_1}{t_s} )} {x_o - x_k} \right)
\end{equation}

\begin{equation}
t_1 = \frac{x_o-x_k}{\gamma y_r}t_f
\end{equation}

\begin{equation}
t_2 = t_1+t_s ln \left( \frac{x_k}{x_o}+ \left(1+\frac{x_k}{x_o}\right) exp \left( \frac{x_o}{\gamma y_r} \frac{t_r}{t_s}  \right)   \right)
\end{equation}

\begin{equation}
e(t) = \left\{ \begin{array}{lcc}
             q y_r (1-exp(-A)) & t \leq t_1 \\
             \\ q' y_r [(t-t_1)exp(-A(1 - z_k))] &  t_1 < t \leq t_2 \\
             \\ x_o - \frac{\gamma y_r t_s}{t_r} &  t > t_2
             \end{array}
   \right.
\end{equation}

\renewcommand{\abstractname}{Resumen}
\begin{abstract}
\normalsize
\begin{center}\rule{0.9\textwidth}{0.1mm} \end{center}
\vspace*{0.5cm}
\end{abstract}
\renewcommand{\abstractname}{Abstract}
\begin{abstract}
\normalsize

Se desarrolló una herramienta para calcular y ajustar los parámetros de difusión y ruptura de matriz vegetal, de los modelos Broken and Intact Cell (BC), los cuales son ampliamente utilizandos en el análisis de la cinética de extracción de solutos inmersos en una matriz vegetal con fluidos supercriticos, permitiendo el procesamiento de los datos experimentales de forma simple para observar su comportamiento y agilizar el análisis del trabajo experimental.

\\ \\
Keywords: fluid supercritical, Python.
\begin{center}\rule{0.9\textwidth}{0.1mm} \end{center}
\vspace*{0.5cm}
\end{abstract}
\end{@twocolumnfalse}
]

\section{INTRODUCCIÓN}

\section{Sepy}

\href{http://www.sepy.io/}{\textcolor{black}{\underline{Sepy}}}
es una librería implementada en el lenguaje de programación Python con licencia MIT, orientada al procesamiento y análisis de datos experimentales de la cinética de extracción con fluidos supercriticos basandose en los modelos denominados Broken and Intact Cell (BC), más conocidos en la literatura especializada como modelos de Sovova, los cuales son presentados en el apendice.

La librería Sepy está disponible en Python Package Index (PyPI) para ser instalada facilmente a traves de internet, utilizando la instrucción en una terminal de comandos.

\begin{minted}[style=friendly]{python}
pip install sepy
\end{minted}

Una vez instalada la librería, se puede importar para comenzar con el análisis de datos de extracción junto con otras librerías python.

\begin{minted}[style=friendly]{python}

import sepy as sep
from sepy import CineticaExtraccion
from sepy import CineticaExtraccionGUI

extraccion_1 = CineticaExtraccion()

interfaz_1 = CineticaExtraccionGUI()

\end{minted}

Esta librería se puede utilizar de 2 formas, la primera es acceder a sus métodos y atributos por medio de código Python y la segunda forma es a través del framework Jupyter Notebook[x], el cual permite el uso interactivo de código enriquecido con diversos contenidos multimedia, imagenes, textos en diferentes formatos y widgets, siendo estos últimos utilizados por Sepy para facilitar el procesamiento de los datos experimentales de extracción, por medio de una interfaz gráfica de usuario como la que se muestra en la figura 1.

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{sepy_gui}
  \caption{Uso interactivo de Sepy}
  \label{fig:ejemplo}
\end{figure}

Por otro lado, en la figura 2 se muestra un esquema de la organización de los modulos relevantes de la librería Sepy, que se implmentaron sobre la base de librerías especializadas en el análisis de datos y calculo numérico como Numpy, Scipy Pandas y Matplotlib entre otras del ecosistema del lenguaje Python.

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{sepy_p}
  \caption{Digrama de paquetes}
  \label{fig:ejemplo}
\end{figure}

\section{RESULTADOS Y DISCUSIÓN}

Utilizando la librería desarrollada Sepy, se realizaron simulaciones y ajustes de parámetros de los modelos de extracción supercritica de los modelos de Reverchon y BC, tomando los datos experimentales de extracción con dioxido de carbono supercritico $scCO_2$ sobre 3 tipos de matriz vegetal: Yerba Bolbo, Yerba Brasil y Yerba Congorosa, cuyas propiedades son presentadas en la tabla 1.

\input{mytableMatrizVegetal}

En la tabla 2 se muestra las condiciones de operación del sistema de extracción que incluye un equipo de ultrasonido para estimular la ructura de la matriz vegetal durante el proceso.

\input{mytable}

En las figuras 3-15 se muestra el ajuste de los parámetros del modelo de extracción supercritica para los 3 tipos de matriz vegetal y condiciones de operación. En las cuales se puede observa que al incrementar la potencia del ultrasonido se presenta un aumento en el rendimiento de extracción como se esperaba,puesto que se promueve la ruptura de celulas de la matriz vegetal.

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{bolbo_0W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{bolbo_160W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{bolbo_200W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{bolbo_280W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{bolbo_400W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{brasil_0W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{brasil_200W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{brasil_280W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{brasil_400W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{congorosa_0W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{congorosa_200W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{congorosa_280W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\begin{figure}[H]
  \centering
    \includegraphics[width=0.5\textwidth]{congorosa_400W}
  \caption{Cinética de extracción con $scCO_2$}
  \label{fig:ejemplo}
\end{figure}

\input{datosExtraccion}

```python
import sepy as sep
from sepy import CineticaExtraccion
from sepy import CineticaExtraccionGUI

extraccion_1 = CineticaExtraccion()

interfaz_1 = CineticaExtraccionGUI()
```

```python

```

```python

```

```python

```

## CONCLUSIÓN

Se desarrolló la librería Sepy en el lenguaje de progrmación Python, como una herramienta que permite simplificar el procesamiento y análisis de datos experimentales de extracción con fluidos supercriticos utilizando modelos de cinética de extracción basados en las aproximaciones de reactores en serie de flujo piston y el modelo de celulas rotas e intectas, verificando su funcionamiento con datos experimentales de extracción de aceite escencial de 3 tipos de matriz vegetal utilizando $scCO_2$ y diferentes potencias de ultrasonido, cuyo incremento de potencia aumenta el rendimiento de extracción.

En esta sección se presentan 3 de los modelos matemáticos comunmente utilizados en el análisis de la cinética de extracción con fluidos supercriticos.

## Nomenclatura

$a_0$ Surface, $\frac{m^2}{m^3}$

- E. Reverchon, Mathematical modelling of supercritical extraction of sage oil, AIChE J. 42 (1996) 1765–1771.
- Amit Rai, Kumargaurao D.Punase, Bikash Mohanty, Ravindra Bhargava, Evaluation of models for supercritical fluid extraction, International Journal of Heat and Mass Transfer Volume 72, May 2014, Pages 274-287.

```python

```


**Opti & Contrôle - TP1**

DESFORGES Guillaume & DUMONT Louis

```python
import numpy as np
```

```python
import matplotlib
```

```python
matplotlib.rcParams['figure.figsize'] = (15, 10)
```

```python
from src import datas_r
```

# Problème primal

On s'intéresse au problème primal d'optimisation **sans contraintes**.

## Résultats théoriques

```javascript
%%javascript
MathJax.Hub.Config({
    TeX: { equationNumbers: { autoNumber: "AMS" } }
});
```

Montrons les équivalences des différents problèmes:

On part du problème d'optimisation :

\begin{equation}
\underset{Aq-f = 0}{\underset{q\in \mathbb{R}^{n}}{min}< q, r\bullet q\bullet | q|> + <p, f_r>}
\end{equation}

La décomposition  de $A$ et $f$ sous la forme

$$A = \begin{pmatrix}
A_r \\ A_d
\end{pmatrix}
\quad
f= \begin{pmatrix}
f_r \\ f_d
\end{pmatrix}$$

Permet de réecrire la condition sous la forme:

$$\left\{
\begin{array}{l}
    A_d q - f_d = 0 \\
    A_r q - f_r = 0
\end{array}\right.$$

La deuxième de ces conditions s'intègre directement dans l'énergie en remplaçant $f_r$ par $A_r q$. On obtient alors le problème équivalent:

\begin{equation}
\underset{A_dq-f_d = 0}{\underset{q\in \mathbb{R}^{n}}{min}< q, r\bullet q\bullet | q|> + <p, A_rq>}
\end{equation}

De plus, la matrice $A_d$ est de plein rang $m_d$, ce qui permet de la réecrire $A_d = (A_{d,T}\; A_{d,C})$, où $A_{d,T}$ est inversible.

En écrivant de même $q = \begin{pmatrix} q_T \\ q_C \end{pmatrix}$, la condition de $(2)$ équivaut à $$q_T = A_{d,T}^{-1} (f_d - A_{d,C}q_C )$$

Une solution de $(2)$ s'écrit donc $q =q^{(0)}+B q_C$ avec
$$q^{(0)} = \begin{pmatrix}A^{-1}_{d,T}f_d \\ 0_{n-m_d}\end{pmatrix}$$ et
$$B = \begin{pmatrix}-A^{-1}_{d,T}A_{d,C} \\ I_{n-m_d}\end{pmatrix}$$

et est solution de :

\begin{equation}
\underset{q_c \in \mathcal{R}^{n - m_d}}{min} \frac{1}{3}<q^{(0)} + B q_c, r \bullet(q^{(0)} + B q_c)\bullet | q^{(0)} + B q_c| > + <p_r, A_r(q^{(0)} + B q_c)>
\end{equation}

Réciproquement, une solution $q_C$ de $(3)$ permet de construire $q = q^{(0)} + B q_C$ solution de $(2)$ et les problèmes sont donc équivalents.

On calcule les formules du **gradient** et de la **hessienne**.

Pour le gradient on a l'expression suivante :

$\nabla_{q_c}(J) = B^T (r \bullet q \bullet |q| + A_r^T p_r)$

Et pour la Hessienne:

$H_{q_c}(J) = 2B^T (r \bullet |q| \bullet B^T)^T $

Où l'on a étendu la définition de $\bullet$ par : $v\bullet A$ est le produit matriciel de la matrice diagonale $diag(v_0 ,..., v_n)$ et de $X$.

## Oracle

On implémente un **oracle**, c'est à dire une fonction qui puise dans les données du problème pour donner à l'algorithme d'optimisation les informations qui lui sont nécessaire : valeur du critère, vecteur gradient du critère et si demandé matrice Hessienne du critère.

```python
from src.oracle import oracle
```

```python
x0 = np.random.normal(size=datas_r.n-datas_r.md)
```

```python
print("Test de l'oracle")
# test pour une valeur aléatoire
loss, gradient, hessian = oracle(x0, compute_hessian=True)
print("Critère :", loss)
print("Gradient :", gradient)
print("Hessienne :", hessian)
```

## Descentes de gradient à pas fixe ou à pas optimal

Dans un premier temps, on implémente la descente de gradient à pas fixe :

```python
from src.gradient import gradient
```

### Test

On teste la descente de gradient à pas fixe sur un problème trivial : $x^2$.

```python
# oracle test : la fonction carré en 1 dimension
oracle_test = lambda x: (x**2, np.array(x*2), None)
print("Descente de gradient à pas fixe :")
result_pas_fixe = gradient(oracle_test, np.array([2]), iter_max=20, default_gradient_step=0.1, threshold=1e-16, use_wolfe=False, verbose=True)
```

On constate une convergence lente pour un problème simple. Une solution est alors d'augmenter le pas du gradient, mais ce n'est pas suffisant pour un problème plus complexe. On utilise alors l'algorithme de Wolfe pour trouver le pas optimal.

```python
print("Descente de gradient à pas optimal :")
result_pas_opti = gradient(oracle_test, np.array([2]), iter_max=50, threshold=1e-16, verbose=True, visual=False)
```

Comme on pouvait s'y attendre, sur un problème convexe à une dimension la solution optimale est trouvée immédiatement.

### Utilisation sur le problème

On peut utiliser ces algorithmes sur notre problème :

```python
print("PAS FIXE :")
result = gradient(oracle, x0, default_gradient_step=0.0001, use_wolfe=False)
```

```python
print("PAS OPTIMAL :")
result = gradient(oracle, x0)
```

## Gradient conjugué

On implémente la méthode de gradient conjugué non linéaire avec l'algorithme de Polak-Ribière.

```python
from src.gradient_conjug_polak import gradient_polak
```

```python
result_polak = gradient_polak(oracle, x0)
```

## Méthode de Newton

On implémente la méthode de Newton.

```python
from src.newton import newton
```

```python
result_newton = newton(oracle, x0)
```

## Méthode quasi-Newton (BFGS)

On implémente la méthode dite "quasi-Newton" BFGS, qui utilise une approximation de l'inverse du hessien.

```python
from src.bfgs import bfgs
```

```python
result_bfgs = bfgs(oracle, x0)
```

# Problème dual

## Résultats théoriques

*TODO*

- Ecrire le lagrangien associé au problème sous contraintes (14) et ses conditions d’optimalité ;
- Vérifier que ces conditions correspondent aux équations (6) et (8) de l’équilibre du réseau.

- Constater que la minimisation en $q$ du lagrangien se fait de manière explicite ;
- Donner l’expression de l’argmin $q^♯$ en fonction du multiplicateur dual $\lambda$ et calculer les expressions de la fonction duale $\Phi$, de son gradient et de son hessien en fonction de $\lambda$.

Le Lagrangien du problème $(14)$ s'écrit:

$\mathbb{L}(q, \lambda) = \frac{1}{3} <q, r\bullet q\bullet | q|> + <p_r, A_rq> + \lambda^T(A_dq - f_d)$

On cherche dans un premier temps à le minimiser (à $\lambda$ fixé), c'est à dire à trouver les points d'annulation de son gradient. Cette condition s'écrit $r\bullet q \bullet |q| + A_r^T p_r + A_d^T \lambda = 0$ (on retrouve l'équation $(8)$ de l'énoncé)

L'extrémum de $\mathbb{L}$ est donc atteint en $\hat{q}(\lambda) = sign(X)\sqrt{|X|}$ où $X = -\frac{A^{T}_r p_r + A^{T}_d \lambda}{r}$

On veut maintenant maximiser par rapport à $\lambda$ le minimum que nous venons de calculer. Pour celà nous calculons son gradient: on note $H(\lambda) = \mathbb{L}(\hat{q}(\lambda),\lambda)$, et on a $\nabla H(\lambda) = A_d\hat{q}(\lambda) - f_d$.
La condition d'optimalité est donc $A_d\hat{q}(\lambda) - f_d = 0$, dans laquelle on retrouve l'équation $(6)$ de l'énoncé.

On dispose comme on l'a vu d'une expression explicite pour $\hat{q}(\lambda)$ minimisant le lagrangien à $\lambda$ fixé.
Pour résoudre le problème de maximisation (ou son opposé problème de minimisation) du minimum du lagrangien en fonction de $\lambda$ par les méthodes précédentes, il nous faut connaître le gradient et la hessienne de $H(\lambda)$.

Le gradient a déjà été présenté: $\nabla H(\lambda) = A_d\hat{q}(\lambda) - f_d$

La Hessienne est, quant à elle, donnée par $\nabla \hat{q}(\lambda) = A_d\quad D(\lambda) \quad A_d^T$, où $D(\lambda)=-\frac{1}{r\bullet \sqrt{|X|}}\bullet I_d$ où $I_d$ est la matrice identité de taille $n$.

Une fois ces deux expressions établies il suffit alors d'appliquer les algorithmes d'optimisation précédents à l'opposé du problème dual (on prend l'opposé pour passer d'un problème de maximisation à un problème de minimisation).

## Oracle

On implémente l'oracle avec le Lagrangien :

```python
from src.oracle_lagrange import oracle as oracle_dual
```

```python
u0 = np.random.normal(size=datas_r.md)
```

## Résolution

On résoud le problème dual grâce aux algorithmes précédents :

```python
print("PAS FIXE :")
result = gradient(oracle_dual, u0, default_gradient_step=0.2, use_wolfe=False, iter_max=10000)
```

```python
print("PAS OPTIMAL :")
result = gradient(oracle_dual, u0, threshold=0.01, iter_max=5000)
```

```python
result_polak = gradient_polak(oracle_dual, u0)
```

```python
result_newton = newton(oracle_dual, u0)
```

```python
result_bfgs = bfgs(oracle_dual, u0)
```

```python

```


ST0256 - Análisis Numérico

Capítulo 1: Error y Representación de números en el computador

2021/01

MEDELLÍN - COLOMBIA

 Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license.(c) Carlos Alberto Alvarez Henao

***

***Docente:*** Carlos Alberto Álvarez Henao, I.C. D.Sc.

***e-mail:*** calvar52@eafit.edu.co

***skype:*** carlos.alberto.alvarez.henao

***Herramienta:*** [Jupyter](http://jupyter.org/)

***Kernel:*** Python 3.8

***

Table of Contents

1  Fuentes de error

1.1  Error en el modelo y los datos

1.2  Error de truncamiento

1.3  Error de  representación de punto fotante

1.4  Definiciones básicas

2  Notación $\text{Big}-\mathcal{O}$

2.1  Reglas para el error de propagación basado en la notación $\text{Big}-\mathcal{O}$

3  Error de truncamiento

3.1  Laboratorio Numérico

4  Error de punto flotante

4.1  Sistemas de punto flotante

4.2  Propiedades de los sistemas de punto flotante

4.3  Sistema Binario

4.3.1  Numero y distribución de números**

4.4  Sistema real - IEEE 754 sistema binario de punto flotante

4.4.1  Precisión simple

4.4.2  Precisión doble

4.5  Acceso de Python a números de la IEEE

5  Por qué debería importarnos esto?

5.1  Ejemplo 1: Aritmética simple

5.2  Ejemplo 2: Cancelación catastrófica

5.3  Ejemplo 3: Evaluación de una función

5.4  Ejemplo 4: Evaluación de un Polinomio

5.5  Ejemplo 5: Evaluación de una función racional

6  Combinación de error

6.1  Ejemplo 1:

6.2  Ejemplo 2:

6.3  Ejemplo 3: Error relativo

7  Operaciones de conteo

7.1  Ejemplo 1: Multiplicación matriz - vector

7.2  Ejemplo 2: Método de Horner para evaluar polinomios

7.3  Algoritmo

***Comentario:*** este capítulo está basado en parte de las  notas del curso del profesor [Kyle T. Mandli](https://github.com/mandli/intro-numerical-methods) (en inglés)

 Fuente: Ariane 5 Explosion

## Fuentes de error

Los cálculos numéricos, que involucran el uso de máquinas (análogas o digitales) presentan una serie de errores que provienen de diferentes fuentes:

- del Modelo

- de los datos

- de truncamiento

- de representación de los números (punto flotante)

- $ \ldots$

***Meta:*** Categorizar y entender cada tipo de error y explorar algunas aproximaciones simples para analizarlas.

[Volver a la Tabla de Contenido](#TOC)

### Error en el modelo y los datos

Errores en la formulación fundamental

- Error en los datos: imprecisiones en las mediciones o incertezas en los parámetros

Infortunadamente no tenemos control de los errores en los datos y el modelo de forma directa pero podemos usar métodos que pueden ser más robustos en la presencia de estos tipos de errores.

[Volver a la Tabla de Contenido](#TOC)

### Error de truncamiento

Los errores surgen de la expansión de funciones con una función simple, por ejemplo, $sin(x) \approx x$ para $|x|\approx0$.

[Volver a la Tabla de Contenido](#TOC)

### Error de  representación de punto fotante

Los errores surgen de aproximar números reales con la representación  en precisión finita de números en el computador.

[Volver a la Tabla de Contenido](#TOC)

### Definiciones básicas

Dado un valor verdadero de una función $f$ y una solución aproximada $F$, se define:

- Error absoluto

$$e_a=|f-F|$$

- Error relativo

$$e_r = \frac{e_a}{|f|}=\frac{|f-F|}{|f|}$$

[Volver a la Tabla de Contenido](#TOC)

## Notación $\text{Big}-\mathcal{O}$

sea $$f(x)= \mathcal{O}(g(x)) \text{ cuando } x \rightarrow a$$

si y solo si

$$|f(x)|\leq M|g(x)| \text{ cuando } |x-a| < \delta \text{ donde } M, a > 0$$

En la práctica, usamos la notación $\text{Big}-\mathcal{O}$ para decir algo sobre cómo se pueden comportar los términos que podemos haber dejado fuera de una serie. Veamos el siguiente ejemplo de la aproximación de la serie de Taylor:

***Ejemplo:***

sea $f(x) = \sin x$ con $x_0 = 0$ entonces

$$T_N(x) = \sum^N_{n=0} (-1)^{n} \frac{x^{2n+1}}{(2n+1)!}$$

Podemos escribir $f(x)$ como

$$f(x) = x - \frac{x^3}{6} + \frac{x^5}{120} + \mathcal{O}(x^7)$$

Esto se vuelve más útil cuando lo vemos como lo hicimos antes con $\Delta x$:

$$f(x) = \Delta x - \frac{\Delta x^3}{6} + \frac{\Delta x^5}{120} + \mathcal{O}(\Delta x^7)$$

[Volver a la Tabla de Contenido](#TOC)

### Reglas para el error de propagación basado en la notación $\text{Big}-\mathcal{O}$

En general, existen dos teoremas que no necesitan prueba y se mantienen cuando el valor de $x$ es grande:

Sea

$$\begin{aligned}
    f(x) &= p(x) + \mathcal{O}(x^n) \\
    g(x) &= q(x) + \mathcal{O}(x^m) \\
    k &= \max(n, m)
\end{aligned}$$

Entonces

$$
    f+g = p + q + \mathcal{O}(x^k)
$$

y

\begin{align}
    f \cdot g &= p \cdot q + p \mathcal{O}(x^m) + q \mathcal{O}(x^n) + O(x^{n + m}) \\
    &= p \cdot q + \mathcal{O}(x^{n+m})
\end{align}

De otra forma, si estamos interesados en valores pequeños de $x$, $\Delta x$, la expresión puede ser modificada como sigue:

\begin{align}
    f(\Delta x) &= p(\Delta x) + \mathcal{O}(\Delta x^n) \\
    g(\Delta x) &= q(\Delta x) + \mathcal{O}(\Delta x^m) \\
    r &= \min(n, m)
\end{align}

entonces

$$
    f+g = p + q + O(\Delta x^r)
$$

y

\begin{align}
    f \cdot g &= p \cdot q + p \cdot \mathcal{O}(\Delta x^m) + q \cdot \mathcal{O}(\Delta x^n) + \mathcal{O}(\Delta x^{n+m}) \\
    &= p \cdot q + \mathcal{O}(\Delta x^r)
\end{align}

***Nota:*** En este caso, supongamos que al menos el polinomio con $k=max(n,m)$ tiene la siguiente forma:

$$
    p(\Delta x) = 1 + p_1 \Delta x + p_2 \Delta x^2 + \ldots
$$

o

$$
    q(\Delta x) = 1 + q_1 \Delta x + q_2 \Delta x^2 + \ldots
$$

para que $\mathcal{O}(1)$

de modo que hay un término $\mathcal{O}(1)$ que garantiza la existencia de $\mathcal{O}(\Delta x^r)$ en el producto final.

Para tener una idea de por qué importa más la potencia en $\Delta x$ al considerar la convergencia, la siguiente figura muestra cómo las diferentes potencias en la tasa de convergencia pueden afectar la rapidez con la que converge nuestra solución. Tenga en cuenta que aquí estamos dibujando los mismos datos de dos maneras diferentes. Graficar el error como una función de $\Delta x$ es una forma común de mostrar que un método numérico está haciendo lo que esperamos y muestra el comportamiento de convergencia correcto. Dado que los errores pueden reducirse rápidamente, es muy común trazar este tipo de gráficos en una escala log-log para visualizar fácilmente los resultados. Tenga en cuenta que si un método fuera realmente del orden $n$, será una función lineal en el espacio log-log con pendiente $n$.

```python
import numpy as np
import matplotlib.pyplot as plt
```

```python
dx = np.linspace(1.0, 1e-4, 100)

fig = plt.figure()
fig.set_figwidth(fig.get_figwidth() * 2.0)
axes = []
axes.append(fig.add_subplot(1, 2, 1))
axes.append(fig.add_subplot(1, 2, 2))

for n in range(1, 5):
    axes[0].plot(dx, dx**n, label="$\Delta x^%s$" % n)
    axes[1].loglog(dx, dx**n, label="$\Delta x^%s$" % n)

axes[0].legend(loc=2)
axes[1].set_xticks([10.0**(-n) for n in range(5)])
axes[1].set_yticks([10.0**(-n) for n in range(16)])
axes[1].legend(loc=4)
for n in range(2):
    axes[n].set_title("Crecimiento del Error vs. $\Delta x^n$")
    axes[n].set_xlabel("$\Delta x$")
    axes[n].set_ylabel("Error Estimado")
    axes[n].set_title("Crecimiento de las diferencias")
    axes[n].set_xlabel("$\Delta x$")
    axes[n].set_ylabel("Error Estimado")

plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

## Error de truncamiento

***Teorema de Taylor:*** Sea $f(x) \in C^{m+1}[a,b]$ y $x_0 \in [a,b]$, para todo $x \in (a,b)$ existe un número $c = c(x)$ que se encuentra entre $x_0$ y $x$ tal que

$$ f(x) = T_N(x) + R_N(x)$$

donde $T_N(x)$ es la aproximación del polinomio de Taylor

$$T_N(x) = \sum^N_{n=0} \frac{f^{(n)}(x_0)\times(x-x_0)^n}{n!}$$

y $R_N(x)$ es el residuo (la parte de la serie que obviamos)

$$R_N(x) = \frac{f^{(n+1)}(c) \times (x - x_0)^{n+1}}{(n+1)!}$$

Otra forma de pensar acerca de estos resultados consiste en reemplazar $x - x_0$ con $\Delta x$. La idea principal es que el residuo $R_N(x)$ se vuelve mas pequeño cuando $\Delta x \rightarrow 0$.

$$T_N(x) = \sum^N_{n=0} \frac{f^{(n)}(x_0)\times \Delta x^n}{n!}$$

y $R_N(x)$ es el residuo (la parte de la serie que obviamos)

$$ R_N(x) = \frac{f^{(n+1)}(c) \times \Delta x^{n+1}}{(n+1)!} \leq M \Delta x^{n+1}$$

***Ejemplo 1:***

$f(x) = e^x$ con $x_0 = 0$

Usando esto podemos encontrar expresiones para el error relativo y absoluto en función de $x$ asumiendo $N=2$.

Derivadas:
$$\begin{aligned}
    f'(x) &= e^x \\
    f''(x) &= e^x \\
    f^{(n)}(x) &= e^x
\end{aligned}$$

Polinomio de Taylor:
$$\begin{aligned}
    T_N(x) &= \sum^N_{n=0} e^0 \frac{x^n}{n!} \Rightarrow \\
    T_2(x) &= 1 + x + \frac{x^2}{2}
\end{aligned}$$

Restos:
$$\begin{aligned}
    R_N(x) &= e^c \frac{x^{n+1}}{(n+1)!} = e^c \times \frac{x^3}{6} \quad \Rightarrow \\
    R_2(x) &\leq \frac{e^1}{6} \approx 0.5
\end{aligned}$$

Precisión:
$$
    e^1 = 2.718\ldots \\
    T_2(1) = 2.5 \Rightarrow e \approx 0.2 ~~ r \approx 0.1
$$

¡También podemos usar el paquete `sympy` que tiene la capacidad de calcular el polinomio de *Taylor* integrado!

```python
import sympy
x = sympy.symbols('x')
f = sympy.symbols('f', cls=sympy.Function)

f = sympy.exp(x)
f.series(x0=0, n=5)
```

$\displaystyle 1 + x + \frac{x^{2}}{2} + \frac{x^{3}}{6} + \frac{x^{4}}{24} + O\left(x^{5}\right)$

Graficando

```python
x = np.linspace(-1, 1, 100)
T_N = 1.0 + x + x**2 / 2.0
R_N = np.exp(1) * x**3 / 6.0

plt.plot(x, T_N, 'r', x, np.exp(x), 'k', x, R_N, 'b')
plt.plot(0.0, 1.0, 'o', markersize=10)
plt.grid(True)
plt.xlabel("x")
plt.ylabel("$f(x)$, $T_N(x)$, $R_N(x)$")
plt.legend(["$T_N(x)$", "$f(x)$", "$R_N(x)$"], loc=2)
plt.show()
```

***Ejemplo 2:***

Aproximar

$$ f(x) = \frac{1}{x} \quad x_0  = 1,$$

usando $x_0 = 1$ para el tercer termino de la serie de Taylor.

$$\begin{aligned}
    f'(x) &= -\frac{1}{x^2} \\
    f''(x) &= \frac{2}{x^3} \\
    f^{(n)}(x) &= \frac{(-1)^n n!}{x^{n+1}}
\end{aligned}$$

$$\begin{aligned}
    T_N(x) &= \sum^N_{n=0} (-1)^n (x-1)^n \Rightarrow \\
    T_2(x) &= 1 - (x - 1) + (x - 1)^2
\end{aligned}$$

$$\begin{aligned}
    R_N(x) &= \frac{(-1)^{n+1}(x - 1)^{n+1}}{c^{n+2}} \Rightarrow \\
    R_2(x) &= \frac{-(x - 1)^{3}}{c^{4}}
\end{aligned}$$

```python
x = np.linspace(0.8, 2, 100)
T_N = 1.0 - (x-1) + (x-1)**2
R_N = -(x-1.0)**3 / (1.1**4)

plt.plot(x, T_N, 'r', x, 1.0 / x, 'k', x, R_N, 'b')
plt.plot(1.0, 1.0, 'o', markersize=10)
plt.grid(True)
plt.xlabel("x")
plt.ylabel("$f(x)$, $T_N(x)$, $R_N(x)$")

plt.legend(["$T_N(x)$", "$f(x)$", "$R_N(x)$"], loc=8)
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Laboratorio Numérico

- Realice la expansión de la serie de Taylor para los dos ejemplos anteriores con 3, 4 y 5 términos. Cuál es el error que se tiene en la medida que se adicionan los términos? Realice una gráfica comparativa del Residuo que se obtiene para cada término adicional. Haga un análisis de lo que sucede. Si extendemos hasta el "infinito" dicho residuo qué pueden concluir?

## Error de punto flotante

Errores surgen de aproximar números reales con números de precisión finita

$$\pi \approx 3.14$$

o $\frac{1}{3} \approx 0.333333333$ en decimal, los resultados forman un número finito de registros para representar cada número.

[Volver a la Tabla de Contenido](#TOC)

### Sistemas de punto flotante

Los números en sistemas de punto flotante se representan como una serie de bits que representan diferentes partes de un número. En los sistemas de punto flotante normalizados, existen algunas convenciones estándar para el uso de estos bits. En general, los números se almacenan dividiéndolos en la forma

$$F = \pm d_1 . d_2 d_3 d_4 \ldots d_p \times \beta^E$$

donde

1. $\pm$ es un bit único y representa el signo del número.

2. $d_1 . d_2 d_3 d_4 \ldots d_p$ es la *mantisa*. observe que, técnicamente, el decimal se puede mover, pero en general, utilizando la notación científica, el decimal siempre se puede colocar en esta ubicación. Los digitos $d_2 d_3 d_4 \ldots d_p$ son llamados la *fracción* con $p$ digitos de precisión. Los sistemas normalizados específicamente ponen el punto decimal en el frente y asume $d_1 \neq 0$ a menos que el número sea exactamente $0$.

3. $\beta$ es la *base*. Para el sistema binario $\beta = 2$, para decimal $\beta = 10$, etc.

4. $E$ es el *exponente*, un entero en el rango $[E_{\min}, E_{\max}]$

Los puntos importantes en cualquier sistema de punto flotante es

1. Existe un conjunto discreto y finito de números representables.

2. Estos números representables no están distribuidos uniformemente en la línea real

3. La aritmética en sistemas de punto flotante produce resultados diferentes de la aritmética de precisión infinita (es decir, matemática "real")

[Volver a la Tabla de Contenido](#TOC)

### Propiedades de los sistemas de punto flotante

Todos los sistemas de punto flotante se caracterizan por varios números importantes

- Número normalizado reducido (underflow si está por debajo, relacionado con números sub-normales alrededor de cero)

- Número normalizado más grande (overflow)

- Cero

- $\epsilon$ o $\epsilon_{mach}$

- `Inf` y `nan`

***Ejemplo: Sistema de juguete***

Considere el sistema decimal de 2 digitos de  precisión (normalizado)

$$f = \pm d_1 . d_2 \times 10^E$$

con $E \in [-2, 0]$.

**Numero y distribución de números**

1. Cuántos números pueden representarse con este sistema?

2. Cuál es la distribución en la línea real?

3. Cuáles son los límites underflow y overflow?

Cuántos números pueden representarse con este sistema?

$$f = \pm d_1 . d_2 \times 10^E ~~~ \text{with} E \in [-2, 0]$$

$$2 \times 9 \times 10 \times 3 + 1 = 541$$

Cuál es la distribución en la línea real?

```python
d_1_values = [1, 2, 3, 4, 5, 6, 7, 8, 9]
d_2_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
E_values = [0, -1, -2]

fig = plt.figure(figsize=(10.0, 1.0))
axes = fig.add_subplot(1, 1, 1)

for E in E_values:
    for d1 in d_1_values:
        for d2 in d_2_values:
            axes.plot( (d1 + d2 * 0.1) * 10**E, 0.0, 'r+', markersize=20)
            axes.plot(-(d1 + d2 * 0.1) * 10**E, 0.0, 'r+', markersize=20)

axes.plot(0.0, 0.0, '+', markersize=20)
axes.plot([-10.0, 10.0], [0.0, 0.0], 'k')

axes.set_title("Distribución de Valores")
axes.set_yticks([])
axes.set_xlabel("x")
axes.set_ylabel("")
axes.set_xlim([-0.1, 0.1])
plt.show()
```

Cuáles son los límites superior (overflow) e inferior (underflow)?

- El menor número que puede ser representado (underflow) es: $1.0 \times 10^{-2} = 0.01$

- El mayor número que puede ser representado (overflow) es:  $9.9 \times 10^0 = 9.9$

[Volver a la Tabla de Contenido](#TOC)

### Sistema Binario

Considere el sistema en base 2 de 2 dígitos de precisión

$$f=\pm d_1 . d_2 \times 2^E \quad \text{with} \quad E \in [-1, 1]$$

[Volver a la Tabla de Contenido](#TOC)

#### Numero y distribución de números**

1. Cuántos números pueden representarse con este sistema?

2. Cuál es la distribución en la línea real?

3. Cuáles son los límites underflow y overflow?

Cuántos números pueden representarse en este sistema?

$$f=\pm d_1 . d_2 \times 2^E ~~~~ \text{con} ~~~~ E \in [-1, 1]$$

$$ 2 \times 1 \times 2 \times 3 + 1 = 13$$

Cuál es la distribución en la línea real?

```python
d_1_values = [1]
d_2_values = [0, 1]
E_values = [1, 0, -1]

fig = plt.figure(figsize=(10.0, 1.0))
axes = fig.add_subplot(1, 1, 1)

for E in E_values:
    for d1 in d_1_values:
        for d2 in d_2_values:
            axes.plot( (d1 + d2 * 0.5) * 2**E, 0.0, 'r+', markersize=20)
            axes.plot(-(d1 + d2 * 0.5) * 2**E, 0.0, 'r+', markersize=20)

axes.plot(0.0, 0.0, 'r+', markersize=20)
axes.plot([-4.5, 4.5], [0.0, 0.0], 'k')

axes.set_title("Distribución de Valores")
axes.set_yticks([])
axes.set_xlabel("x")
axes.set_ylabel("")
axes.set_xlim([-3.5, 3.5])
plt.show()
```

Cuáles son los límites superior (*overflow*) e inferior (*underflow*)?

- El menor número que puede ser representado (*underflow*) es: $1.0 \times 2^{-1} = 0.5$

- El mayor número que puede ser representado (*overflow*) es:  $1.1 \times 2^1 = 3$

Observe que estos números son en sistema binario.

Una rápida regla de oro:

$$2^3 2^2 2^1 2^0 . 2^{-1} 2^{-2} 2^{-3}$$

corresponde a

8s, 4s, 2s, 1s . mitades, cuartos, octavos, $\ldots$

[Volver a la Tabla de Contenido](#TOC)

### Sistema real - [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) sistema binario de punto flotante

#### Precisión simple

- Almacenamiento total es de 32 bits

- Exponente de 8 bits $\Rightarrow E \in [-126, 127]$

- Fracción 23 bits ($p = 24$)

```
s EEEEEEEE FFFFFFFFFFFFFFFFFFFFFFF
0 1      8 9                     31
```

Overflow $= 2^{127} \approx 3.4 \times 10^{38}$

Underflow $= 2^{-126} \approx 1.2 \times 10^{-38}$

$\epsilon_{\text{machine}} = 2^{-23} \approx 1.2 \times 10^{-7}$

[Volver a la Tabla de Contenido](#TOC)

#### Precisión doble

- Almacenamiento total asignado es 64 bits

- Exponenete de 11 bits $\Rightarrow E \in [-1022, 1024]$

- Fracción de 52 bits ($p = 53$)

```
s EEEEEEEEEE FFFFFFFFFF FFFFFFFFFF FFFFFFFFFF FFFFFFFFFF FFFFFFFFFF FF
0 1       11 12                                                      63
```
Overflow $= 2^{1024} \approx 1.8 \times 10^{308}$

Underflow $= 2^{-1022} \approx 2.2 \times 10^{-308}$

$\epsilon_{\text{machine}} = 2^{-52} \approx 2.2 \times 10^{-16}$

[Volver a la Tabla de Contenido](#TOC)

### Acceso de Python a números de la IEEE

Accede a muchos parámetros importantes, como el epsilon de la máquina

```python
import numpy
numpy.finfo(float).eps
```

```python
import numpy
numpy.finfo(float).eps

print(numpy.finfo(numpy.float16))
print(numpy.finfo(numpy.float32))
print(numpy.finfo(float))
print(numpy.finfo(numpy.float128))
```

[Volver a la Tabla de Contenido](#TOC)

## Por qué debería importarnos esto?

- Aritmética de punto flotante no es conmutativa o asociativa

- Errores de punto flotante compuestos, No asuma que la precisión doble es suficiente

- Mezclar precisión es muy peligroso

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 1: Aritmética simple

Aritmética simple $\delta < \epsilon_{\text{machine}}$

   $$(1+\delta) - 1 = 1 - 1 = 0$$

   $$1 - 1 + \delta = \delta$$

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 2: Cancelación catastrófica

Miremos qué sucede cuando sumamos dos números $x$ y $y$ cuando $x+y \neq 0$. De hecho, podemos estimar estos límites haciendo un análisis de error. Aquí necesitamos presentar la idea de que cada operación de punto flotante introduce un error tal que

$$
    \text{fl}(x ~\text{op}~ y) = (x ~\text{op}~ y) (1 + \delta)
$$

donde $\text{fl}(\cdot)$ es una función que devuelve la representación de punto flotante de la expresión encerrada, $\text{op}$ es alguna operación (ex. $+, -, \times, /$), y $\delta$ es el error de punto flotante debido a  $\text{op}$.

De vuelta a nuestro problema en cuestión. El error de coma flotante debido a la suma es

$$\text{fl}(x + y) = (x + y) (1 + \delta).$$

Comparando esto con la solución verdadera usando un error relativo tenemos

$$\begin{aligned}
    \frac{(x + y) - \text{fl}(x + y)}{x + y} &= \frac{(x + y) - (x + y) (1 + \delta)}{x + y} = \delta.
\end{aligned}$$

entonces si $\delta = \mathcal{O}(\epsilon_{\text{machine}})$ no estaremos muy preocupados.

Que pasa si consideramos un error de punto flotante en la representación de $x$ y $y$, $x \neq y$, y decimos que $\delta_x$ y $\delta_y$ son la magnitud de los errores en su representación. Asumiremos que esto constituye el error de punto flotante en lugar de estar asociado con la operación en sí.

Dado todo esto, tendríamos

$$\begin{aligned}
    \text{fl}(x + y) &= x (1 + \delta_x) + y (1 + \delta_y) \\
    &= x + y + x \delta_x + y \delta_y \\
    &= (x + y) \left(1 + \frac{x \delta_x + y \delta_y}{x + y}\right)
\end{aligned}$$

Calculando nuevamente el error relativo, tendremos

$$\begin{aligned}
    \frac{x + y - (x + y) \left(1 + \frac{x \delta_x + y \delta_y}{x + y}\right)}{x + y} &= 1 - \left(1 + \frac{x \delta_x + y \delta_y}{x + y}\right) \\
    &= \frac{x}{x + y} \delta_x + \frac{y}{x + y} \delta_y \\
    &= \frac{1}{x + y} (x \delta_x + y \delta_y)
\end{aligned}$$

Lo importante aquí es que ahora el error depende de los valores de $x$ y $y$, y más importante aún, su suma. De particular preocupación es el tamaño relativo de $x + y$. A medida que se acerca a cero en relación con las magnitudes de $x$ y $y$, el error podría ser arbitrariamente grande. Esto se conoce como ***cancelación catastrófica***.

```python
dx = numpy.array([10**(-n) for n in range(1, 16)])
x = 1.0 + dx
y = -numpy.ones(x.shape)
error = numpy.abs(x + y - dx) / (dx)

fig = plt.figure()
fig.set_figwidth(fig.get_figwidth() * 2)

axes = fig.add_subplot(1, 2, 1)
axes.loglog(dx, x + y, 'o-')
axes.set_xlabel("$\Delta x$")
axes.set_ylabel("$x + y$")
axes.set_title("$\Delta x$ vs. $x+y$")

axes = fig.add_subplot(1, 2, 2)
axes.loglog(dx, error, 'o-')
axes.set_xlabel("$\Delta x$")
axes.set_ylabel("$|x + y - \Delta x| / \Delta x$")
axes.set_title("Diferencia entre $x$ y $y$ vs. Error relativo")

plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 3: Evaluación de una función

Considere la función

$$
    f(x) = \frac{1 - \cos x}{x^2}
$$

con $x\in[-10^{-4}, 10^{-4}]$.

Tomando el límite cuando $x \rightarrow 0$ podemos ver qué comportamiento esperaríamos ver al evaluar esta función:

$$
    \lim_{x \rightarrow 0} \frac{1 - \cos x}{x^2} = \lim_{x \rightarrow 0} \frac{\sin x}{2 x} = \lim_{x \rightarrow 0} \frac{\cos x}{2} = \frac{1}{2}.
$$

¿Qué hace la representación de punto flotante?

```python
x = numpy.linspace(-1e-3, 1e-3, 100, dtype=numpy.float32)
error = (0.5 - (1.0 - numpy.cos(x)) / x**2) / 0.5

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, error, 'o')
axes.set_xlabel("x")
axes.set_ylabel("Error Relativo")
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 4: Evaluación de un Polinomio

 $$f(x) = x^7 - 7x^6 + 21 x^5 - 35 x^4 + 35x^3-21x^2 + 7x - 1$$

```python
x = numpy.linspace(0.988, 1.012, 1000, dtype=numpy.float16)
y = x**7 - 7.0 * x**6 + 21.0 * x**5 - 35.0 * x**4 + 35.0 * x**3 - 21.0 * x**2 + 7.0 * x - 1.0

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, y, 'r')
axes.set_xlabel("x")
axes.set_ylabel("y")
axes.set_ylim((-0.1, 0.1))
axes.set_xlim((x[0], x[-1]))
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 5: Evaluación de una función racional

Calcule $f(x) = x + 1$ por la función $$F(x) = \frac{x^2 - 1}{x - 1}$$

¿Cuál comportamiento esperarías encontrar?

```python
x = numpy.linspace(0.5, 1.5, 101, dtype=numpy.float16)
f_hat = (x**2 - 1.0) / (x - 1.0)

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, numpy.abs(f_hat - (x + 1.0)))
axes.set_xlabel("$x$")
axes.set_ylabel("Error Absoluto")
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

## Combinación de error

En general, nos debemos ocupar de la combinación de error de truncamiento con el error de punto flotante.

- Error de Truncamiento: errores que surgen de la aproximación de una función, truncamiento de una serie.

$$\sin x \approx x - \frac{x^3}{3!} + \frac{x^5}{5!} + O(x^7)$$

- Error de punto flotante: errores derivados de la aproximación de números reales con números de precisión finita

$$\pi \approx 3.14$$

o $\frac{1}{3} \approx 0.333333333$ en decimal, los resultados forman un número finito de registros para representar cada número.

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 1:

Considere la aproximación de diferencias finitas donde $f(x) = e^x$ y estamos evaluando en $x=1$

$$f'(x) \approx \frac{f(x + \Delta x) - f(x)}{\Delta x}$$

Compare el error entre disminuir $\Delta x$ y la verdadera solucion $f'(1) = e$

```python
delta_x = numpy.linspace(1e-20, 5.0, 100)
delta_x = numpy.array([2.0**(-n) for n in range(1, 60)])
x = 1.0
f_hat_1 = (numpy.exp(x + delta_x) - numpy.exp(x)) / (delta_x)
f_hat_2 = (numpy.exp(x + delta_x) - numpy.exp(x - delta_x)) / (2.0 * delta_x)

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.loglog(delta_x, numpy.abs(f_hat_1 - numpy.exp(1)), 'o-', label="Unilateral")
axes.loglog(delta_x, numpy.abs(f_hat_2 - numpy.exp(1)), 's-', label="Centrado")
axes.legend(loc=3)
axes.set_xlabel("$\Delta x$")
axes.set_ylabel("Error Absoluto")
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 2:

Evalúe $e^x$ con la serie de *Taylor*

$$e^x = \sum^\infty_{n=0} \frac{x^n}{n!}$$

podemos elegir $n< \infty$ que puede aproximarse $e^x$ en un rango dado $x \in [a,b]$ tal que el error relativo $E$ satisfaga $E<8 \cdot \varepsilon_{\text{machine}}$?

¿Cuál podría ser una mejor manera de simplemente evaluar el polinomio de Taylor directamente por varios $N$?

```python
import scipy.special

def my_exp(x, N=10):
    value = 0.0
    for n in range(N + 1):
        value += x**n / scipy.special.factorial(n)

    return value

x = numpy.linspace(-2, 2, 100, dtype=numpy.float32)
for N in range(1, 50):
    error = numpy.abs((numpy.exp(x) - my_exp(x, N=N)) / numpy.exp(x))
    if numpy.all(error < 8.0 * numpy.finfo(float).eps):
        break

print(N)

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, error)
axes.set_xlabel("x")
axes.set_ylabel("Error Relativo")
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 3: Error relativo

Digamos que queremos calcular el error relativo de dos valores $x$ y $y$ usando $x$ como valor de normalización

$$
    E = \frac{x - y}{x}
$$
y
$$
    E = 1 - \frac{y}{x}
$$

son equivalentes. En precisión finita, ¿qué forma pidría esperarse que sea más precisa y por qué?

Ejemplo tomado de [blog](https://nickhigham.wordpress.com/2017/08/14/how-and-how-not-to-compute-a-relative-error/) posteado por Nick Higham*

Usando este modelo, la definición original contiene dos operaciones de punto flotante de manera que

$$\begin{aligned}
    E_1 = \text{fl}\left(\frac{x - y}{x}\right) &= \text{fl}(\text{fl}(x - y) / x) \\
    &= \left[ \frac{(x - y) (1 + \delta_+)}{x} \right ] (1 + \delta_/) \\
    &= \frac{x - y}{x}  (1 + \delta_+) (1 + \delta_/)
\end{aligned}$$

Para la otra formulación tenemos

$$\begin{aligned}
    E_2 = \text{fl}\left( 1 - \frac{y}{x} \right ) &= \text{fl}\left(1 - \text{fl}\left(\frac{y}{x}\right) \right) \\
    &= \left(1 - \frac{y}{x} (1 + \delta_/) \right) (1 + \delta_-)
\end{aligned}$$

Si suponemos que todos las $\text{op}$s tienen magnitudes de error similares, entonces podemos simplificar las cosas dejando que

$$
    |\delta_\ast| \le \epsilon.
$$

Para comparar las dos formulaciones, nuevamente usamos el error relativo entre el error relativo verdadero $e_i$ y nuestras versiones calculadas $E_i$

Definición original

$$\begin{aligned}
    \frac{e - E_1}{e} &= \frac{\frac{x - y}{x} - \frac{x - y}{x}  (1 + \delta_+) (1 + \delta_/)}{\frac{x - y}{x}} \\
    &\le 1 - (1 + \epsilon) (1 + \epsilon) = 2 \epsilon + \epsilon^2
\end{aligned}$$

Definición manipulada:

$$\begin{aligned}
    \frac{e - E_2}{e} &= \frac{e - \left[1 - \frac{y}{x}(1 + \delta_/) \right] (1 + \delta_-)}{e} \\
    &= \frac{e - \left[e - \frac{y}{x} \delta_/) \right] (1 + \delta_-)}{e} \\
    &= \frac{e - \left[e + e\delta_- - \frac{y}{x} \delta_/ - \frac{y}{x} \delta_/ \delta_-)) \right] }{e} \\
    &= - \delta_- + \frac{1}{e} \frac{y}{x} \left(\delta_/ + \delta_/ \delta_- \right) \\
    &= - \delta_- + \frac{1 -e}{e} \left(\delta_/ + \delta_/ \delta_- \right) \\
    &\le \epsilon + \left |\frac{1 - e}{e}\right | (\epsilon + \epsilon^2)
\end{aligned}$$

Vemos entonces que nuestro error de punto flotante dependerá de la magnitud relativa de $e$

```python
# Based on the code by Nick Higham
# https://gist.github.com/higham/6f2ce1cdde0aae83697bca8577d22a6e
# Compares relative error formulations using single precision and compared to double precision

N = 501    # Note: Use 501 instead of 500 to avoid the zero value
d = numpy.finfo(numpy.float32).eps * 1e4
a = 3.0
x = a * numpy.ones(N, dtype=numpy.float32)
y = [x[i] + numpy.multiply((i - numpy.divide(N, 2.0, dtype=numpy.float32)), d, dtype=numpy.float32) for i in range(N)]

# Compute errors and "true" error
relative_error = numpy.empty((2, N), dtype=numpy.float32)
relative_error[0, :] = numpy.abs(x - y) / x
relative_error[1, :] = numpy.abs(1.0 - y / x)
exact = numpy.abs( (numpy.float64(x) - numpy.float64(y)) / numpy.float64(x))

# Compute differences between error calculations
error = numpy.empty((2, N))
for i in range(2):
    error[i, :] = numpy.abs((relative_error[i, :] - exact) / numpy.abs(exact))

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.semilogy(y, error[0, :], '.', markersize=10, label="$|x-y|/|x|$")
axes.semilogy(y, error[1, :], '.', markersize=10, label="$|1-y/x|$")

axes.grid(True)
axes.set_xlabel("y")
axes.set_ylabel("Error Relativo")
axes.set_xlim((numpy.min(y), numpy.max(y)))
axes.set_ylim((5e-9, numpy.max(error[1, :])))
axes.set_title("Comparasión Error Relativo")
axes.legend()
plt.show()
```

Algunos enlaces de utilidad con respecto al punto flotante IEEE:

- [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)

- [IEEE 754 Floating Point Calculator](http://babbage.cs.qc.edu/courses/cs341/IEEE-754.html)

- [Numerical Computing with IEEE Floating Point Arithmetic](http://epubs.siam.org/doi/book/10.1137/1.9780898718072)

[Volver a la Tabla de Contenido](#TOC)

## Operaciones de conteo

- ***Error de truncamiento:*** *¿Por qué no usar más términos en la serie de Taylor?*

- ***Error de punto flotante:*** *¿Por qué no utilizar la mayor precisión posible?*

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 1: Multiplicación matriz - vector

Sea $A, B \in \mathbb{R}^{N \times N}$ y $x \in \mathbb{R}^N$.

1. Cuenta el número aproximado de operaciones que tomará para calcular $Ax$

2. Hacer lo mismo para $AB$

***Producto Matriz-vector:*** Definiendo $[A]_i$ como la $i$-ésima fila de $A$ y $A_{ij}$ como la $i$,$j$-ésima entrada entonces

$$
    A x = \sum^N_{i=1} [A]_i \cdot x = \sum^N_{i=1} \sum^N_{j=1} A_{ij} x_j
$$

Tomando un caso en particular, siendo $N=3$, entonces la operación de conteo es

$$
    A x = [A]_1 \cdot v + [A]_2 \cdot v + [A]_3 \cdot v = \begin{bmatrix}
        A_{11} \times v_1 + A_{12} \times v_2 + A_{13} \times v_3 \\
        A_{21} \times v_1 + A_{22} \times v_2 + A_{23} \times v_3 \\
        A_{31} \times v_1 + A_{32} \times v_2 + A_{33} \times v_3
    \end{bmatrix}
$$

Esto son 15 operaciones (6 sumas y 9 multiplicaciones)

Tomando otro caso, siendo $N=4$, entonces el conteo de operaciones es:

$$
    A x = [A]_1 \cdot v + [A]_2 \cdot v + [A]_3 \cdot v = \begin{bmatrix}
        A_{11} \times v_1 + A_{12} \times v_2 + A_{13} \times v_3 + A_{14} \times v_4 \\
        A_{21} \times v_1 + A_{22} \times v_2 + A_{23} \times v_3 + A_{24} \times v_4 \\
        A_{31} \times v_1 + A_{32} \times v_2 + A_{33} \times v_3 + A_{34} \times v_4 \\
        A_{41} \times v_1 + A_{42} \times v_2 + A_{43} \times v_3 + A_{44} \times v_4 \\
    \end{bmatrix}
$$

Esto lleva a 28 operaciones (12 sumas y 16 multiplicaciones).

Generalizando, hay $N^2$ mutiplicaciones y $N(N-1)$ sumas para un total de

$$
    \text{operaciones} = N (N - 1) + N^2 = \mathcal{O}(N^2).
$$

***Producto Matriz-Matriz ($AB$):*** Definiendo $[B]_j$ como la $j$-ésima columna de $B$ entonces

$$
    (A B)_{ij} = \sum^N_{i=1} \sum^N_{j=1} [A]_i \cdot [B]_j
$$

El producto interno de dos vectores es representado por

$$
    a \cdot b = \sum^N_{i=1} a_i b_i
$$

conduce a $\mathcal{O}(3N)$ operaciones. Como hay $N^2$ entradas en la matriz resultante, tendríamos $\mathcal{O}(N^3)$ operaciones

Existen métodos para realizar la multiplicación matriz - matriz más rápido. En la siguiente figura vemos una colección de algoritmos a lo largo del tiempo que han podido limitar el número de operaciones en ciertas circunstancias
$$
    \mathcal{O}(N^\omega)
$$

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 2: Método de Horner para evaluar polinomios

Dado

$$P_N(x) = a_0 + a_1 x + a_2 x^2 + \ldots + a_N x^N$$

o

$$P_N(x) = p_1 x^N + p_2 x^{N-1} + p_3 x^{N-2} + \ldots + p_{N+1}$$

queremos encontrar la mejor vía para evaluar $P_N(x)$

Primero considere dos vías para escribir $P_3$

$$ P_3(x) = p_1 x^3 + p_2 x^2 + p_3 x + p_4$$

y usando multiplicación anidada

$$ P_3(x) = ((p_1 x + p_2) x + p_3) x + p_4$$

Considere cuántas operaciones se necesitan para cada...

$$ P_3(x) = p_1 x^3 + p_2 x^2 + p_3 x + p_4$$

$$P_3(x) = \overbrace{p_1 \cdot x \cdot x \cdot x}^3 + \overbrace{p_2 \cdot x \cdot x}^2 + \overbrace{p_3 \cdot x}^1 + p_4$$

Sumando todas las operaciones, en general podemos pensar en esto como una pirámide

podemos estimar de esta manera que el algoritmo escrito de esta manera tomará aproximadamente $\mathcal{O}(N^2/2)$ operaciones para completar.

Mirando nuetros otros medios de evaluación

$$ P_3(x) = ((p_1 x + p_2) x + p_3) x + p_4$$

Aquí encontramos que el método es $\mathcal{O}(N)$ (el 2 generalmente se ignora en estos casos). Lo importante es que la primera evaluación es $\mathcal{O}(N^2)$ y la segunda $\mathcal{O}(N)$!

[Volver a la Tabla de Contenido](#TOC)

### Algoritmo

Complete la función e implemente el método de *Horner*

```python
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x should be a float.
    """
    pass
```

```python
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x should be a float.
    """
    ### ADD CODE HERE
    pass
```

```python
# Scalar version
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x should be a float.
    """

    y = p[0]
    for coefficient in p[1:]:
        y = y * x + coefficient

    return y

# Vectorized version
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x can by a NumPy ndarray.
    """

    y = numpy.ones(x.shape) * p[0]
    for coefficient in p[1:]:
        y = y * x + coefficient

    return y

p = [1, -3, 10, 4, 5, 5]
x = numpy.linspace(-10, 10, 100)
plt.plot(x, eval_poly(p, x))
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)


```python
import numpy as np
import sympy as sm
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.cm as cm
sm.init_printing(use_unicode=True)
```

```python
alpha = sm.symbols('alpha')
b = sm.symbols('b')
gamma = sm.symbols('gamma')
h = sm.symbols('h')
phi = sm.symbols('phi')
yt = sm.symbols('y_t')
pit = sm.symbols('pi_t')
pit_m1=sm.symbols('pi_t-1')
yt_m1 = sm.symbols('y_t-1')
vt = sm.symbols('v_t')
st = sm.symbols('s_t')
st_m1 = sm.symbols('s_t-1')
rt = sm.symbols('r_t')
it = sm.symbols('i_t')
exp_pit = sm.symbols('E[pi_t]')
exp_pit_p1 = sm.symbols('E[pi_t+1]')
```

Question 1

```python
SRAS_pit=pit_m1-phi*gamma*yt_m1+st-phi*st_m1+gamma*yt
SRAS_pit
```

```python
AD_pit=(1/(h*alpha))*(vt-(1+b*alpha)*yt)
AD_pit
```

```python
AS_minus_AD= SRAS_pit-AD_pit
AS_minus_AD
```

```python
eq_y = sm.solve(sm.Eq(AS_minus_AD,0),yt)
eq_y
```

```python
eq_pi= SRAS_pit.subs(yt,eq_y[0])
eq_pi
```

```python
#not necessary
eq_pi= SRAS_pit.subs(yt,eq_yt)
eq_pii= sm.solve(sm.Eq(eq_pi,pit),pit)
eq_pii
```

```python

```

Question 2

```python
pms = {}

pms['alpha']=5.76
pms['h'] = 0.5
pms['b'] = 0.5
pms['phi'] = 0
pms['gamma'] = 0.075
```

```python
equil_output= sm.lambdify((vt, st, pit_m1, st_m1, yt_m1, alpha, h, b, phi, gamma),eq_y[0])
def equili_output (vt, st, pit_m1, st_m1, yt_m1, alpha=pms['alpha'], h=pms['h'], b=pms['b'], phi=pms['phi'], gamma=pms['gamma']):
    return equil_output (vt, st, pit_m1, st_m1, yt_m1, alpha, h, b, phi, gamma)

```

```python
equili_output(0,0,0,0,0)

```

```python
equili_output(0.1,0,0,0,0)
```

```python
equil_pi= sm.lambdify((pit_m1, yt, yt_m1, st, st_m1, gamma, phi), SRAS_pit)
def equili_pi (pit_m1, yt, yt_m1, st, st_m1, gamma=pms['gamma'], phi=pms['phi']):
    return equil_pi (pit_m1, yt, yt_m1, st, st_m1, gamma, phi)
```

```python
equili_pi(0,0,0,0,0)
```

```python
equili_pi(0,0.0244140625,0,0,0)
```

Question 3

```python
pms["delta"] = 0.80
pms["omega"] = 0.15
delta = sm.symbols("delta")
omega = sm.symbols("omega")
vt_m1 = sm.symbols("v_t-1")
xt = sm.symbols("x_t")
ct = sm.symbols("c_t")

```

```python
vt_ar = delta*vt_m1 + xt
st_ar = omega*st_m1 + ct
```

```python
eq_y
```

```python
eq_y_ar = eq_y[0].subs(vt,vt_ar)
eq_y_ar
```

```python
eq_pi
```

```python
eq_pi_ar = eq_pi.subs(st,st_ar)
eq_pi_ar
```

```python
equil_piii= sm.lambdify((yt_m1, yt, pit_m1, st_m1, ct, omega, gamma, phi),eq_pi_ar)
def equili_pi_ar (yt_m1, yt, pit_m1, st_m1, ct, omega=pms['omega'], gamma=pms['gamma'], phi=pms['phi']):
    return equil_piii (yt_m1, yt, pit_m1, st_m1, ct, omega, gamma, phi)
```

```python
equili_pi_ar(0,0,0,0,0)
```

```python
equil_piii= sm.lambdify((yt_m1, yt, pit_m1, st_m1, ct, omega, gamma, phi),eq_pi_ar)
def equili_pi_ar (yt_m1, yt, pit_m1, st_m1, ct, omega=pms['omega'], gamma=pms['gamma'], phi=pms['phi']):
    return equil_piii (yt_m1, yt, pit_m1, st_m1, ct, omega, gamma, phi)
```

```python
#other approach
```

```python
#define shocks:
vt_ar = delta*vt_m1 + xt
st_ar = omega*st_m1 + ct
```

```python
#set number of periods
P= 1000
# all initial values are 0:
initial_y=
```

### Question 3

Consider an **exchange economy** with

1. 3 goods, $(x_1,x_2,x_3)$
2. $N$ consumers indexed by \\( j \in \{1,2,\dots,N\} \\)
3. Preferences are Cobb-Douglas with log-normally distributed coefficients

    $$ \begin{eqnarray*}
    u^{j}(x_{1},x_{2},x_{3}) &=&
    \left(x_{1}^{\beta_{1}^{j}}x_{2}^{\beta_{2}^{j}}x_{3}^{\beta_{3}^{j}}\right)^{\gamma}\\
     &  & \,\,\,\beta_{i}^{j}=\frac{\alpha_{i}^{j}}{\alpha_{1}^{j}+\alpha_{2}^{j}+\alpha_{3}^{j}} \\
     &  & \,\,\,\boldsymbol{\alpha}^{j}=(\alpha_{1}^{j},\alpha_{2}^{j},\alpha_{3}^{j}) \\
     &  & \,\,\,\log(\boldsymbol{\alpha}^j) \sim \mathcal{N}(\mu,\Sigma) \\
    \end{eqnarray*} $$

4. Endowments are exponentially distributed,

$$
\begin{eqnarray*}
\boldsymbol{e}^{j} &=& (e_{1}^{j},e_{2}^{j},e_{3}^{j}) \\
 &  & e_i^j \sim f, f(z;\zeta) =  1/\zeta \exp(-z/\zeta)
\end{eqnarray*}
$$

Let $p_3 = 1$ be the **numeraire**. The implied **demand functions** are:

$$
\begin{eqnarray*}
x_{i}^{\star j}(p_{1},p_{2},\boldsymbol{e}^{j})&=&\beta^{j}_i\frac{I^j}{p_{i}} \\
\end{eqnarray*}
$$

where consumer $j$'s income is

$$I^j = p_1 e_1^j + p_2 e_2^j +p_3 e_3^j$$

We define parameters:

```python
#parameters:
N = 100
mu = np.array([3,2,1])
Sigma = np.array([[0.25, 0, 0], [0, 0.25, 0], [0, 0, 0.25]])
gamma = 0.8
zeta = 1

#seed
seed = 1986
np.random.seed(seed)

#preferances
alphas = np.exp(np.random.multivariate_normal(mu, Sigma, size=N))
betas = alphas/np.reshape(np.sum(alphas,axis=1),(N,1))

beta1= betas[:,0]
beta2= betas[:,1]
beta3=betas[:,2]

alpha1= alphas[:,0]
alpha2= alphas[:,1]
alpha2= alphas[:,2]

#endowments
e1 = np.random.exponential(zeta,size=N)
e2 = np.random.exponential(zeta,size=N)
e3 = np.random.exponential(zeta,size=N)
```

#### Exercise 3.1

#### Question 1: Plot the histograms of the budget shares for each good across agents.

We make a histogram and plot the betas:

```python
plt.hist(betas, bins = 60, label= ("Good1", 'Good2', 'Good3'))
plt.legend()
plt.xlabel('Budget share')
plt.ylabel('Number of consumers')
plt.show
```

#### Exercise 3.2

Consider the **excess demand functions:**

$$ z_i(p_1,p_2) = \sum_{j=1}^N x_{i}^{\star j}(p_{1},p_{2},\boldsymbol{e}^{j}) - e_i^j$$

**Question 2:** Plot the excess demand functions.

We define the demand and excess demand functions for each of the three goods. We have also converted the excess demand functions into one value by summing them up, to use later to find the equilibrium.

```python
# demand functions:
# We can eliminate p3 from the demand function, as it is equal to 1 (with p3*e3=e3)

def demand_good1 (beta1, p1, e1, p2, e2, e3):
    I= p1*e1 + p2*e2 + e3
    return beta1*(I/p1)

def demand_good2 (beta2, p1, e1, p2, e2, e3):
    I= p1*e1 + p2*e2 + e3
    return beta2*(I/p2)

def demand_good3 (beta3, p1, e1, p2, e2, e3):
    I= p1*e1 + p2*e2 + e3
    return beta2*(I)
```

```python
#excess demand functions:

def excess_demand_good1(beta1, p1, e1, p2, e2, e3):

        #demand
    demand = demand_good1(beta1, p1, e1, p2, e2, e3)

        #supply
    supply= e1

        #excess demand
    excess_demand= demand-supply

    return excess_demand
```

```python
def excess_demand_good2(beta2, p1, e1, p2, e2, e3):

    #demand
    demand= demand_good2(beta2, p1, e1, p2, e2, e3)

    #supply
    supply= e2

    #excess demand
    excess_demand= demand- supply

    return excess_demand
```

```python
def excess_demand_good1_value(beta1, p1, e1, p2, e2, e3):

    excess_demand_good1_value = np.sum(excess_demand_good1(beta1, p1, e1, p2, e2, e3))

    return excess_demand_good1_value
```

```python
def excess_demand_good2_value(beta2, p1, e1, p2, e2, e3):

    #demand
    excess_demand_value2= np.sum(excess_demand_good2(beta2, p1, e1, p2, e2, e3))

    return excess_demand_value2
```

We make two 3D plots for the excess demands of goods 1 and 2.

```python
# a. prices
p1_vec = np.linspace(0.1,10,100)
p2_vec = np.linspace(0.1,10,100)

# b. plot
p1_grid,p2_grid = np.meshgrid(p1_vec,p2_vec,indexing='ij')
excess_demand_good1_grid = excess_demand_good1(beta1,p1_grid,e1,p2_grid,e2,e3)

fig = plt.figure()
ax = fig.add_subplot(1,1,1,projection='3d')
cs = ax.plot_surface(p1_grid,p2_grid,excess_demand_good1_grid,cmap=cm.jet)

# c. add labels
ax.set_xlabel('$p_1$')
ax.set_ylabel('$p_2$')
ax.set_zlabel('Excess demand good2')

# d. invert xaxis
ax.invert_xaxis()

# e. remove background
ax.xaxis.pane.fill = False
ax.yaxis.pane.fill = False
ax.zaxis.pane.fill = False

# f. add colorbar
fig.colorbar(cs);
plt.show()
```

```python
# make a 3d plot of excess demand for good2:

# a. plot
p1_grid,p2_grid = np.meshgrid(p1_vec,p2_vec,indexing='ij')
excess_demand_good2_grid = excess_demand_good2(beta2,p1_grid,e1,p2_grid,e2,e3)

fig = plt.figure()
ax = fig.add_subplot(1,1,1,projection='3d')
cs = ax.plot_surface(p1_grid,p2_grid,excess_demand_good1_grid,cmap=cm.jet)

# b. add labels
ax.set_xlabel('$p_1$')
ax.set_ylabel('$p_2$')
ax.set_zlabel('Excess demand Good2')

# c. invert xaxis
ax.invert_xaxis()

# d. remove background
ax.xaxis.pane.fill = False
ax.yaxis.pane.fill = False
ax.zaxis.pane.fill = False

# e. add colorbar
fig.colorbar(cs);
plt.show()
```

#### Excercise 3.3

**Quesiton 3:** Find the Walras-equilibrium prices, $(p_1,p_2)$, where both excess demands are (approximately) zero, e.g. by using the following tâtonnement process:

1. Guess on $p_1 > 0$, $p_2 > 0$ and choose tolerance $\epsilon > 0$ and adjustment aggressivity parameter, $\kappa > 0$.
2. Calculate $z_1(p_1,p_2)$ and $z_2(p_1,p_2)$.
3. If $|z_1| < \epsilon$ and $|z_2| < \epsilon$ then stop.
4. Else set $p_1 = p_1 + \kappa \frac{z_1}{N}$ and $p_2 = p_2 + \kappa \frac{z_2}{N}$ and return to step 2.

Just like in the lecture, we use a while loop to determine the equilibrium prices, for which excess demands are both zero. We set out guesses for the prices to one.

```python
def find_equilibrium(betas, p1, e1, p2, e2, e3, kappa=0.5,eps=1e-8,maxiter=10000):

    t = 0
    while True:

        # a. step 1: excess demand
        Z1 = excess_demand_good1_value(beta1, p1, e1, p2, e2, e3)
        Z2 = excess_demand_good2_value(beta2, p1, e1, p2, e2, e3)

        # b: step 2: stop?
        if np.abs(Z1) < eps and np.abs(Z2) < eps or t >= maxiter:
            print(f'{t}: p1= {p1} -> excess demand Z1= {Z1}')
            print(f'{t}: p2= {p2} -> excess demand Z2= {Z2}')
            break

        # c. step 3: update p1
        p1 = p1 + kappa*Z1/N
        p2 = p2 + kappa*Z2/N

        t += 1

    return p1, p2
```

```python
p1,p2= find_equilibrium(betas, 1, e1, 1, e2, e3)
```

    1844: p1= 6.149742400548352 -> excess demand Z1= 9.996033889514422e-09
    1844: p2= 2.685539126759021 -> excess demand Z2= 4.059743818896067e-09

After 1844 iterations, we find the equilibrium prices of around 6.15 for p1 and 2.67 for p2. P3 is normalized to 1.

We check to make sure that demand is really smaller than epsilon.

```python
eps=1e-8
Z1 = excess_demand_good1_value(beta1, p1, e1, p2, e2, e3)
Z2 = excess_demand_good2_value(beta2, p1, e1, p2, e2, e3)
print(Z1,Z2)
assert(np.abs(Z1) < eps)
assert(np.abs(Z2) < eps)
```

    9.996033889514422e-09 4.059743818896067e-09

#### Excercise 3.4

**Question 4:** Plot the distribution of utility in the Walras-equilibrium and calculate its mean and variance.

We define the utility function, insert the above calculated equilibrium prices in the function and plot it as a histogram.

```python
#defining utility function
def utility(betas, p1, e1, p2, e2, e3, gamma):
    x1= demand_good1(betas[:,0], p1, e1, p2, e2, e3)
    x2= demand_good2(betas[:,1], p1, e1, p2, e2, e3)
    x3= demand_good3(betas[:,2], p1, e1, p2, e2, e3)

    utility= (x1**beta1+x2**beta2+x3**beta3)**gamma

    return utility
```

```python
#inserting equilibrium prices
utility_dist = utility(betas, 6.149742400548352, e1, 2.685539126759021, e2, e3, gamma)
```

```python
#making a histogram
plt.hist(utility_dist, bins= 20)
plt.xlabel('Utility')
plt.ylabel('Number of consumers')
plt.show
```

Lastly, we calculate mean and variance.

```python
np.mean(utility_dist)
```

```python
np.var(utility_dist)
```