# FOM Newton computation

Luca Mechelli, Tim Keil

```python
# ~~~
# This file is part of the paper:
#
#  "A NON-CONFORMING DUAL APPROACH FOR ADAPTIVE TRUST-REGION REDUCED BASIS
#           APPROXIMATION OF PDE-CONSTRAINED OPTIMIZATION"
#
#   https://github.com/TiKeil/NCD-corrected-TR-RB-approach-for-pde-opt
#
# Copyright 2019-2020 all developers. All rights reserved.
# License: Licensed as BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)
# ~~~
```

# Preparations

## details

In this notebook we discretize the following problem:
Find $u_{\mu}$ solving the following parametrized constrained equation

\begin{align*}
	-  \nabla \cdot \left( \kappa_{\mu}  \nabla u_{\mu} \right) &= f_{\mu} &&\text{in } \Omega, \\
	( \kappa_{\mu}  \nabla u_{\mu} \cdot n) &= c_{\mu}(u_{\text{out}} - u_{\mu}) &&\text{on } \partial \Omega.
\end{align*}

For the definition of $\kappa_{\mu}$ and $f_{\mu}$ in the computational domain $\Omega$, we use the `BitmapFunction` from PyMOR. This function uses the Python Imaging Library (PIL) in order to convert a .png grayscale picture to a data function on our domain.
Walls, windows and doors are stored in seperated .png to enable a affine decomposition of each part of the picture. Furthermore, we are considering a heater at each window. The numbering of the components for $\kappa_{\mu}$ can be seen in the following picture

Here, numbers with | are walls, numbers with _ are doors and numbers with dots are windows

The heaters are numbered in the following way:

For our pde constrained model, we need to define a cost functional. In a general quadratic model, we have

\begin{align}
\mathcal{J}(u, \mu) := \Theta(\mu) + j_\mu(u) + k_\mu(u, u),
\end{align}

For this code, we restrict ourselves to the following definition
\begin{align}
\mathcal{J}(v, \mu) = \frac{\sigma_D}{2} \int_{D}^{} (v - u^{\text{d}})^2 + \frac{1}{2} \sum^{M}_{i=1} \sigma_i \mu_i^2,
\end{align}
which means

\begin{align}
\Theta(\mu) &= \frac{1}{2} \sum^{M}_{i=1} \sigma_i \mu_i^2 + \frac{\sigma_D}{2} \int_{D}^{} u^{\text{d}} u^{\text{d}}\\
j_{\mu}(u) & = -\sigma_D \int_{D}^{} u^{\text{d}}u \\
k_{\mu}(u,u) &= \frac{\sigma_D}{2} \int_{D}^{} u^2
\end{align}

The following code implements this particular case.

```python
import sys
path = '../../'
sys.path.append(path)
import numpy as np

from matplotlib import pyplot as plt

from pymor.basic import *
set_log_levels({'pymor': 'WARN'})
```

```python
from pymor.core.logger import set_log_levels, getLogger
set_log_levels({'pymor': 'ERROR',
                'distributed_adaptive_discretizations': 'DEBUG',
                'notebook': 'INFO'})
logger = getLogger('notebook.notebook')
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (12.0, 8.0)
mpl.rcParams['font.size'] = 12
mpl.rcParams['savefig.dpi'] = 300

data_path = '../../../EXC_data'
# domain of interest
bounding_box = [[0,0],[2,1]]
domain_of_interest = BitmapFunction('{}/Domain_of_interest.png'.format(data_path), range=[1,0], bounding_box=bounding_box)
```

## problem definition

```python
from pdeopt.problems import EXC_problem, set_input_dict
from pdeopt.discretizer import discretize_quadratic_pdeopt_stationary_cg

parametric_quantities = {'walls': [1,4,9], 'windows': [], 'doors': [], 'heaters': [1,3,5,6,7,8,9]}
inactive_quantities = {'removed_walls': [], 'open_windows': [], 'open_doors': [1,2,3,4,5,6,7,10], 'active_heaters': []}
summed_quantities = {'walls': [[1,2,3,8],[4,5,6,7]], 'windows': [], 'doors': [], 'heaters': [[1,2],[3,4],[9,10,11,12]]}

coefficient_expressions = None

parameters_in_q = True
input_dict = set_input_dict(parametric_quantities, inactive_quantities, coefficient_expressions, summed_quantities, parameters_in_q,
                            ac=0.5, owc=[0.025,0.1], iwc= [0.025,0.1], idc=[0.005], wc=[0.0005], ht=[0,100],
                                    owc_c=0.001,  iwc_c= 0.025,     idc_c=0.01,  wc_c=0.025,  ht_c=80)

parameter_scaling = False
u_out = 5

problem, parameter_scales = EXC_problem(input_dict, summed_quantities, outside_temperature=u_out, #, q_inverse=0.0001
                                        data_path = data_path,parameters_in_q=parameters_in_q,
                                        parameter_scaling=parameter_scaling,
                                        coefficient_expressions=coefficient_expressions)

u_d = 18

mu_d = None

sigma_d = 100
weights = {'walls': [0.5,0.25,0.05], 'doors': 1, 'heaters': [0.002,0.002,0.001,0.001,0.001,0.001,0.004], 'windows': 1, 'state': sigma_d}

diameter = np.sqrt(2)/200.
opt_fom, data, mu_bar = discretize_quadratic_pdeopt_stationary_cg(problem, diameter, weights, parameter_scales,
                                                          domain_of_interest, desired_temperature=u_d,
                                                          mu_for_u_d=mu_d, mu_for_tikhonov=mu_d,
                                                          parameters_in_q=parameters_in_q, product='fixed_energy',
                                                          use_corrected_gradient= True)
```

    Warning: Check whether you have not used coefficient_expressions for the later experiment
    I am using the corrected functional!!
    I am using the corrected gradient!!
    {heaters: [10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0], walls: [0.049999999999999996, 0.049999999999999996, 0.049999999999999996]}
    my product is fixed_energy

```python
print('information on the grid:')
print(data['grid'])

radius = 0.1               # TR radius
FOC_tolerance = 1e-12       # tau_FOC
max_it = 400               # Maximum number of iteration
max_it_arm = 50            # Maximum number of iteration of the Armijo rule
init_step_armijo = 0.5     # Initial step for the Armijo rule
armijo_alpha = 1e-4        # kappa_arm
epsilon_i = 1e-8           # Treshold for the epsilon active set (Kelley '99)
```

    information on the grid:
    Rect-Grid on domain [0,2] x [0,1]
    x0-intervals: 400, x1-intervals: 200
    faces: 80000, edges: 160600, vertices: 80601

Now we pick one specific starting parameter

```python
# starting with
mu = problem.parameter_space.sample_randomly(1, seed= 1)[0]
print(mu)
```

    {heaters: [41.7022004702574, 72.0324493442158, 0.011437481734488664, 30.233257263183976, 14.675589081711305, 9.233859476879779, 18.62602113776709], walls: [0.05091705452822859, 0.05475756056730025, 0.06541125505025178]}

## Visualizations

Visualize the position of left- and right-hand sides parameters

```python
from pymor.discretizers.builtin.cg import InterpolationOperator

vis_mu = problem.parameter_space.sample_randomly(1, seed= 13)[0]
vis_mu['walls'][0] = 0.35
vis_mu['walls'][1] = 0.2
vis_mu['walls'][2] = 0.0001
vis_mu['heaters'][0] = 30
vis_mu['heaters'][1] = 40
vis_mu['heaters'][2] = 50
vis_mu['heaters'][3] = 60
vis_mu['heaters'][4] = 70
vis_mu['heaters'][5] = 80
vis_mu['heaters'][6] = 90
diff = InterpolationOperator(data['grid'], problem.diffusion).as_vector(vis_mu)
rhs = InterpolationOperator(data['grid'], problem.rhs).as_vector(vis_mu)
doI = InterpolationOperator(data['grid'], domain_of_interest).as_vector(vis_mu)
opt_fom.visualize(diff)
plt.savefig('ciao.pdf', format='pdf', bbox_inches="tight")
opt_fom.visualize(rhs)
#plt.savefig('exc_2_rhs.pdf', format='pdf', bbox_inches="tight")
opt_fom.visualize(doI)
```

    ThreeJSPlot(children=(HBox(children=(Renderer(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, …

    ThreeJSPlot(children=(HBox(children=(Renderer(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, …

    ThreeJSPlot(children=(HBox(children=(Renderer(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, …

    <Figure size 864x576 with 0 Axes>

We can easily solve the primal and dual equations by calling

```python
u = opt_fom.solve(mu)
p = opt_fom.solve_dual(mu)

opt_fom.visualize(u)
opt_fom.visualize(p)
```

    ThreeJSPlot(children=(HBox(children=(Renderer(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, …

    ThreeJSPlot(children=(HBox(children=(Renderer(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, …

# Full Order Optimization

```python
print('Starting parameter: ', opt_fom.parse_parameter_inverse(mu))
J_start = opt_fom.output_functional_hat(mu)
print('Starting J: ', J_start)
```

    Starting parameter:  [4.17022005e+01 7.20324493e+01 1.14374817e-02 3.02332573e+01
     1.46755891e+01 9.23385948e+00 1.86260211e+01 5.09170545e-02
     5.47575606e-02 6.54112551e-02]
    Starting J:  501.30528547701397

## FOM Newton

```python
from pdeopt.TR import solve_optimization_NewtonMethod
from pdeopt.tools import compute_errors

TR_parameters = {'radius': 1.e18, 'sub_tolerance': FOC_tolerance, 'max_iterations': max_it,
                 'starting_parameter': mu,
                 'epsilon_i': epsilon_i,
                 'max_iterations_armijo': max_it_arm, 'initial_step_armijo': init_step_armijo,
                 'armijo_alpha': armijo_alpha,
                 'full_order_model': True }

muoptfom,_,_,_, times_1, mus_1, Js_1, FOC_1 = solve_optimization_NewtonMethod(opt_fom,opt_fom.parameter_space,mu,TR_parameters, timing=True)
```

    Starting parameter {heaters: [41.7022004702574, 72.0324493442158, 0.011437481734488664, 30.233257263183976, 14.675589081711305, 9.233859476879779, 18.62602113776709], walls: [0.05091705452822859, 0.05475756056730025, 0.06541125505025178]}
    Using truncated CG for the linear system
    CG truncated at iteration: 0 with residual: 57.62637204479378, because p_k is not a descent direction
    Choosing the gradient as direction
    Step [2.85904420e+01 5.85622807e+01 0.00000000e+00 2.33556820e+01
     7.57377500e+00 2.03159762e+00 0.00000000e+00 2.50000000e-02
     2.50000000e-02 2.50000000e-02], functional 5.663654308707237 , FOC condition 0.4691470741933961
    Using truncated CG for the linear system
    Step [28.015539   28.69083877  0.09879301 29.51555641 30.42519391 30.94807228
      0.38696257  0.1         0.1         0.04595768], functional 4.0088310055441525 , FOC condition 0.27091308376320594
    Using truncated CG for the linear system
    Step [16.63884255 16.98044618 17.27428522 17.37332441 17.90079958 18.09332588
     17.00230683  0.04445708  0.1         0.025     ], functional 2.7750661310421947 , FOC condition 0.04612533769438143
    Using truncated CG for the linear system
    Step [16.49460499 16.91668327 17.32719028 17.48225557 18.11977136 18.31858072
     16.97675542  0.025       0.07365179  0.025     ], functional 2.773607887631215 , FOC condition 0.02048322391895388
    Using truncated CG for the linear system
    Step [16.52117948 16.9282115  17.32112814 17.46592787 18.11574119 18.39759481
     16.94009947  0.025       0.025       0.025     ], functional 2.772773406164333 , FOC condition 0.00041865039117550064
    Using truncated CG for the linear system
    Step [16.51891986 16.92820878 17.32387911 17.46951228 18.12299797 18.40759241
     16.93448658  0.025       0.025       0.025     ], functional 2.7727731849149677 , FOC condition 9.290601002255482e-13
    Converged: FOC = 9.290601002255482e-13
    relative differences mu 0.00031459451869582736 and J 7.979352550168532e-08

# Results

```python
for i in range(len(mus_1[-1])):
    print("{0:.12e}".format(mus_1[-1][i]))
u = opt_fom.solve(opt_fom.pre_parse_parameter(mus_1[-1]))
opt_fom.visualize(u)
```

    1.651891986460e+01
    1.692820877511e+01
    1.732387911116e+01
    1.746951227840e+01
    1.812299797114e+01
    1.840759241105e+01
    1.693448657882e+01
    2.500000000000e-02
    2.500000000000e-02
    2.500000000000e-02

    ThreeJSPlot(children=(HBox(children=(Renderer(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, …

```python
from pymor.discretizers.builtin.cg import (L2ProductP1, L2ProductQ1, InterpolationOperator)
from pymor.discretizers.builtin.grids.referenceelements import square
from pymor.discretizers.builtin.grids.boundaryinfos import EmptyBoundaryInfo
if data['grid'].reference_element is square:
    L2_OP = L2ProductQ1
else:
    L2_OP = L2ProductP1
if mu_d is None:
    empty_bi = EmptyBoundaryInfo(data['grid'])
    u_d = InterpolationOperator(data['grid'], ConstantFunction(u_d,2)).as_vector()
    diff= u.to_numpy()-u_d.to_numpy()
    diff_= opt_fom.solution_space.from_numpy(diff)
    Restricted_L2_OP = L2_OP(data['grid'], empty_bi, dirichlet_clear_rows=False, coefficient_function=domain_of_interest)
    print("{}".format(Restricted_L2_OP.apply2(diff_,diff_)[0][0]))
    print("{}".format(Restricted_L2_OP.apply2(diff_,diff_)[0][0]/Restricted_L2_OP.apply2(u_d,u_d)))
```

    6.477934888377442e-05
    [[1.71324989e-06]]


```python
from IPython.display import Image
Image('../../../python_for_probability_statistics_and_machine_learning.jpg')
```

[Python for Probability, Statistics, and Machine Learning](https://www.springer.com/fr/book/9783319307152)

```python
import numpy as np
np.random.seed(12345)
```

# Conditional Expectation and Mean Square Error

In this section, we work through a detailed example using conditional
expectation and optimization methods.  Suppose we have two fair six-sided die
($X$ and $Y$) and we want to measure the sum of the two variables as $Z=X+Y$.
Further, let's suppose that given $Z$, we want the best estimate of $X$ in the
mean-squared-sense. Thus, we want to minimize the following:

$$
J(\alpha) = \sum ( x - \alpha z )^2 \mathbb{P}(x,z)
$$

 where $\mathbb{P}$ is the probability mass function for this problem.
The idea is that when we have solved this problem, we will have a function of
$Z$ that is going to be the minimum MSE  estimate of $X$.  We can substitute in
for $Z$ in $J$ and get:

$$
J(\alpha) = \sum ( x - \alpha (x+y) )^2 \mathbb{P}(x,y)
$$

  Let's work out the steps in Sympy in the following:

```python
import sympy as S
from sympy.stats import density, E, Die

x=Die('D1',6)     # 1st six sided die
y=Die('D2',6)     # 2nd six sides die
a=S.symbols('a')
z = x+y           # sum of 1st and 2nd die
J = E((x-a*(x+y))**2) # expectation
print S.simplify(J)
```

    329*a**2/6 - 329*a/6 + 91/6

 With all that setup we can now use basic calculus to minimize the
objective function $J$,

```python
sol,=S.solve(S.diff(J,a),a) # using calculus to minimize
print sol # solution is 1/2
```

    1/2

**Programming Tip.**

Sympy has a `stats` module that can do some basic work with expressions
involving probability densities and expectations. The above code uses its `E`
function to compute the expectation.

 This says that $z/2$ is the MSE estimate of $X$ given $Z$ which means
geometrically (interpreting the MSE as a squared distance weighted by the
probability mass function) that $z/2$ is as *close* to $x$ as we are going to
get for a given $z$.

Let's look at the same problem using the conditional expectation operator $
\mathbb{E}(\cdot|z) $ and apply it to our definition of $Z$. Then

$$
\mathbb{E}(z|z)=\mathbb{E}(x+y|z)=\mathbb{E}(x|z)+\mathbb{E}(y|z)=z
$$

 using the linearity of the expectation. Now, since by the
symmetry of the problem (i.e., two identical die), we have

$$
\mathbb{E}(x|z)=\mathbb{E}(y|z)
$$

 we can plug this in and solve

$$
2 \mathbb{E}(x|z)=z
$$

 which once again gives,

$$
\mathbb{E}(x|z)  =\frac{z}{2}
$$

```python
%matplotlib inline

from numpy import arange,array
from matplotlib.pylab import subplots, cm
from sympy import Integer
fig,ax = subplots()
v = arange(1,7) + arange(1,7)[:,None]
foo=lambda i: density(z)[Integer(i)].evalf() # some tweaks to get a float out
Zmass=array(map(foo,v.flat),dtype=float).reshape(6,6)

pc=ax.pcolor(arange(1,8),arange(1,8),Zmass,cmap=cm.gray)
_=ax.set_xticks([(i+0.5) for i in range(1,7)])
_=ax.set_xticklabels([str(i) for i in range(1,7)])
_=ax.set_yticks([(i+0.5) for i in range(1,7)])
_=ax.set_yticklabels([str(i) for i in range(1,7)])
for i in range(1,7):
    for j in range(1,7):
        _=ax.text(i+.5,j+.5,str(i+j),fontsize=18,fontweight='bold',color='goldenrod')

_=ax.set_title(r'Probability Mass for $Z$',fontsize=18)
_=ax.set_xlabel('$X$ values',fontsize=18)
_=ax.set_ylabel('$Y$ values',fontsize=18);
cb=fig.colorbar(pc)
_=cb.ax.set_title(r'Probability',fontsize=12)
#fig.savefig('fig-probability/Conditional_expectation_MSE_001.png')
```

The values of $Z$ are in yellow with the corresponding values for $X$ and $Y$ on the axes. The gray scale colors indicate the underlying joint probability density.

 which is equal to the estimate we just found by minimizing the MSE.
Let's explore this further with [Figure](#fig:Conditional_expectation_MSE_001).  [Figure](#fig:Conditional_expectation_MSE_001) shows the values of $Z$ in yellow with
the corresponding values for $X$ and $Y$ on the axes.  Suppose $z=2$, then the
closest $X$ to this is $X=1$, which is what $\mathbb{E}(x|z)=z/2=1$ gives. What
happens when $Z=7$? In this case, this value is spread out diagonally along the
$X$ axis so if $X=1$, then $Z$ is 6 units away, if $X=2$, then $Z$ is 5 units
away and so on.

Now, back to the original question, if we had $Z=7$ and we wanted
to get as close as we could to this using $X$, then why not choose
$X=6$ which is only one unit away from $Z$? The problem with doing
that is $X=6$ only occurs 1/6 of the time, so we are not likely to
get it right the other 5/6 of the time. So, 1/6 of the time we are
one unit away but 5/6 of the time we are much more than one unit
away. This means that the MSE score is going to be worse. Since
each value of $X$ from 1 to 6 is equally likely, to play it safe,
we choose $7/2$ as the estimate, which is what the conditional
expectation suggests.

We can check this claim with samples using Sympy below:

```python
import numpy as np
from sympy import stats
# Eq constrains Z
samples_z7 = lambda : stats.sample(x, S.Eq(z,7))
#using 6 as an estimate
mn= np.mean([(6-samples_z7())**2 for i in range(100)])
#7/2 is the MSE estimate
mn0= np.mean([(7/2.-samples_z7())**2 for i in range(100)])
print 'MSE=%3.2f using 6 vs MSE=%3.2f using 7/2 ' % (mn,mn0)
```

    MSE=8.44 using 6 vs MSE=2.43 using 7/2

**Programming Tip.**

The `stats.sample(x, S.Eq(z,7))` function call samples the `x` variable subject
to a condition on the `z` variable. In other words, it generates random samples
of `x` die, given that the sum of the outcomes of that die and the `y` die add
up to `z==7`.

 Please run the above code repeatedly in the Jupyter/IPython
Notebook corresponding to this section until you have convinced
yourself  that the $\mathbb{E}(x|z)$ gives the lower MSE every
time.  To push this reasoning, let's consider the case where the
die is so biased so that the outcome of `6` is ten times more
probable than any of the other outcomes. That is,

$$
\mathbb{P}(6) = 2/3
$$

 whereas $\mathbb{P}(1)=\mathbb{P}(2)=\ldots=\mathbb{P}(5)=1/15$.
We can explore this using Sympy as in the following:

```python
# here 6 is ten times more probable than any other outcome
x=stats.FiniteRV('D3',{1:1/15., 2:1/15.,
                       3:1/15., 4:1/15.,
                       5:1/15., 6:2/3.})
```

 As before, we construct the sum of the two dice, and plot the
corresponding probability mass function in [Figure](#fig:Conditional_expectation_MSE_002).  As compared with [Figure](#fig:Conditional_expectation_MSE_001), the probability mass has been shifted
away from the smaller numbers.

```python
z = x + y
foo=lambda i: density(z)[S.Integer(i)].evalf() # some tweaks to get a float out
v = np.arange(1,7) + np.arange(1,7)[:,None]
Zmass=np.array(map(foo,v.flat),dtype=float).reshape(6,6)

from matplotlib.pylab import subplots, cm
fig,ax=subplots()
pc=ax.pcolor(np.arange(1,8),np.arange(1,8),Zmass,cmap=cm.gray)
_=ax.set_xticks([(i+0.5) for i in range(1,7)])
_=ax.set_xticklabels([str(i) for i in range(1,7)])
_=ax.set_yticks([(i+0.5) for i in range(1,7)])
_=ax.set_yticklabels([str(i) for i in range(1,7)])
for i in range(1,7):
    for j in range(1,7):
        _=ax.text(i+.5,j+.5,str(i+j),fontsize=18,fontweight='bold',color='goldenrod')

_=ax.set_title(r'Probability Mass for $Z$; Nonuniform case',fontsize=16)
_=ax.set_xlabel('$X$ values',fontsize=18)
_=ax.set_ylabel('$Y$ values',fontsize=18);
cb=fig.colorbar(pc)
_=cb.ax.set_title(r'Probability',fontsize=12)
#fig.savefig('fig-probability/Conditional_expectation_MSE_002.png')
```

The values of $Z$ are in yellow with the corresponding values for $X$ and $Y$ on the axes.

Let's see what the conditional expectation says about how we can estimate $X$
from $Z$.

```python
E(x, S.Eq(z,7)) # conditional expectation E(x|z=7)
```

    5.00000000000000

  Now that we have $\mathbb{E}(x|z=7) = 5$, we can generate
samples as before and see if this gives the minimum MSE.

```python
samples_z7 = lambda : stats.sample(x, S.Eq(z,7))
#using 6 as an estimate
mn= np.mean([(6-samples_z7())**2 for i in range(100)])
#5 is the MSE estimate
mn0= np.mean([(5-samples_z7())**2 for i in range(100)])
print 'MSE=%3.2f using 6 vs MSE=%3.2f using 5 ' % (mn,mn0)
```

    MSE=4.08 using 6 vs MSE=2.20 using 5

Using a simple example, we have emphasized the connection between minimum mean
squared error problems and conditional expectation. Hopefully, the last two
figures helped expose the role of the probability density.  Next, we'll
continue revealing  the true power of the conditional expectation as we
continue to develop corresponding geometric intuition.

```python

```


# High order accurate time integration methods

### Information about the course
1. Teacher: Davide Torlo, postdoc at Cardamom in Inria Bordeaux, working on hyperbolic PDE, shallow water, finite element, model order reduction
1. 12 hours: $14^{th}-15^{th}$ March, 9.00-12.00  +  13.30-16.30
1. Check presence every
1. Share half screen for whiteboard or Jupyter Notebook
1. Jupyter notebooks on [GitHub](https://github.com/accdavlo/HighOrderODESolvers) (python3, few packages (numpy, scipy, matplotlib, nodepy)) or open them in [Google colab](https://github.com/accdavlo/HighOrderODESolvers#readme)
1. Prerequisite: a bit of coding (anything is fine), basics of mathematics
1. Topic is recent, but not difficult
1. References:
    1. Remi Abgrall's course at University of Zurich [course page](https://www.math.uzh.ch/index.php?id=ve_vo_det&key2=2970&keySemId=34) and [notes](https://www.math.uzh.ch/typo3conf/ext/qfq/Classes/Api/download.php?s=605796143d42a)
    1. Hairer E., Nørsett S. P., Wanner G., *Solving Ordinary Differential Equations I: Nonstiff Problems* [book](https://www.springer.com/de/book/9783540566700)
    1. Hairer E., Wanner G., *Solving Ordinary Differential Equations II: Stiff and Differential-Algebraic Problems* [book](https://www.springer.com/de/book/9783540604525)
    1. J. C. Butcher, *Numerical Methods for Ordinary Differential Equations*, 2016, John Wiley & Sons, Ltd [book](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119121534)
    1. David I. Ketcheson, *Relaxation Runge--Kutta Methods: Conservation and Stability for Inner-Product Norms*. SIAM Journal on Numerical Analysis 2019 57:6, 2850-2870. https://doi.org/10.1137/19M1263662 [arXiv](https://arxiv.org/abs/1905.09847)
    1. Han Veiga, M., Öffner, P. & Torlo, D. *DeC and ADER: Similarities, Differences and a Unified Framework.* J Sci Comput 87, 2 (2021). https://doi.org/10.1007/s10915-020-01397-5 [arXiv](https://arxiv.org/abs/2002.11764)
    1. Hans Burchard, Eric Deleersnijder, Andreas Meister, *A high-order conservative Patankar-type discretisation for stiff systems of production–destruction equations*. Applied Numerical Mathematics, 47, 1 (2003). https://doi.org/10.1016/S0168-9274(03)00101-6
    1. Öffner, P. & Torlo, D. *Arbitrary high-order, conservative and positivity preserving Patankar-type deferred correction schemes*. Applied Numerical Mathematics (153), July 2020, Pages 15-34 https://doi.org/10.1016/j.apnum.2020.01.025 [arXiv](https://arxiv.org/abs/1905.09237)
    1. Gottlieb, S. , Shu, C.-W., and Tadmor, E.. *Strong stability-preserving high-order time discretization methods.* SIAM review 43.1 (2001): 89-112. https://doi.org/10.1137/S003614450036757X [pdf](https://apps.dtic.mil/sti/pdfs/ADA376446.pdf)
    1. Ketcheson et al.  [Nodepy package](https://nodepy.readthedocs.io/)

We will work on python Jupyter notebooks and we will need a couple of modules

```python
# If you do not have numpy, matplotlib, scipy or nodepy, run this cell
!pip install numpy
# This is the basic package in python with all the numerical functions

!pip install scipy
# This package has some functions to deal with polynomials

!pip install matplotlib
# This package allows to  plot

!pip install nodepy
# This is a package of numerical solvers for ODE which implements many useful functions
```

#### Python (what do we need)
* Interpreted language
* Indentation rules the cycles and the colon :
* Indexing from 0
* Modules

```python
import numpy as np
## Example
k=0
for j in range(5):
    k = k+j
    print("This is the ",j,"th triangular number: ",k, "and its sqrt",np.sqrt(k),\
         " and this is its square", k**2.)
```

```python
# numpy arrays
myArray=np.ones(5)
print(myArray)

myArray2=np.zeros((3,2))
print(myArray2)

myArray3=np.array([1,2,5])
print(myArray3)

myArray4 = np.array([[1,2,5],[4,6,7]])
print(myArray4)
```

#### Jupyter notebooks
* Practical to alternate (Markdown) text and codes (not only python)
* Cells that can be evaluated (Ctrl+Enter or Shift+Enter to go to the next one)
* Create new cell above (A) below (B)
* To edit a cell (double click/Enter)

## Theory of ODE

### Table of contents
 1. Chapter 1: Theory of ODEs
    1. [Section 1.1: Motivational problems](#examples)
    1. [Section 1.2: Properties of ODEs](#properties)
    1. [Section 1.3: Existence and Uniqueness](#existence)

In many branches of natural sciences, ordinary differential equations as well as time-dependent partial differential equations play an important role in the modeling of physical, biological or chemical phenomena.

In this course we will deal with **ordinary differential equations** (ODEs) and with numerical methods which are able to approximate the solutions of such equations with different levels of **accuracy**.

An ODE can be written in the following setting. Consider a *time interval* $I:=[t_0, t_{end}]\subset \mathbb{R}$ an unknown variable $y:I\to \mathbb{R}^{S}$ and a *continuous* function $F:I\times \mathbb{R}^S\to\mathbb{R}^S$. The initial value problem (IVP) (or Cauchy problem) of a system of ODEs can be written as

$$
\begin{equation}
\begin{cases}
\frac{dy}{dt} = F(t,y(t)),\\
y(0)=y_0.
\end{cases}
\end{equation}
$$

The previous form is general enough even for higher order ODEs, indeed, suppose that we want to solve

\begin{equation}
y^{(p)}=f(t,y(t),y'(t),\dots,y^{(p-1)}(t))
\end{equation}

then, we can write it into the following first order system

\begin{align}
\begin{cases}
y'(t)=z_1(t)\\
z_1'(t)=z_2(t)\\
\vdots \\
z_{p-1}(t)= z_p(t)\\
z_p'(t)= f(t,z_1(t),z_2(t), \dots, z_{p-1}(t))
\end{cases}
\end{align}

### Integral solution form
#### Remark
If y is a solution of
$$
\begin{equation}
\begin{cases}
\frac{dy}{dt} = F(t,y(t)), \quad t\in I\\
y(0)=y_0.
\end{cases}
\end{equation}
$$
then, for any $t\in I$
$$
y(t) = y_0 + \int_{t_0}^t F(s,y(s)) ds.
$$

Also the opposite is true, if $y\in \mathcal{C}^0(I)$ and it satisfies the integral form, they $y\in \mathcal{C}^1(I)$ and it solves the differential form.

## Motivational ODE problems

Before starting with a bit of theory, let us show some famous and important examples of ODEs that we will carry all along the course.

#### Scalar linear ODE (Dahlquist's problem)
This is the *simplest* ODE one can consider and it is defined as
$$
\begin{equation}
\begin{cases}
y'(t) = -\lambda y(t)\\
y(t_0)=y_0
\end{cases}
\end{equation}
$$
with exact solution computable from
$$
\begin{align}
&\int_{t_0}^t \frac{y'(s)}{y(s)}ds = -\lambda \int_{t_0}^t dt\\
&\int_{t_0}^t \frac{d}{ds} \log(y(s))ds = -\lambda (t-t_0)\\
&\log(y(t))- \log(y(t_0)) = -\lambda (t-t_0)\\
&\log(y(t)/y_0) = -\lambda (t-t_0)\\
&y(t)/y_0 = e^{-\lambda (t-t_0)}\\
&y(t) = y_0 e^{-\lambda (t-t_0)}
\end{align}
$$

This equation will be useful to study the stability of numerical methods. Indeed, the solution can converge to 0, when $\lambda > 0$ or can explode exponentially if $\lambda < 0$, while it stays in an unstable equilibrium for $\lambda =0$.

```python
##Exercise: plot the solution of Dahlquist's problem for lambda = 2 and lambda = -1

import numpy as np
import matplotlib.pyplot as plt

xx=np.linspace(0,2,101)

plt.plot(xx, FILL IN THE SOLUTION  ,label="lambda=2") ##Here the solution
plt.plot(xx, FILL IN THE SOLUTION ,label="lambda=-1") ##Here the solution
plt.legend()
plt.grid(True)
plt.show()
```

#### Linear system (Production destruction system)
There are two populations $c_1,\,c_2$, they destroy themselves and increase in function of the other population (linearly).

$$
\begin{equation}
\begin{aligned}
& \begin{cases}
c_1'(t)=c_2(t)-5c_1(t),\\
c_2'(t)=5c_1(t)-c_2(t),
\end{cases}\\
 &c_1(0)=c_1^0=0.9, \quad  &c_2(0)=c_2^0=0.1 \, .
\end{aligned}
\end{equation}
$$

Total mass is conserved
$$
c_1'+c_2'=0.
$$

We can compute the exact solution as exponential, by writing the problem in vectorial form

$$
\begin{align}
c'(t)=Ac(t)\\
c(0)=c^0
\end{align}\\
c(t)=e^{At}c^0
$$
where the exponential matrix can be computed as
$$
e^{At}=\sum_{k=0}^{\infty} \frac{A^k t^k}{k!}
$$
notice that $A^2=-6 A$,

$$
c(t)=c^0+\frac{1-e^{-6t}}{6} Ac^0.
$$

If $c_0>0$, then $c(t)>0$ for all $t\in \mathbb{R}$.

```python
#### Exercise: plot the exact solution for the linear system
#### To compute matrix A vector b multiplication you can use np.matmul(A,b)
A=np.array([[-5,1],[5,1]])
c0=np.array([0.9,0.1])
tt=np.linspace(0,1,100)

ct=np.zeros((len(c0),len(tt)))
for it, t in enumerate(tt):
    ct[:,it]= FILL IN THE EXACT SOLUTION  ##Here the solution

# Then we plot the two components
plt.plot(tt,ct[0,:], label="c1")
plt.plot(tt,ct[1,:], label="c2")
plt.legend()
```

#### Lotka-Volterra predator-prey
$$
x'(t)=\alpha x - \beta x y\\
y'(t)=\delta xy - \gamma y
$$

No analytical solution is known for this problem.

The evolution looks like this.

One can study equilibria of the dynamical system ($x'=0$, $y'=0$).

And the orbits around the stable equilibrium points.

On each orbit the quantity

$$
\eta(x,y) = \delta x -\gamma \log(x) +\beta y - \alpha \log(y)
$$

is conserved. How to prove it?

##### Exercise
Prove that $\langle \nabla \eta(x,y),F(x,y) \rangle =0$

#### Nonlinear Oscillator
\begin{equation}
\begin{cases}
u'(t)=-\frac{v(t)}{n(t)}\\
v'(t)= \frac{u(t)}{n(t)}\\
n(t):=\sqrt{u^2(t)+v^2(t)}
\end{cases}
\end{equation}

##### Exercise
Prove that the energy $\frac{n(t)^2}{2}$ is conserved.

```

```

Thanks to the energy, we can also describe the **exact solution**

$$
\begin{pmatrix}
u\\v
\end{pmatrix}= \begin{pmatrix} \cos(\theta) &-\sin(\theta)\\ \sin(\theta) & \cos(\theta)\end{pmatrix}\begin{pmatrix}
u^0\\v^0
\end{pmatrix},\qquad \theta(t)=\frac{t}{n}.
$$

```python
## Plot the nonlinear oscillator components and their phase space
tt=np.linspace(0,100,100)
u0=np.array([2,0])
uu=np.zeros((len(u0),len(tt)))   # Solution at all times
n0 = np.sqrt(u0[0]**2.+u0[1]**2.)
for it,t in enumerate(tt):
    theta=t/n0
    A=np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])
    uu[:,it]=np.dot(A,u0)

plt.figure()   #Plot the two components of the solution with respect to time
plt.plot( FILL IN )
plt.plot( FILL IN )
plt.show()

plt.figure()   ## Plot the solution in the phase space (u,v)
plt.plot(FILL IN THE SOLUTION ,'.')  #The '.' plots dots instead of continuous lines
plt.show()
```

#### Damped Nonlinear Oscillator
\begin{equation}
\begin{cases}
u'(t)=-\frac{v(t)}{n(t)}- \alpha u\\
v'(t)= \frac{u(t)}{n(t)}- \alpha v\\
n(t):=\sqrt{u^2(t)+v^2(t)}
\end{cases}
\end{equation}

#### Exercise
Prove that the energy $\frac{n(t)^2}{2}$ is decreasing.
$$
\frac{d}{dt}\left(\frac{n^2}{2}\right)=
$$

This leads us to
$$
n(t) = e^{-\alpha t} n(0).
$$

Exact solution
$$
\begin{pmatrix}
u\\v
\end{pmatrix}= e^{-\alpha t} \begin{pmatrix} \cos(\theta) &-\sin(\theta)\\ \sin(\theta) & \cos(\theta)\end{pmatrix}\begin{pmatrix}
u^0\\v^0
\end{pmatrix}, \qquad \theta(t):= \frac{e^{\alpha t}-1}{\alpha n(t)}.
$$

```python
## Plot the exact solution in time and in phase space
tt=np.linspace(0,100,1000)
alpha=0.01
u0=np.array([2,0])
n0=np.sqrt(np.sum(u0**2))
uu=np.zeros((len(u0),len(tt)))
for it,t in enumerate(tt):
    nt  = np.exp(-alpha*t)*n0
    theta = (np.exp(-alpha*t)-1)/alpha/nt
    A = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])
    uu[:,it]= FILL IN THE FINAL SOLUTION ### ADD the final solution here

plt.figure()
plt.plot(tt,uu[0,:])
plt.plot(tt,uu[1,:])
plt.show()

plt.figure()
plt.plot(uu[0,:],uu[1,:],'.')
plt.show()
```

#### Non linear pendulum
After adimensionalizations ti can be written as

$$u''=-sin(u)$$

in our notation becomes

$$
\begin{cases}
u'=v\\
v'=-sin(u)
\end{cases}
$$

The total energy (kinetic + potential)

$$
\eta(t)=\frac{1}{2}v^2-\cos(u)
$$
is conserved.

##### Exercise
Prove it!

#### Robertson's problem
It is a stiff problem where the time scales vary a lot. It is the equation for a chemical reaction system.
It consists of

\begin{equation}
 \begin{cases}
  c_1'(t)&=10^4c_2(t)c_3(t)-0.04c_1(t),\\
  c_2'(t)&= 0.04c_1(t)-10^4c_2(t)c_3(t)-3\cdot 10^7c_2(t)^2,\\
  c_3'(t)&=3\cdot 10^7c_2(t)^2
 \end{cases}
\end{equation}
with initial conditions

$c^0=(1,0,0)$

Again we preserve the total mass

$$
c_1'+c_2'+c_3'=0,
$$

all the quantities stay nonnegative $c_i(t)\geq 0$.

#### SIR model
Well known in these days, SIR models the development of a sickness in a population. The population is subdivided into $S$ susceptible, $I$ infected and $R$ recovered.

$$
\begin{cases}
S'= -\beta I\frac{S}{N}\\
I' = \beta I \frac{S}{N} - \gamma I\\
R' = \gamma I\\
N=S+I+R
\end{cases}
$$

Total population $N$ does not change
$$
N'=S'+I'+R'=0,
$$
all the classes stay nonnegative $S,I,R\geq 0$.

#### Semidiscretized PDE
Suppose we want to solve a time dependent PDE, e.g. find $U(x,t):\Omega \times [0,T] \to \mathbb{R}^d$

$$
\partial_t U + \partial_{xx}(a(U)) + \partial_x b(U) +c(U)=0, \qquad \forall x\in \Omega, \, \forall t \in [0,T].
$$

Once discretized the spatial domain, we obtain a vector $\underline{U}\in \mathbb{R}^N$ and we can discretize the operators $a,b,c$ with our favorite method (FV, FD, FEM, DG), obtaining

$$
\partial_t \underline{U} + D_2A(\underline{U}) + DB(\underline{U}) +C(\underline{U})=0, \qquad \forall t \in [0,T].
$$

Then, defining $f(\underline{U}):=-\left(  D_2A(\underline{U}) + DB(\underline{U}) +C(\underline{U})\right)$, we can solve the ODE

$$
\partial_t \underline{U} = f(\underline{U}).
$$

#### Semidiscretized conservation law
If we deal with a (scalar) hyperbolic conservation law, with $b$ a convex function,

$$
\partial_t U(x,t) + \partial_x b(U(x,t))=0,
$$

a property we might be interested in preserving also at the discrete level is the total variation diminishing (TVD) character of the weak solution, i.e.,

$$
\sum_{i} |U^{n+1}_i -U^{n+1}_{i-1}|\leq |U^{n}_i -U^{n}_{i-1}|.
$$

Indeed, there are TVD fluxes for FV/FD schemes.

For example, there is interest in building schemes such that they can be written in the incremental form

$$
U^{n+1}_j=U^{n}_j +\Delta t \left [ C_{j+1/2}(U_{j+1}^n-U_{j}^n)-D_{j-1/2}(U_{j}^n-U_{j-1}^n) \right]
$$

where $C_{j+1/2},D_{j+1/2}\geq 0$ and $\Delta t (C_{j+1/2}+D_{j+1/2})\leq 1$.

This form guarantees that the explicit Euler method we have written above is TVD.

## Properties of some ODEs to keep in mind when discretizing
#### Entropy/Energy/Liyapunov conservation/dissipation

We have seen in the previous examples that many ODE systems preserve some quantity of interest. It might be the physical energy, the total mass, an entropy or another Liyapunov functional.
It might be important to preserve these properties also in the discrete settings, for example, denoting with $\eta$ the quantity we would like to preserve (or dissipate) we might want to require

\begin{equation}
\eta(u^{n+1})\stackrel{(\leq)}{=}\eta(u^n).
\end{equation}

#### Positivity/TVD
A positive system is such that, given initial conditions $y_i^0\geq 0 \, \forall i$, then the system stays positive for all times $y_i(t)\geq 0,\ \forall t,\, \forall i$.
This is the case of many physical, biological, chemical models, where the amount of some quantities must stay nonnegative.

Mathematically this condition is  met if $F\in \mathcal C(\mathbb R^S)$ is such that, when $y_i\to 0$, then $F(t,y)_i\geq 0$.
More formally one can write

$$
\lim_{y_i\to 0^+} F(t,y)_i \geq 0, \, \forall t \in \mathbb R, \, \forall y_j \in \mathbb R^+ \text{ with }j\neq i.
$$

By contradiction one can prove that this prevents $y_i$ to become negative.

Again, one may want to preserve this property in the discrete solution.

## Existence and uniqueness of solutions (briefly)

Let us write the ODE on $I:=[t_0, t_{end}]\subset \mathbb{R}$, the unknown variable $y:I\to \mathbb{R}^{S}$ and the *continuous* function $F:I\times \mathbb{R}^S\to\mathbb{R}^S$. The initial value problem (IVP) (or Cauchy problem) of a system of ODEs  written as

$$
\begin{equation}
\begin{cases}\label{eq:IVP}
\frac{dy}{dt} = F(t,y(t)),\\
y(0)=y_0.
\end{cases}
\end{equation}
$$

### Theorem (Cauchy-Peano) Existence
Assume that $F$ is continuous in a neighbourhood of $(t_0,y_0) \in I \times \mathbb R^S$. Then, there exists an interval $J$,  neighbourhood of $y_0$, such that $y'(t)=F(t,y(t))$ for all $t\in J$.

The proof of the theorem the integral form of the ODE and builds a family of equicontinuous solutions in $\mathcal C^0(J)$, then it uses Ascoli-Arzelà.

This defines the concept of **local solutions** that can be extended to larger intervals, until having a **global solution**.

Now, if we look for a solution, we would like to know if there is more than one. For example

$$
\begin{cases} y'=\sqrt{|y|} \\ y(0)=0 \end{cases}
$$
has multiple solutions.
Consider $y(t)=0$. This is a solution.

And $y=t^{2}/4$ is also a solution.

Which one should we consider?

```python
#Possible solutions of the previous equation
tt=np.linspace(0,10,100)
yy0=0*tt;
yy1=tt**2./4.
plt.plot(tt,yy0)
plt.plot(tt,yy1)
```

### Theorem (Cauchy-Lipschitz) Uniqueness
If $F$ is Lipschitz continuous on $I\times \mathbb R^S$ with constant $L$, i.e., for any $(t,y)$ and $(t,z)\in I \times \mathbb R^S$
$$
||F(t,y)-F(t,z)||\leq L ||y-z||,
$$
then the problem has a unique solution.

#### Proof Ingredients
* Fixed point theorem
* Integral form
* Gronwall Lemma

Under this assumption it makes sense to search for the *exact* solution and try to get it or a **good approximation** of it!

### Regularity of the solution
Let $y\in \mathcal C^1(I)$ be the solution of the ODE

$$
\begin{equation}
\begin{cases}
\frac{dy}{dt} = F(t,y(t)),\\
y(0)=y_0.
\end{cases}
\end{equation}
$$

If $F\in \mathcal{C}$ is $p$ times differentiable in $(t,y(t))$, then $y$ is $p+1$ times differentiable at $t$ and

$$
y^{(p+1)}(t)=F^{(p)}(t,y(t)),
$$

where $F^{(l)}$ are defined recursively as

$$
F^{(0)}(t,y)=F(t,y),\\
F^{(l+1)}(t,y)= \frac{\partial F^{(l)}}{\partial t}(t,y)+\frac{\partial F^{(l)}}{\partial y}(t,y).
$$


# Optimización media-varianza

La **teoría de portafolios** es una de los avances más importantes en las finanzas modernas e inversiones.
- Apareció por primera vez en un [artículo corto](https://www.math.ust.hk/~maykwok/courses/ma362/07F/markowitz_JF.pdf) llamado "Portfolio Selection" en la edición de Marzo de 1952 de "the Journal of Finance".
- Escrito por un desconocido estudiante de la Universidad de Chicago, llamado Harry Markowitz.
- Escrito corto (sólo 14 páginas), poco texto, fácil de entender, muchas gráficas y unas cuantas referencias.
- No se le prestó mucha atención hasta los 60s.

Finalmente, este trabajo se convirtió en una de las más grandes ideas en finanzas, y le dió a Markowitz el Premio Nobel casi 40 años después.
- Markowitz estaba incidentalmente interesado en los mercados de acciones e inversiones.
- Estaba más bien interesado en entender cómo las personas tomaban sus mejores decisiones cuando se enfrentaban con "trade-offs".
- Principio de conservación de la miseria. O, dirían los instructores de gimnasio: "no pain, no gain".
- Si queremos más de algo, tenemos que perder en algún otro lado.
- El estudio de este fenómeno era el que le atraía a Markowitz.

De manera que nadie se hace rico poniendo todo su dinero en la cuenta de ahorros. La única manera de esperar altos rendimientos es si se toma bastante riesgo. Sin embargo, riesgo significa también la posibilidad de perder, tanto como ganar.

Pero, ¿qué tanto riesgo es necesario?, y ¿hay alguna manera de minimizar el riesgo mientras se maximizan las ganancias?
- Markowitz básicamente cambió la manera en que los inversionistas pensamos acerca de esas preguntas.
- Alteró completamente la práctica de la administración de inversiones.
- Incluso el título de su artículo era innovador. Portafolio: una colección de activos en lugar de tener activos individuales.
- En ese tiempo, un portafolio se refería a una carpeta de piel.
- En el resto de este módulo, nos ocuparemos de la parte analítica de la teoría de portafolios, la cual puede ser resumida en dos frases:
 - No pain, no gain.
 - No ponga todo el blanquillo en una sola bolsa.

**Objetivos:**
- ¿Qué es la línea de asignación de capital?
- ¿Qué es el radio de Sharpe?
- ¿Cómo deberíamos asignar nuestro capital entre un activo riesgoso y un activo libre de riesgo?

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

## 1. Línea de asignación de capital

### 1.1. Motivación

El proceso de construcción de un portafolio tiene entonces los siguientes dos pasos:
1. Escoger un portafolio de activos riesgosos.
2. Decidir qué tanto de tu riqueza invertirás en el portafolio y qué tanto invertirás en activos libres de riesgo.

Al paso 2 lo llamamos **decisión de asignación de activos**.

Preguntas importantes:
1. ¿Qué es el portafolio óptimo de activos riesgosos?
 - ¿Cuál es el mejor portafolio de activos riesgosos?
 - Es un portafolio eficiente en media-varianza.
2. ¿Qué es la distribución óptima de activos?
 - ¿Cómo deberíamos distribuir nuestra riqueza entre el portafolo riesgoso óptimo y el activo libre de riesgo?
 - Concepto de **línea de asignación de capital**.
 - Concepto de **radio de Sharpe**.

Dos suposiciones importantes:
- Funciones de utilidad media-varianza.
- Inversionista averso al riesgo.

La idea sorprendente que saldrá de este análisis, es que cualquiera que sea la actitud del inversionista de cara al riesgo, el mejor portafolio de activos riesgosos es idéntico para todos los inversionistas.

Lo que nos importará a cada uno de nosotros en particular, es simplemente la desición óptima de asignación de activos.
___

### 1.2. Línea de asignación de capital

Sean:
- $r_s$ el rendimiento del activo riesgoso,
- $r_f$ el rendimiento libre de riesgo, y
- $w$ la fracción invertida en el activo riesgoso.

 Realizar deducción de la línea de asignación de capital en el tablero.

**Tres doritos después...**

#### Línea de asignación de capital (LAC):
$E[r_p]$ se relaciona con $\sigma_p$ de manera afín. Es decir, mediante la ecuación de una recta:

$$E[r_p]=r_f+\frac{E[r_s-r_f]}{\sigma_s}\sigma_p.$$

- La pendiente de la LAC es el radio de Sharpe $\frac{E[r_s-r_f]}{\sigma_s}=\frac{E[r_s]-r_f}{\sigma_s}$,
- el cual nos dice qué tanto rendimiento obtenemos por unidad de riesgo asumido en la tenencia del activo (portafolio) riesgoso.

Ahora, la pregunta es, ¿dónde sobre esta línea queremos estar?
___

### 1.3. Resolviendo para la asignación óptima de capital

Recapitulando de la clase pasada, tenemos las curvas de indiferencia: **queremos estar en la curva de indiferencia más alta posible, que sea tangente a la LAC**.

 Ver en el tablero.

Analíticamente, el problema es

$$\max_{w} \quad E[U(r_p)]\equiv\max_{w} \quad E[r_p]-\frac{1}{2}\gamma\sigma_p^2,$$

donde los puntos $(\sigma_p,E[r_p])$ se restringen a estar en la LAC, esto es $E[r_p]=r_f+\frac{E[r_s-r_f]}{\sigma_s}\sigma_p$ y $\sigma_p=w\sigma_s$. Entonces el problema anterior se puede escribir de la siguiente manera:

$$\max_{w} \quad r_f+wE[r_s-r_f]-\frac{1}{2}\gamma w^2\sigma_s^2.$$

 Encontrar la $w$ que maximiza la anterior expresión en el tablero.

**Tres doritos después...**

La solución es entonces:

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

De manera intuitiva:
- $w^\ast\propto E[r_s-r_f]$: a más exceso de rendimiento que se obtenga del activo riesgoso, más querremos invertir en él.
- $w^\ast\propto \frac{1}{\gamma}$: mientras más averso al riesgo seas, menos querrás invertir en el activo riesgoso.
- $w^\ast\propto \frac{1}{\sigma_s^2}$: mientras más riesgoso sea el activo, menos querrás invertir en él.
___

## 2. Ejemplo de asignación óptima de capital: acciones y billetes de EU

Pongamos algunos números con algunos datos, para ilustrar la derivación que acabamos de hacer.

En este caso, consideraremos:
- **Portafolio riesgoso**: mercado de acciones de EU (representados en algún índice de mercado como el S&P500).
- **Activo libre de riesgo**: billetes del departamento de tesorería de EU (T-bills).

Tenemos los siguientes datos:

$$E[r_{US}]=11.9\%,\quad \sigma_{US}=19.15\%, \quad r_f=1\%.$$

Recordamos que podemos escribir la expresión de la LAC como:

\begin{align}
E[r_p]&=r_f+\left[\frac{E[r_{US}-r_f]}{\sigma_{US}}\right]\sigma_p\\
      &=0.01+\text{S.R.}\sigma_p,
\end{align}

donde $\text{S.R}=\frac{0.119-0.01}{0.1915}\approx0.569$ es el radio de Sharpe (¿qué es lo que es esto?).

Grafiquemos la LAC con estos datos reales:

```python
# Importamos librerías que vamos a utilizar
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
```

```python
# Datos
Erus, rf, sus = 0.119, 0.01, 0.1915
# Radio de Sharpe para este activo
SR = (Erus-rf)/sus
# Vector de volatilidades del portafolio (sugerido: 0% a 50%)
sp = np.linspace(0, 0.5, 100)
# LAC
Erp = rf + SR*sp
```

```python
# Gráfica
plt.figure(figsize=(8,6))
plt.plot(sp, Erp, 'g', lw = 2, label='LAC')
plt.plot(0, rf, 'ob', ms = 5, label='Activo libre de riesgo')
plt.plot(sus, Erus, 'or', ms = 5, label='Portafolio riesgoso')
plt.axhline(y=0, color='k')
plt.axvline(x=0, color='k')
plt.grid()
plt.xlabel('Volatilidad del portafolio $\sigma_p$')
plt.ylabel('Rendimiento esperado del portafolio $E[r_p]$')
```

Bueno, y ¿en qué punto de esta línea querríamos estar?
- Pues ya vimos que depende de tus preferencias.
- En particular, de tu actitud de cara al riesgo, medido por tu coeficiente de aversión al riesgo.

Solución al problema de asignación óptima de capital:

$$\max_{w} \quad E[U(r_p)]$$

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

Dado que ya tenemos datos, podemos intentar para varios coeficientes de aversión al riesgo:

```python
# importar pandas
import pandas as pd
```

```python
# Crear un DataFrame con los pesos, rendimiento
# esperado y volatilidad del portafolio óptimo
# entre los activos riesgoso y libre de riesgo
# cuyo índice sean los coeficientes de aversión
# al riesgo del 1 al 10 (enteros)
g = np.arange(1, 11, 1)
data = pd.DataFrame(index = g, columns=['$w*$', '$E[r_p]$', '$s_p$'])
data.index.name = '$\gamma$'
data['$w*$'] = (Erus-rf)/(g*sus**2)
data['$E[r_p]$'] = data['$w*$']*Erus+(1-data['$w*$'])*rf
data['$s_p$'] = data['$w*$']*sus

data
```

      $w*$
      $E[r_p]$
      $s_p$

      $\gamma$

      1
      2.972275
      0.333978
      0.569191

      2
      1.486137
      0.171989
      0.284595

      3
      0.990758
      0.117993
      0.189730

      4
      0.743069
      0.090994
      0.142298

      5
      0.594455
      0.074796
      0.113838

      6
      0.495379
      0.063996
      0.094865

      7
      0.424611
      0.056283
      0.081313

      8
      0.371534
      0.050497
      0.071149

      9
      0.330253
      0.045998
      0.063243

      10
      0.297227
      0.042398
      0.056919

¿Cómo se interpreta $w^\ast>1$?
- Cuando $0<w^\ast<1$, entonces $0<1-w^\ast<1$. Lo cual implica posiciones largas en el mercado de activos y en el activo libre de riesgo.
- Por el contrario, cuando $w^\ast>1$, tenemos $1-w^\ast<0$. Lo anterior implica una posición corta en el activo libre de riesgo (suponiendo que se puede) y una posición larga (de más del 100%) en el mercado de activos: apalancamiento.

# Anuncios parroquiales.

## 1. Quiz la siguiente clase.
## 2. [Calificaciones](https://docs.google.com/spreadsheets/d/1mSzxoIHPFx99Znw9EE8xQz05Azq1QvzDURPbQQSGLoA/edit?usp=sharing)
## 3. [Encuesta](http://cursos.iteso.mx/course/view.php?id=1480)

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


# Случайни ефекти върху генетичната структура на популации

***

Изследвани са основни популационни модели върху генетичната структура на популации, както са представени в курса във ФМИ ["Въведение в изчислителната биология", доц. П. Рашков (ИМИ-БАН)](http://www.math.bas.bg/nummeth/rashkov/teaching/), 2018-2019г
***

## Стохастичен модел за популация с два фенотипа

Разглеждаме популация от белокрили и чернокрили молци.

Цветът на крилата на индивид от популацията се определя от локус с два алела. Означаваме ги с **W** и **w**.

Когато локусът е:
- **WW** или **Ww** - молецът е белокрил
- **ww** - молецът е чернокрил

#### Закон на Харди-Вайнберг

>В идеална популация съотношението между алелите остава непроменено с течение на поколенията.

Под идеална се имат предвид следните условия (за един локус с 2 алела):
- Поколенията са неприпокриващи се
- Чифтосването между индивидите е на случаен принцип
- Всички са еднакво способни да оцелеят
- Няма мутации
- Честотата на даден алел е еднаква за цялата популация

#### Подобрен модел

Нека бележим с $\alpha$ делът на белокрилите молци (WW или Ww), които оцеляват. А, с $\gamma$ делът на чернокрилите (ww), които оцеляват.

Очевидно, ако $\alpha > \gamma$, белокрилите имат предимство. Обратно, ако $\gamma > \alpha$ чернокрилите са повече.

Нека $p_n = \frac{\text{бр. W-алели}}{\text{бр. всички алели}}$

За да изследваме генотипа на бъдещите поколения, ще използваме [квадрата на Пънет](https://en.wikipedia.org/wiki/Punnett_square):

            Женски

            W
            w

            Мъжки
            W
            $\alpha p_n^2$
            $\alpha p_n (1 - p_n)$

            w
            $\alpha p_n (1 - p_n)$
            $\gamma (1 - p_n)^2$

От тук следва, че вероятностите за генотипа на потомството са:
- **WW**: $\alpha p_n^2$
- **Ww**: $2 \alpha p_n (1 - p_n)$
- **ww**: $\gamma (1 - p_n)^2$

Тогава за $p_{n+1}$ получаваме:
\begin{equation}
p_{n+1} = \frac{\alpha p_n^2 + 2 \alpha p_n (1 - p_n) \frac{1}{2}}{\alpha p_n^2 + 2 \alpha p_n (1 - p_n) + \gamma (1 - p_n)^2} \Leftrightarrow p_{n+1} = \frac{\alpha p_n}{p_n^2(\gamma - \alpha) - 2 p_n (\gamma - \alpha) + \gamma}
\end{equation}

- Ако $\alpha = \gamma$ => Закон на Харди-Вайнберг (съотношението между алелите остава непроменено)
- Ако $\alpha \neq \gamma$, търсим равновесни точки. Можем да ги видим и на графиката:

```python
import math
import matplotlib.pyplot as plt

from numpy import *
```

```python
def f(pn, alpha=0.5, gamma=0.7):
    return (alpha * pn) / (pn**2 * (gamma - alpha) - 2 * pn * (gamma - alpha) + gamma)
```

```python
def show_balance_points(initial, **kwargs):
    previous = initial
    for i in range(50):
        current = f(previous, **kwargs)
        plt.plot(previous, current, 'bo')
        previous = current

    plt.xlabel("p_n")
    plt.ylabel("p_n+1")
    plt.show()
```

Когато $\alpha > \gamma$, $p_n \rightarrow 1$ и следователно белокрилите имат предимство:

```python
show_balance_points(0.3, alpha=0.7, gamma=0.3)
```

Когато $\alpha > \gamma$, $p_n \rightarrow 0$ и следователно чернокрилите имат предимство:

```python
show_balance_points(0.3, alpha=0.4, gamma=0.6)
```

## Стохастични модели за генетичния дрейф

> **Генетичният дрейф** представлява промяна в честотата на гените (алелите), която е случайна и няма приспособителен
характер.

>Генетичната структура на природните популации е **силно зависими от случайния генетичен дрейф**, тъй като случайните
ефекти могат:
>- да унищожат генетичното разнообразие, изградено чрез мутация
>- да противодействат на естествения отбор,
>- да изградят статистически асоциации между различни локуси в генома

>Затова изясняването на повечето въпроси в еволюционната биология изисква да се вземат под внимание случайните ефекти.

>Значението на случайните ефекти **зависи от много параметри на модела** (размера на популацията или скоростта на мутация).

>В това упражнение ще изведем прости случайни модели, за да разграничим ролята различните фактори и за да получим по-ясна представа за генетичния дрейф.

### Основен модел

В основния модел разглеждаме само един локус с алели A и a.

Двата алела имат честоти fA и fa = 1 − fA .

За простота предполагаме, че поколенията са дискретни и във всяко има мутация и естествен отбор.
По принцип трябва да симулираме мутацията и естествения отбор като случайни процеси.

За да опростим модела, разглеждаме приближение, в което:
1. мутацията и отборът са едновременно детерминистични,
2. крайният размер на популацията се определя чрез вземане„

на извадка от N генома (N е размерът на популацията) след като са се случили мутацията и естественият отбор.

```python
def model(population_size=1000, mutation_rate=0.0001, selection_strenght=0, generations=5000, initial_frequency=0.1, sampling=True):
    frequencies = []
    previous_frequency = initial_frequency
    for i in range(1, generations):
        # Mutation:
        frequency = mutation_rate * (1 - previous_frequency) + (1 - mutation_rate) * previous_frequency # m(1 − fA) + (1 − m)fA
        # Selection:
        average_adaptability = (1 - selection_strenght) * frequency + (1 - frequency)
        frequency = frequency * (1 - selection_strenght) / average_adaptability
        # Sampling:
        if sampling:
            frequency = random.binomial(population_size, frequency) / population_size
        frequencies.append(frequency)
        previous_frequency = frequency

    plt.plot(frequencies)
    plt.show()
```

```python
model(population_size=1000, generations=5000, initial_frequency=0.1)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1)
```

```python
model(population_size=10000000, generations=5000, initial_frequency=0.1)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1, sampling=False)
```

```python
model(population_size=10000, generations=500, initial_frequency=0.1, sampling=False)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.6, sampling=False)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.6, sampling=True)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.5, sampling=False)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.5, sampling=True)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1, sampling=False, mutation_rate=0.03)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1, sampling=True, mutation_rate=0.03)
```

```python
model(population_size=10000000, generations=5000, initial_frequency=0.1, sampling=True, mutation_rate=0.03)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1, mutation_rate=0.03, selection_strenght=0.09)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1, mutation_rate=0.01, selection_strenght=0.01, sampling=False)
```

```python
model(population_size=10000, generations=5000, initial_frequency=0.1, mutation_rate=0.01, selection_strenght=0.09, sampling=False)
```

### Разширен модел (кръстосване)

```python
DEFAULT_FREQUENCIES = {'ab': 0.1, 'Ab': 0.25, 'aB': 0.25, 'AB': 0.4}

def _opposite_allel(allel):
    if ord(allel) < 97:
        return allel.lower()
    return allel.upper()

def _different_allel(locus, diff_index):
    new_locus = ''
    for i, allel in enumerate(locus):
        if (i + 1) in diff_index:
            new_locus += _opposite_allel(allel)
        else:
            new_locus += allel
    return new_locus

def _mutate(frequencies, m): # m - mutation_rate
    new_frequencies = dict()
    for locus, freq in frequencies.items():
        new_frequencies[locus] = ((1 - m)**2)*freq +\
                                 (1 - m) * m * (frequencies[_different_allel(locus, (1,))] + frequencies[_different_allel(locus, (2,))]) +\
                                 (m**2)*frequencies[_different_allel(locus, (1,2))]
    return new_frequencies

def _select(f, s): # f - frequencies, s - selection strengths (dicts per locus type)
    new_frequencies = dict()
    average_adaptability = sum([s[locus] * f[locus] for locus in f])
    for locus in f:
        new_frequencies[locus] = f[locus] * s[locus] / average_adaptability
    return new_frequencies

def _recombinate(f, r): # f - frequencies, r - recombination rate
    D = f['ab'] * f['AB'] - f['Ab'] * f['aB'] # коефициентът на неравновесие на свързване на гените
    new_frequencies = dict()
    new_frequencies['ab'] = f['ab'] - r * D
    new_frequencies['AB'] = f['AB'] - r * D
    new_frequencies['Ab'] = f['Ab'] + r * D
    new_frequencies['aB'] = f['aB'] + r * D
    return new_frequencies

def model_two_loci(
        population_size=1000, mutation_rate=0.0001, selection_strengths=None,
        generations=5000, initial_frequencies=None, recombination_rate=0.1):
    initial_frequencies = initial_frequencies or DEFAULT_FREQUENCIES
    assert sum(list(initial_frequencies.values())) == 1.0, "All frequencies must equal 1.0"
    frequencies_values = list()
    previous_frequencies = initial_frequencies
    for i in range(1, generations):
        # Mutation:
        frequencies = _mutate(previous_frequencies, mutation_rate)
        # Selection:
        frequencies = _select(frequencies, selection_strengths)
        # Recombination:
        frequencies = _recombinate(frequencies, recombination_rate)
        # Sampling:
        freqs = sorted(frequencies.items())
        freqs_values = [x[1] for x in freqs]
        freqs_values = [x / population_size for x in random.multinomial(population_size, freqs_values)]
        frequencies_values.append(freqs_values)
        frequencies = dict(zip([x[0] for x in freqs], freqs_values))
        previous_frequencies = frequencies

    graph = plt.plot(frequencies_values)
    first_legend = plt.legend(graph, sorted(previous_frequencies))
    plt.ylabel("frequency")
    plt.xlabel("generation")
    plt.rcParams["figure.figsize"] = 10, 8
    plt.show()
```

```python
model_two_loci(
    initial_frequencies={'ab': 1, 'Ab': 0, 'aB': 0, 'AB': 0},
    selection_strengths={'ab': 1, 'Ab': 1.01, 'aB': 1.01, 'AB': 1.02},
    population_size=10000, generations=1000, recombination_rate=0.1)
```

```python
model_two_loci(
    initial_frequencies={'ab': 1, 'Ab': 0, 'aB': 0, 'AB': 0},
    selection_strengths={'ab': 1, 'Ab': 1.01, 'aB': 1.01, 'AB': 1.02},
    population_size=10000, generations=1000, recombination_rate=0.4)
```

```python

```

Stochastic Population Genetics Basic Models.ipynb


# Applied Stochastic Processes HW_15

**11510691 程$
% color
% Aquamarine, black, blue, brown, cyan, darkgray, gray, green, lightgray, lime, magenta, olive, orange, pink, purple, red, teal, violet, white, yellow
\DeclareMathOperator*{\argmin}{argmin}
\DeclareMathOperator*{\argmax}{argmax}
\DeclareMathOperator*{\plim}{plim}
\newcommand{\ffrac}{\displaystyle \frac}
\newcommand{\d}[1]{\displaystyle{#1}}
\newcommand{\space}{\text{ }}
\newcommand{\bspace}{\;\;\;\;}
\newcommand{\bbspace}{\;\;\;\;\;\;\;\;}
\newcommand{\QQQ}{\boxed{?\:}}
\newcommand{\void}{\left.\right.}
\newcommand{\Tran}[1]{{#1}^{\mathrm{T}}}
\newcommand{\CB}[1]{\left\{ #1 \right\}}
\newcommand{\SB}[1]{\left[ #1 \right]}
\newcommand{\P}[1]{\left( #1 \right)}
\newcommand{\abs}[1]{\left| #1 \right|}
\newcommand{\norm}[1]{\left\| #1 \right\|}
\newcommand{\given}[1]{\left. #1 \right|}
\newcommand{\using}[1]{\stackrel{\mathrm{#1}}{=}}
\newcommand{\asim}{\overset{\text{a}}{\sim}}
\newcommand{\RR}{\mathbb{R}}
\newcommand{\EE}{\mathbb{E}}
\newcommand{\II}{\mathbb{I}}
\newcommand{\NN}{\mathbb{N}}
\newcommand{\ZZ}{\mathbb{Z}}
\newcommand{\QQ}{\mathbb{Q}}
\newcommand{\PP}{\mathbb{P}}
\newcommand{\AcA}{\mathcal{A}}
\newcommand{\FcF}{\mathcal{F}}
\newcommand{\AsA}{\mathscr{A}}
\newcommand{\FsF}{\mathscr{F}}
\newcommand{\dd}{\mathrm{d}}
\newcommand{\I}[1]{\mathrm{I}\left( #1 \right)}
\newcommand{\N}[1]{\mathcal{N}\left( #1 \right)}
\newcommand{\Exp}[1]{\mathrm{E}\left[ #1 \right]}
\newcommand{\Var}[1]{\mathrm{Var}\left[ #1 \right]}
\newcommand{\Avar}[1]{\mathrm{Avar}\left[ #1 \right]}
\newcommand{\Cov}[1]{\mathrm{Cov}\left( #1 \right)}
\newcommand{\Corr}[1]{\mathrm{Corr}\left( #1 \right)}
\newcommand{\ExpH}{\mathrm{E}}
\newcommand{\VarH}{\mathrm{Var}}
\newcommand{\AVarH}{\mathrm{Avar}}
\newcommand{\CovH}{\mathrm{Cov}}
\newcommand{\CorrH}{\mathrm{Corr}}
\newcommand{\ow}{\text{otherwise}}
\newcommand{\wp}{\text{with probability }}
\newcommand{\FSD}{\text{FSD}}
\newcommand{\SSD}{\text{SSD}}$远星$^\dagger$**

## Question 1
$\P 1$

$\bspace$First $\Exp{V} = 0 + 0 = 0$, however its variance is

$$\begin{align}
\Var{V\P t}&= \Var{B\P t} + \Var{B\P t^2} + 2\Cov{B\P t, B\P{t^2}}\\
&= t + t^2 + 2\min\P{t,t^2}
\end{align}$$

$\bspace$For $X\sim V\P t$, its pdf is $\ffrac{1}{\sqrt{2\pi\Var{V\P t}}}\exp\CB{-\ffrac{x^2}{2\Var{V\P t}}}$, not a standart Brownian Motion.

$\P 2$

$\bspace$Firstly, $Y\P 0 = -B\P 0 = 0$ and then about the increments, $Y\P t-Y\P s = B\P s - B\P t $, thus $\CB{Y\P t,t\geq0}$ also has independent and stationary increments. And for every $t>0$, $Y\P t = -B\P t$, thus normally distributed with mean $\Exp{Y\P t} = -\Exp{B\P t} = 0$ and variance $\Var{Y\P t} = \Var{B\P t} = t$, which also implies that $\sigma = 1$, standard.

## Question 2

$\bspace$We can write $X\P t$ using the given information as $X\P t = -4+2t+3B\P t$.

$\P 1$

$$\begin{align}
P\CB{X\P 5 < 10} &= P\CB{-4+2\cdot 5+3B\P 5 < 10}\\
&= P\CB{B\P 5 < \ffrac{4}{3}}\\
&= \Phi\P{\ffrac{4}{3\sqrt 5}} = 0.7245075062074533
\end{align}$$

$\P 2$

$$\begin{align}
P\CB{X\P 5 < 10 \mid X \P 3 = 5} &= P\CB{-4+2\cdot 5+3B\P 5 < 10 \mid -4+2 \cdot 3+3B\P 3 = 5}\\
&= P\CB{B\P 5 < \ffrac{4}{3} \mid B\P 3 = 1}\\
&= P\CB{B\P 5 - B\P 3 < \ffrac{1}{3} \mid B\P 3 = 1}\\
&= P\CB{B\P 2 < \ffrac{1}{3}}\\
&= \Phi\P{\ffrac{1}{3\sqrt 2}} = 0.59316814211660407
\end{align}$$

$\P 3$

$$\begin{align}
P\CB{e^{X\P 3} < 20 \mid X\P 2 = 0}&= P\CB{-4+2\cdot3+3B\P 3 < \ln\P{20} \mid -4  + 2 \cdot 2 + 3B\P 2 = 0}\\
&= P\CB{B\P 3 < \ffrac{\ln\P{20} - 2}{3} \mid B\P 2 = 0}\\
&= P\CB{B\P 3 - B\P 2 < \ffrac{\ln\P{20} - 2}{3} - 0}\\
&= \Phi\P{\ffrac{\ln\P{20} - 2}{3\sqrt1}} = 0.6300216765658242
\end{align}$$

## Question 3
$\P 1$

$\bspace$Now $X\P t = X\P s + X\P t - X\P s = c + X\P{t-s} \sim \N{c+\mu\P{t-s},\sigma^2\P{t-s}}$

$\P 2$

$\bspace$Since for $s< t$, $\Exp{B\P s \mid B\P t = B} = \ffrac{s}{t}B$ and $\Var{B\P s \mid B\P t = B} = \ffrac{s}{t}\P{t-s}$, $X\P t = \mu t + \sigma B\P t$ and thus for $t<s$

$$\Exp{X\P t \mid  X\P s = c} = \Exp{\sigma B\P t \mid B\P s = \ffrac{c - \mu s}{\sigma}} + \mu t = \ffrac{\P{c - \mu s}t}{s} + \mu t \\
\Var{X\P t \mid  X\P s = c} = \sigma^2 \Var{B\P t \mid B\P s = \ffrac{c - \mu s}{\sigma}} = \ffrac{\sigma^2t\P{s-t}}{s}$$

## Question 4

$$\begin{align}
\ffrac{\partial p}{\partial t} &= \ffrac{1}{\sqrt{2\pi}\sigma}\P{-\ffrac{1}{2\sqrt{t^3}}\exp\CB{-\ffrac{\P{ y-x-\mu t}^2}{2\sigma^2t}}+\ffrac{1}{\sqrt t}\P{-\ffrac{1}{2\sigma^2}}\P{\mu^2 - \ffrac{\P{y-x}^2}{t^2}}\exp\CB{-\ffrac{\P{ y-x-\mu t}^2}{2\sigma^2t}}}\\
&= \ffrac{-1}{\sqrt{8\pi t}\sigma}\P{\ffrac{1}{t} + \ffrac{\mu^2 - \P{y-x}^2/t^2}{\sigma^2}}\exp\CB{-\ffrac{\P{ y-x-\mu t}^2}{2\sigma^2t}}\\
\ffrac{\partial p}{\partial x} &= \ffrac{1}{\sqrt{2\pi t} \sigma} \ffrac{2\P{ y-x-\mu t}}{2\sigma^2t}\exp\CB{-\ffrac{\P{ y-x-\mu t}^2}{2\sigma^2t}} = \ffrac{2\P{ y-x-\mu t}}{\sqrt{8\pi t^3} \sigma^3}\exp\CB{-\ffrac{\P{ y-x-\mu t}^2}{2\sigma^2t}}\\
\ffrac{\partial^2 p}{\partial x^2} &= \ffrac{1}{\sqrt{8\pi t^3} \sigma^3}\P{-2 \exp\CB{-\ffrac{\P{ y-x-\mu t}^2}{2\sigma^2t}} + \ffrac{4\P{ y-x-\mu t}^2}{2\sigma^2t}\exp\CB{-\ffrac{\P{ y-x-\mu t}^2}{2\sigma^2t}}}
\end{align}$$

$\bspace$Ignoring the exponential part, we now are required to prove

$$
\ffrac{-1}{\sqrt{8\pi t}\sigma}\P{\ffrac{1}{t} + \ffrac{\mu^2 - \P{y-x}^2/t^2}{\sigma^2}} = \ffrac{1}{2}\sigma^2 \ffrac{1}{\sqrt{8\pi t^3} \sigma^3}\P{-2 + \ffrac{4\P{ y-x-\mu t}^2}{2\sigma^2t}} + \mu \ffrac{2\P{ y-x-\mu t}}{\sqrt{8\pi t^3} \sigma^3}$$

$$\begin{align}
\text{RHS}&= \ffrac{-1}{\sqrt{8\pi t^3}\sigma} + \ffrac{1}{\sqrt{8\pi t^3}\sigma^3}\P{\ffrac{\P{ y-x-\mu t}^2}{t} + 2\mu\P{ y-x-\mu t}}\\
&= \ffrac{-1}{\sqrt{8\pi t}\sigma}\ffrac{1}{t} + \ffrac{1}{\sqrt{8\pi t}\sigma}\ffrac{\P{ y-x-\mu t}^2 + 2\mu t\P{ y-x-\mu t}}{t^2\sigma^2}\\
&= \ffrac{-1}{\sqrt{8\pi t}\sigma}\ffrac{1}{t} + \ffrac{1}{\sqrt{8\pi t}\sigma}\ffrac{\P{y-x}^2 - \mu^2t^2}{t^2\sigma^2} = \text{LHS}
\end{align}$$

$\bspace$And the other one is true since $\ffrac{\partial p}{\partial x} = -\ffrac{\partial p}{\partial y}$ and $\ffrac{\partial^2 p}{\partial x^2} = \ffrac{\partial^2 p}{\partial y^2}$

## Question 5
$\P 1$

$$\begin{align}
\Exp{B\P t \mid B\P u, 0 \leq u \leq s} &= \Exp{B\P t - B\P s + B\P s \mid B\P u, 0 \leq u \leq s}\\
&= B\P s + \Exp{B\P t - B\P s \mid B\P u, 0 \leq u \leq s}\\
&= B\P s + \Exp{B\P{t-s}} = B\P s
\end{align}$$

$\P 2$

$$\begin{align}
\Exp{B^2\P t -t\mid B^2\P u - u, 0 \leq u \leq s} &= \Exp{B^2\P t\mid B\P u, 0 \leq u \leq s} -t\\
&= \Exp{\P{B\P t - B\P s}^2 + B^2\P s + 2B\P s \P{B\P t - B\P s}\mid B\P u, 0 \leq u \leq s} -t\\
&= \Exp{\P{B\P t - B\P s}^2} + B^2\P s + 2B\P s\Exp{\P{B\P t - B\P s}} - t\\
&= t-s + B^2\P s + 0 - t = B^2\P s - s
\end{align}$$

$\P 3$

$$\begin{align}
\Exp{\exp\CB{cB\P t - c^2t/2} \mid \exp\CB{cB\P u - c^2u/2}, 0 \leq u \leq s} &= e^{ - c^2t/2}\Exp{\exp\CB{cB\P t} \mid B\P u, 0 \leq u \leq s}\\
&= \exp\CB{cB\P s - c^2t/2} \Exp{\exp\CB{c\P{B\P t - B\P s}}}\\
&= \exp\CB{cB\P s - c^2t/2} \Exp{\exp\CB{cB\P{t-s}}}\\
&= \exp\CB{cB\P s - c^2t/2}\exp\CB{c^2\P{t-s}/2}\\
&= \exp\CB{cB\P s - c^2s/2}
\end{align}$$

***
$\\[50em]$

```python
import scipy.stats as scs
import math
```

```python
scs.norm.cdf((math.log(20) - 2)/3)
```

    0.6300216765658242

```python
4/(3*math.sqrt(5))
```

    0.5962847939999439

Probability and Statistics/Applied Random Process/HW/HW_15.ipynb


# Prediction of hardness based on chemical formula and load (state)

This notebooks applies MODNet on [a hardness dataset extracted from literature](https://github.com/ziyan1996/VickersHardnessPrediction). It is a good example on how MODNet can be used for a composition-only task
with state variables such as temperature or pressure or in this case the applied load
(N) during hardness testing.

The citation for hardness data used is as follows:
> Zhang. Z, Tehrani. A.M., Oliynyk. A.O., Day. B and Brgoch. J, Finding Superhard Materials through Ensemble Learning, Adv. Mater. 2020, 33, 2005112.

## Dependencies

```python
!pip install ml-matrics
```

    Requirement already satisfied: ml-matrics in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (0.2.5)
    Requirement already satisfied: scipy in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ml-matrics) (1.7.3)
    Requirement already satisfied: pandas in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ml-matrics) (1.1.5)
    Requirement already satisfied: scikit-learn in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ml-matrics) (0.23.2)
    Requirement already satisfied: plotly in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ml-matrics) (5.5.0)
    Requirement already satisfied: pymatgen in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ml-matrics) (2020.8.13)
    Requirement already satisfied: numpy>=1.21.0 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ml-matrics) (1.22.0)
    Requirement already satisfied: matplotlib in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ml-matrics) (3.5.1)
    Requirement already satisfied: fonttools>=4.22.0 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from matplotlib->ml-matrics) (4.28.5)
    Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from matplotlib->ml-matrics) (1.3.2)
    Requirement already satisfied: pillow>=6.2.0 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from matplotlib->ml-matrics) (9.0.0)
    Requirement already satisfied: pyparsing>=2.2.1 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from matplotlib->ml-matrics) (3.0.6)
    Requirement already satisfied: cycler>=0.10 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from matplotlib->ml-matrics) (0.11.0)
    Requirement already satisfied: python-dateutil>=2.7 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from matplotlib->ml-matrics) (2.8.2)
    Requirement already satisfied: packaging>=20.0 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from matplotlib->ml-matrics) (21.3)
    Requirement already satisfied: six>=1.5 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from python-dateutil>=2.7->matplotlib->ml-matrics) (1.16.0)
    Requirement already satisfied: pytz>=2017.2 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pandas->ml-matrics) (2021.3)
    Requirement already satisfied: tenacity>=6.2.0 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from plotly->ml-matrics) (8.0.1)
    Requirement already satisfied: ruamel.yaml>=0.15.6 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (0.17.20)
    Requirement already satisfied: requests in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (2.27.1)
    Requirement already satisfied: palettable>=3.1.1 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (3.3.0)
    Requirement already satisfied: tabulate in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (0.8.9)
    Requirement already satisfied: monty>=3.0.2 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (2021.12.1)
    Requirement already satisfied: networkx>=2.2 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (2.6.3)
    Requirement already satisfied: sympy in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (1.9)
    Requirement already satisfied: spglib>=1.9.9.44 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from pymatgen->ml-matrics) (1.16.3)
    Requirement already satisfied: ruamel.yaml.clib>=0.2.6 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from ruamel.yaml>=0.15.6->pymatgen->ml-matrics) (0.2.6)
    Requirement already satisfied: idna<4,>=2.5 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from requests->pymatgen->ml-matrics) (3.3)
    Requirement already satisfied: certifi>=2017.4.17 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from requests->pymatgen->ml-matrics) (2021.10.8)
    Requirement already satisfied: urllib3<1.27,>=1.21.1 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from requests->pymatgen->ml-matrics) (1.26.7)
    Requirement already satisfied: charset-normalizer~=2.0.0 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from requests->pymatgen->ml-matrics) (2.0.10)
    Requirement already satisfied: joblib>=0.11 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from scikit-learn->ml-matrics) (1.1.0)
    Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from scikit-learn->ml-matrics) (3.0.0)
    Requirement already satisfied: mpmath>=0.19 in c:\users\sterg\miniconda3\envs\modnet\lib\site-packages (from sympy->pymatgen->ml-matrics) (1.2.1)

```python
from os.path import join
import numpy as np
import pandas as pd
from modnet.models import MODNetModel
from modnet.preprocessing import MODData
import matplotlib.pyplot as plt
from ml_matrics.parity import density_scatter, density_hexbin
from pymatgen.core import Composition
```

The Zhang et al. hardness dataset contains 1062 individual Vickers hardness measurements
at various loads.

```python
df = pd.read_csv(join("data","hv_comp_load","hv_comp_load.csv"))
# df = df.iloc[0:100] # uncomment if you only want to use a small subset of the data
df["composition"] = df["composition"].map(Composition) # maps composition to a pymatgen composition object
target_name = "hardness"
state_name = "load"
df.head()
```

      composition
      hardness
      load

      0
      (Ag, Gd, Pd)
      1.810
      0.49

      1
      (Ag, Y, Pd)
      1.640
      0.49

      2
      (Ag, Pb, Sb, Te)
      0.578
      2.94

      3
      (Al, Si, N, O)
      15.030
      0.98

      4
      (Al, B)
      23.800
      2.00

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

      hardness
      load

      count
      1062.000000
      1062.000000

      mean
      25.011255
      2.334731

      std
      13.567154
      3.613743

      min
      0.250000
      0.001500

      25%
      11.305000
      0.590000

      50%
      28.448737
      0.980000

      75%
      34.266283
      2.940000

      max
      92.000000
      49.030000

We can view the distribution of the data (load and hardness values) via a hexagonally
binned scatter plot, or "hexbin".

```python
fig, ax = plt.subplots(facecolor="w")
hb = ax.hexbin(df[target_name], df[state_name], bins="log")
plt.ylabel(state_name)
plt.xlabel(target_name)
ax.set_box_aspect(1)
cb = fig.colorbar(hb, ax=ax)
cb.set_label('counts')
```

Or via a more traditional x-y scatter plot:

```python
fig, ax = plt.subplots(facecolor="w")
ax.scatter(df[target_name], df[state_name],s=1)
plt.ylabel(state_name)
plt.xlabel(target_name)
ax.set_box_aspect(1)
```

## MODData

```python
# This instantiates the MODData
data = MODData(
    materials=df["composition"], # you can provide composition objects to MODData
    targets=df[target_name],
    target_names=[target_name]
)
```

    2022-01-07 01:50:52,663 - modnet - INFO - Loaded CompositionOnlyFeaturizer featurizer.

```python
# Featurization of the moddata
# It will automatically apply composition only featurizers
data.featurize()
```

    2022-01-07 01:50:53,334 - modnet - INFO - Computing features, this can take time...
    2022-01-07 01:50:53,335 - modnet - INFO - Applying composition featurizers...
    2022-01-07 01:50:53,339 - modnet - INFO - Applying featurizers (AtomicOrbitals(), AtomicPackingEfficiency(), BandCenter(), ElementFraction(), ElementProperty(data_source=<matminer.utils.data.MagpieData object at 0x0000023EC43062E0>,
                    features=['Number', 'MendeleevNumber', 'AtomicWeight',
                              'MeltingT', 'Column', 'Row', 'CovalentRadius',
                              'Electronegativity', 'NsValence', 'NpValence',
                              'NdValence', 'NfValence', 'NValence', 'NsUnfilled',
                              'NpUnfilled', 'NdUnfilled', 'NfUnfilled', 'NUnfilled',
                              'GSvolume_pa', 'GSbandgap', 'GSmagmom',
                              'SpaceGroupNumber'],
                    stats=['minimum', 'maximum', 'range', 'mean', 'avg_dev',
                           'mode']), IonProperty(), Miedema(ss_types=['min'], struct_types=['inter', 'amor', 'ss']), Stoichiometry(), TMetalFraction(), ValenceOrbital(), YangSolidSolution()) to column 'composition'.

    MultipleFeaturizer: 100%|██████████| 1062/1062 [00:17<00:00, 59.97it/s]

    2022-01-07 01:51:19,034 - modnet - INFO - Data has successfully been featurized!

### add hardness data post-hoc

```python
data.df_featurized[state_name] = df[state_name].values
```

```python
data.df_featurized
```

      AtomicOrbitals|HOMO_character
      AtomicOrbitals|HOMO_element
      AtomicOrbitals|HOMO_energy
      AtomicOrbitals|LUMO_character
      AtomicOrbitals|LUMO_element
      AtomicOrbitals|LUMO_energy
      AtomicOrbitals|gap_AO
      AtomicPackingEfficiency|mean simul. packing efficiency
      AtomicPackingEfficiency|mean abs simul. packing efficiency
      AtomicPackingEfficiency|dist from 1 clusters |APE| < 0.010
      ...
      ValenceOrbital|avg p valence electrons
      ValenceOrbital|avg d valence electrons
      ValenceOrbital|avg f valence electrons
      ValenceOrbital|frac s valence electrons
      ValenceOrbital|frac p valence electrons
      ValenceOrbital|frac d valence electrons
      ValenceOrbital|frac f valence electrons
      YangSolidSolution|Yang omega
      YangSolidSolution|Yang delta
      load

      id

      id0
      3
      46
      -0.160771
      3
      46
      -0.160771
      0.000000
      0.011270
      0.013120
      0.016120
      ...
      0.000000
      9.568000
      0.336000
      0.014527
      0.000000
      0.952040
      0.033433
      3.587849e-01
      0.050501
      0.49

      id1
      1
      39
      -0.150727
      1
      39
      -0.150727
      0.000000
      0.011100
      0.011407
      0.016120
      ...
      0.000000
      9.568000
      0.000000
      0.015030
      0.000000
      0.984970
      0.000000
      3.608379e-01
      0.054901
      0.49

      id2
      2
      52
      -0.226594
      2
      51
      -0.185623
      0.040971
      0.002165
      0.025647
      0.061237
      ...
      2.875000
      10.000000
      3.500000
      0.102740
      0.157534
      0.547945
      0.191781
      7.854369e+06
      0.093935
      2.94

      id3
      2
      7
      -0.266297
      2
      7
      -0.266297
      0.000000
      -0.017054
      0.023132
      0.010758
      ...
      2.571429
      0.000000
      0.000000
      0.437500
      0.562500
      0.000000
      0.000000
      6.726751e+06
      0.320641
      0.98

      id4
      2
      5
      -0.136603
      2
      5
      -0.136603
      0.000000
      -0.016771
      0.016771
      0.023197
      ...
      1.000000
      0.000000
      0.000000
      0.666667
      0.333333
      0.000000
      0.000000
      4.767616e+06
      0.147942
      2.00

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      id1057
      3
      23
      -0.204634
      3
      23
      -0.204634
      0.000000
      -0.023942
      0.023942
      0.029463
      ...
      0.000000
      2.666667
      0.000000
      0.428571
      0.000000
      0.571429
      0.000000
      3.221823e+00
      0.079674
      0.98

      id1058
      3
      74
      -0.220603
      3
      74
      -0.220603
      0.000000
      -0.023617
      0.023617
      0.029463
      ...
      0.000000
      3.333333
      9.333333
      0.136364
      0.000000
      0.227273
      0.636364
      2.098711e+00
      0.075997
      0.98

      id1059
      3
      76
      -0.296791
      1
      76
      -0.191489
      0.105302
      -0.029887
      0.029887
      0.049622
      ...
      0.666667
      2.000000
      4.666667
      0.214286
      0.071429
      0.214286
      0.500000
      6.616627e-01
      0.213724
      0.25

      id1060
      3
      76
      -0.296791
      1
      76
      -0.191489
      0.105302
      -0.029887
      0.029887
      0.049622
      ...
      0.666667
      2.000000
      4.666667
      0.214286
      0.071429
      0.214286
      0.500000
      6.616627e-01
      0.213724
      0.49

      id1061
      3
      76
      -0.296791
      1
      76
      -0.191489
      0.105302
      -0.029887
      0.029887
      0.049622
      ...
      0.666667
      2.000000
      4.666667
      0.214286
      0.071429
      0.214286
      0.500000
      6.616627e-01
      0.213724
      1.96

1062 rows × 271 columns

### train-test split

```python
from sklearn.model_selection import train_test_split
split = train_test_split(range(df.shape[0]), test_size=0.1, random_state=1234)
train, test = data.split(split)
```

```python
train.feature_selection(n=-1)
```

    2022-01-07 02:04:21,110 - modnet - INFO - Starting target 1/1: hardness ...
    2022-01-07 02:04:21,111 - modnet - INFO - Computing mutual information between features and target...
    2022-01-07 02:04:30,343 - modnet - INFO - Computing optimal features...
    2022-01-07 02:04:33,639 - modnet - INFO - Selected 50/173 features...
    2022-01-07 02:04:35,895 - modnet - INFO - Selected 100/173 features...
    2022-01-07 02:04:37,143 - modnet - INFO - Selected 150/173 features...
    2022-01-07 02:04:37,302 - modnet - INFO - Done with target 1/1: hardness.
    2022-01-07 02:04:37,302 - modnet - INFO - Merging all features...
    2022-01-07 02:04:37,303 - modnet - INFO - Done.

```python
state_name in train.optimal_features
```

    True

## MODNet model

```python
model = MODNetModel([[[target_name]]],
                    weights={target_name:1},
                    num_neurons = [[256], [128], [16], [16]],
                    n_feat = 150,
                    act =  "elu"
                   )
```

### training

```python
model.fit(train,
          val_fraction = 0.1,
          lr = 0.0002,
          batch_size = 64,
          loss = 'mae',
          epochs = 100,
          verbose = 1,
         )
```

    C:\Users\sterg\Miniconda3\envs\modnet\lib\site-packages\keras\optimizer_v2\adam.py:105: UserWarning: The `lr` argument is deprecated, use `learning_rate` instead.
      super(Adam, self).__init__(name, **kwargs)

    Epoch 1/100
    14/14 [==============================] - 1s 11ms/step - loss: 23.9045 - mae: 23.9045 - val_loss: 22.5993 - val_mae: 22.5993
    Epoch 2/100
    14/14 [==============================] - 0s 2ms/step - loss: 21.4289 - mae: 21.4289 - val_loss: 20.4993 - val_mae: 20.4993
    Epoch 3/100
    14/14 [==============================] - 0s 2ms/step - loss: 19.3106 - mae: 19.3106 - val_loss: 18.3110 - val_mae: 18.3110
    Epoch 4/100
    14/14 [==============================] - 0s 3ms/step - loss: 16.9091 - mae: 16.9091 - val_loss: 15.6841 - val_mae: 15.6841
    Epoch 5/100
    14/14 [==============================] - 0s 3ms/step - loss: 13.9103 - mae: 13.9103 - val_loss: 12.3490 - val_mae: 12.3490
    Epoch 6/100
    14/14 [==============================] - 0s 3ms/step - loss: 10.1801 - mae: 10.1801 - val_loss: 8.3267 - val_mae: 8.3267
    Epoch 7/100
    14/14 [==============================] - 0s 3ms/step - loss: 7.4886 - mae: 7.4886 - val_loss: 7.7784 - val_mae: 7.7784
    Epoch 8/100
    14/14 [==============================] - 0s 3ms/step - loss: 6.9602 - mae: 6.9602 - val_loss: 6.6447 - val_mae: 6.6447
    Epoch 9/100
    14/14 [==============================] - 0s 2ms/step - loss: 6.3513 - mae: 6.3513 - val_loss: 6.1008 - val_mae: 6.1008
    Epoch 10/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.9950 - mae: 5.9950 - val_loss: 5.9176 - val_mae: 5.9176
    Epoch 11/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.8134 - mae: 5.8134 - val_loss: 5.8231 - val_mae: 5.8231
    Epoch 12/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.6542 - mae: 5.6542 - val_loss: 5.7137 - val_mae: 5.7137
    Epoch 13/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.5065 - mae: 5.5065 - val_loss: 5.5987 - val_mae: 5.5987
    Epoch 14/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.3685 - mae: 5.3685 - val_loss: 5.4750 - val_mae: 5.4750
    Epoch 15/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.2598 - mae: 5.2598 - val_loss: 5.4609 - val_mae: 5.4609
    Epoch 16/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.1616 - mae: 5.1616 - val_loss: 5.2524 - val_mae: 5.2524
    Epoch 17/100
    14/14 [==============================] - 0s 2ms/step - loss: 5.1293 - mae: 5.1293 - val_loss: 5.2511 - val_mae: 5.2511
    Epoch 18/100
    14/14 [==============================] - 0s 3ms/step - loss: 5.0488 - mae: 5.0488 - val_loss: 5.0509 - val_mae: 5.0509
    Epoch 19/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.9519 - mae: 4.9519 - val_loss: 4.9905 - val_mae: 4.9905
    Epoch 20/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.9068 - mae: 4.9068 - val_loss: 4.9113 - val_mae: 4.9113
    Epoch 21/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.8095 - mae: 4.8095 - val_loss: 4.8864 - val_mae: 4.8864
    Epoch 22/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.8075 - mae: 4.8075 - val_loss: 4.8130 - val_mae: 4.8130
    Epoch 23/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.7030 - mae: 4.7030 - val_loss: 4.7173 - val_mae: 4.7173
    Epoch 24/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.6979 - mae: 4.6979 - val_loss: 4.7039 - val_mae: 4.7039
    Epoch 25/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.6426 - mae: 4.6426 - val_loss: 4.6425 - val_mae: 4.6425
    Epoch 26/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.5736 - mae: 4.5736 - val_loss: 4.6029 - val_mae: 4.6029
    Epoch 27/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.5277 - mae: 4.5277 - val_loss: 4.5316 - val_mae: 4.5316
    Epoch 28/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.5100 - mae: 4.5100 - val_loss: 4.5562 - val_mae: 4.5562
    Epoch 29/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.4737 - mae: 4.4737 - val_loss: 4.4898 - val_mae: 4.4898
    Epoch 30/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.4414 - mae: 4.4414 - val_loss: 4.4648 - val_mae: 4.4648
    Epoch 31/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.4446 - mae: 4.4446 - val_loss: 4.4469 - val_mae: 4.4469
    Epoch 32/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.4518 - mae: 4.4518 - val_loss: 4.4066 - val_mae: 4.4066
    Epoch 33/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.3907 - mae: 4.3907 - val_loss: 4.3816 - val_mae: 4.3816
    Epoch 34/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.3735 - mae: 4.3735 - val_loss: 4.3528 - val_mae: 4.3528
    Epoch 35/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.3472 - mae: 4.3472 - val_loss: 4.4062 - val_mae: 4.4062
    Epoch 36/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.3978 - mae: 4.3978 - val_loss: 4.3792 - val_mae: 4.3792
    Epoch 37/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.3364 - mae: 4.3364 - val_loss: 4.4843 - val_mae: 4.4843
    Epoch 38/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.3015 - mae: 4.3015 - val_loss: 4.4021 - val_mae: 4.4021
    Epoch 39/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.2813 - mae: 4.2813 - val_loss: 4.3447 - val_mae: 4.3447
    Epoch 40/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.3154 - mae: 4.3154 - val_loss: 4.3216 - val_mae: 4.3216
    Epoch 41/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.2387 - mae: 4.2387 - val_loss: 4.3084 - val_mae: 4.3084
    Epoch 42/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.2281 - mae: 4.2281 - val_loss: 4.3432 - val_mae: 4.3432
    Epoch 43/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.1948 - mae: 4.1948 - val_loss: 4.3013 - val_mae: 4.3013
    Epoch 44/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.2546 - mae: 4.2546 - val_loss: 4.3136 - val_mae: 4.3136
    Epoch 45/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.1960 - mae: 4.1960 - val_loss: 4.3197 - val_mae: 4.3197
    Epoch 46/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.2032 - mae: 4.2032 - val_loss: 4.2988 - val_mae: 4.2988
    Epoch 47/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.2198 - mae: 4.2198 - val_loss: 4.3282 - val_mae: 4.3282
    Epoch 48/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.1502 - mae: 4.1502 - val_loss: 4.3818 - val_mae: 4.3818
    Epoch 49/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.1415 - mae: 4.1415 - val_loss: 4.3158 - val_mae: 4.3158
    Epoch 50/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.1233 - mae: 4.1233 - val_loss: 4.3268 - val_mae: 4.3268
    Epoch 51/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.1365 - mae: 4.1365 - val_loss: 4.3565 - val_mae: 4.3565
    Epoch 52/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.2174 - mae: 4.2174 - val_loss: 4.3533 - val_mae: 4.3533
    Epoch 53/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0583 - mae: 4.0583 - val_loss: 4.2825 - val_mae: 4.2825
    Epoch 54/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0692 - mae: 4.0692 - val_loss: 4.5247 - val_mae: 4.5247
    Epoch 55/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0934 - mae: 4.0934 - val_loss: 4.2950 - val_mae: 4.2950
    Epoch 56/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0545 - mae: 4.0545 - val_loss: 4.3103 - val_mae: 4.3103
    Epoch 57/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.1886 - mae: 4.1886 - val_loss: 4.3223 - val_mae: 4.3223
    Epoch 58/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.1972 - mae: 4.1972 - val_loss: 4.2836 - val_mae: 4.2836
    Epoch 59/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0462 - mae: 4.0462 - val_loss: 4.3197 - val_mae: 4.3197
    Epoch 60/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0720 - mae: 4.0720 - val_loss: 4.3063 - val_mae: 4.3063
    Epoch 61/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.9575 - mae: 3.9575 - val_loss: 4.3080 - val_mae: 4.3080
    Epoch 62/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.9742 - mae: 3.9742 - val_loss: 4.4214 - val_mae: 4.4214
    Epoch 63/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0313 - mae: 4.0313 - val_loss: 4.5135 - val_mae: 4.5135
    Epoch 64/100
    14/14 [==============================] - 0s 2ms/step - loss: 4.0176 - mae: 4.0176 - val_loss: 4.3921 - val_mae: 4.3921
    Epoch 65/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.9953 - mae: 3.9953 - val_loss: 4.3066 - val_mae: 4.3066
    Epoch 66/100
    14/14 [==============================] - 0s 3ms/step - loss: 4.0344 - mae: 4.0344 - val_loss: 4.3966 - val_mae: 4.3966
    Epoch 67/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.9056 - mae: 3.9056 - val_loss: 4.3519 - val_mae: 4.3519
    Epoch 68/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.9473 - mae: 3.9473 - val_loss: 4.3525 - val_mae: 4.3525
    Epoch 69/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8781 - mae: 3.8781 - val_loss: 4.3397 - val_mae: 4.3397
    Epoch 70/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.8581 - mae: 3.8581 - val_loss: 4.3803 - val_mae: 4.3803
    Epoch 71/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.8844 - mae: 3.8844 - val_loss: 4.3755 - val_mae: 4.3755
    Epoch 72/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.9025 - mae: 3.9025 - val_loss: 4.3396 - val_mae: 4.3396
    Epoch 73/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8437 - mae: 3.8437 - val_loss: 4.2815 - val_mae: 4.2815
    Epoch 74/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8364 - mae: 3.8364 - val_loss: 4.3080 - val_mae: 4.3080
    Epoch 75/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.8019 - mae: 3.8019 - val_loss: 4.3109 - val_mae: 4.3109
    Epoch 76/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8951 - mae: 3.8951 - val_loss: 4.3677 - val_mae: 4.3677
    Epoch 77/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8389 - mae: 3.8389 - val_loss: 4.3026 - val_mae: 4.3026
    Epoch 78/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7746 - mae: 3.7746 - val_loss: 4.2648 - val_mae: 4.2648
    Epoch 79/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7852 - mae: 3.7852 - val_loss: 4.3646 - val_mae: 4.3646
    Epoch 80/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8445 - mae: 3.8445 - val_loss: 4.2898 - val_mae: 4.2898
    Epoch 81/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7731 - mae: 3.7731 - val_loss: 4.2981 - val_mae: 4.2981
    Epoch 82/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8404 - mae: 3.8404 - val_loss: 4.3462 - val_mae: 4.3462
    Epoch 83/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7422 - mae: 3.7422 - val_loss: 4.3811 - val_mae: 4.3811
    Epoch 84/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.8382 - mae: 3.8382 - val_loss: 4.3880 - val_mae: 4.3880
    Epoch 85/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7342 - mae: 3.7342 - val_loss: 4.2839 - val_mae: 4.2839
    Epoch 86/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7253 - mae: 3.7253 - val_loss: 4.3042 - val_mae: 4.3042
    Epoch 87/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7356 - mae: 3.7356 - val_loss: 4.2722 - val_mae: 4.2722
    Epoch 88/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7333 - mae: 3.7333 - val_loss: 4.2563 - val_mae: 4.2563
    Epoch 89/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7329 - mae: 3.7329 - val_loss: 4.2633 - val_mae: 4.2633
    Epoch 90/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.7250 - mae: 3.7250 - val_loss: 4.2972 - val_mae: 4.2972
    Epoch 91/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.6697 - mae: 3.6697 - val_loss: 4.2523 - val_mae: 4.2523
    Epoch 92/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.7004 - mae: 3.7004 - val_loss: 4.2884 - val_mae: 4.2884
    Epoch 93/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.6743 - mae: 3.6743 - val_loss: 4.3196 - val_mae: 4.3196
    Epoch 94/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.6566 - mae: 3.6566 - val_loss: 4.2773 - val_mae: 4.2773
    Epoch 95/100
    14/14 [==============================] - 0s 4ms/step - loss: 3.6771 - mae: 3.6771 - val_loss: 4.2939 - val_mae: 4.2939
    Epoch 96/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.6800 - mae: 3.6800 - val_loss: 4.2495 - val_mae: 4.2495
    Epoch 97/100
    14/14 [==============================] - 0s 3ms/step - loss: 3.6092 - mae: 3.6092 - val_loss: 4.3311 - val_mae: 4.3311
    Epoch 98/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.6233 - mae: 3.6233 - val_loss: 4.2545 - val_mae: 4.2545
    Epoch 99/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.6140 - mae: 3.6140 - val_loss: 4.3443 - val_mae: 4.3443
    Epoch 100/100
    14/14 [==============================] - 0s 2ms/step - loss: 3.6378 - mae: 3.6378 - val_loss: 4.4015 - val_mae: 4.4015

### Predicting

```python
pred = model.predict(test)
pred.head()
```

    WARNING:tensorflow:5 out of the last 11 calls to <function Model.make_predict_function.<locals>.predict_function at 0x0000023ED392BAF0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.

      hardness

      id207
      7.599497

      id661
      33.518353

      id1049
      8.367121

      id470
      34.632599

      id485
      10.467034

### Model Performance

#### Mean Absolute Error

```python
mae_test = np.absolute(pred.values-test.df_targets.values).mean()
print(f'mae: {mae_test}')
```

    mae: 4.699142387146926

#### Hexagonally-binned parity plot

```python
ax = density_hexbin(test.df_targets.values, pred.values)
ax.set_box_aspect(1)
```

#### Traditional parity plot

```python
ax = density_scatter(test.df_targets.values.ravel(), pred.values.ravel())
ax.set_box_aspect(1)
```