# Notes on Chapter 2 of Statistical Rethinking 2nd Edition by Richard McElreath

```python
from matplotlib import pyplot as plt
import tensorflow as tf
import tensorflow_probability as tfp
from math import pi
tfd = tfp.distributions
tfm = tf.math
```

```python
%load_ext rpy2.ipython
```

### 2.1

```r
%%R

ways <- c(0, 3, 8, 9, 0)
ways/sum(ways)
```

    [1] 0.00 0.15 0.40 0.45 0.00

```python
ways = tf.constant([0, 3, 8, 9, 0])
ways/tfm.reduce_sum(ways)
```

    <tf.Tensor: shape=(5,), dtype=float64, numpy=array([0.  , 0.15, 0.4 , 0.45, 0.  ])>

### 2.2

```r
%%R

dbinom(6, size=9, prob=0.5)
```

    [1] 0.1640625

```python
float(tfd.Binomial(total_count=9, probs=0.5).prob(6))
```

    0.16406261920928955

### 2.3

```r
%%R

p_grid <- seq(from=0, to=1, length.out=20)
prior <- rep(1, 20)
likelihood <- dbinom(6, size=9, prob=p_grid)
unstd.posterior <- likelihood * prior
posterior <- unstd.posterior/ sum(unstd.posterior)
```

```python
p_grid = tf.linspace(0., 1, 20)
prior = tf.ones_like(p_grid)
likelihood = tfd.Binomial(total_count=9, probs=p_grid).prob(6)
unnormalized_posterior = likelihood * prior
posterior = unnormalized_posterior / tfm.reduce_sum(unnormalized_posterior)
```

### 2.4

```r
%%R

plot(p_grid, posterior, type="b", xlab="probability of water",
     ylab="posterior probability")
mtext("20 points")
```

```python
plt.plot(p_grid, posterior, "k.-")
plt.xlabel("probability of water")
plt.ylabel("posterior probability")
plt.title(f'{len(p_grid)} points');
```

### 2.5

```r
%%R

p_grid <- seq(from=0, to=1, length.out=20)

prior <- ifelse(p_grid < 0.5, 0, 1)
likelihood <- dbinom(6, size=9, prob=p_grid)
unstd.posterior <- likelihood * prior
posterior <- unstd.posterior/ sum(unstd.posterior)
plot(p_grid, posterior, type="b", xlab="probability of water",
     ylab="posterior probability")

prior <- exp(-0.5*abs(p_grid - 0.5))
likelihood <- dbinom(6, size=9, prob=p_grid)
unstd.posterior <- likelihood * prior
posterior <- unstd.posterior/ sum(unstd.posterior)
lines(p_grid, posterior, type="b", col="blue")

mtext("20 points, truncated and pointy priors")
```

```python
p_grid = tf.linspace(0., 1, 20)

prior = tf.where(p_grid < 0.5, tf.zeros_like(p_grid), tf.ones_like(p_grid))
likelihood = tfd.Binomial(total_count=9, probs=p_grid).prob(6)
unnormalized_posterior = likelihood * prior
posterior = unnormalized_posterior / tfm.reduce_sum(unnormalized_posterior)
plt.plot(p_grid, posterior, ".-k")

prior = tfm.exp(-0.5*tfm.abs(p_grid - 0.5))
likelihood = tfd.Binomial(total_count=9, probs=p_grid).prob(6)
unnormalized_posterior = likelihood * prior
posterior = unnormalized_posterior / tfm.reduce_sum(unnormalized_posterior)
plt.plot(p_grid, posterior, ".-b")

plt.xlabel("probability of water")
plt.ylabel("posterior probability")
plt.title(f'{len(p_grid)} points, truncated and pointy priors');
```

### 2.6

```r
%%R

library(rethinking)
globe.qa <- quap(
    alist(
        W ~ dbinom(W+L, p),
        p ~ dunif(0, 1)
    ),
    data=list(W=6, L=3)
)

precis(globe.qa)
```

    R[write to console]: Loading required package: rstan

    R[write to console]: Loading required package: StanHeaders

    R[write to console]: Loading required package: ggplot2

    R[write to console]: rstan (Version 2.19.3, GitRev: 2e1f913d3ca3)

    R[write to console]: For execution on a local, multicore CPU with excess RAM we recommend calling
    options(mc.cores = parallel::detectCores()).
    To avoid recompilation of unchanged Stan programs, we recommend calling
    rstan_options(auto_write = TRUE)

    R[write to console]: Loading required package: parallel

    R[write to console]: Loading required package: dagitty

    R[write to console]: rethinking (Version 2.01)

    R[write to console]:
    Attaching package: ‘rethinking’

    R[write to console]: The following object is masked from ‘package:stats’:

        rstudent

      mean   sd 5.5% 94.5%
    p 0.67 0.16 0.42  0.92

I don't see an equivalent of the author's library for tensorflow probability, so we'll need to implement the quadratic approximation by hand...

```python
W = tf.constant(6.)
L = tf.constant(3.)

def likelihood(p):
    N = W + L
    return tfd.Binomial(N, probs=p).prob(W) * tfd.Uniform().prob(p)

# Tensorflow gives a lot of warnings that don't have workarounds to avoid
# the warning; thus we need to disable them manually
def NestedGradientWarningFilter(record):
    return not record.getMessage().startswith(
        'Calling GradientTape.gradient on a persistent tape inside its contex')

# find the point of maximum likelihood
mu = tf.Variable(0.5, 'mu')
optimizer = tf.keras.optimizers.Adam(0.1)
for i in range(100):
    with tf.GradientTape() as tape:
        loss = tfm.negative(tfm.log(likelihood(mu)))
    gradients = tape.gradient(loss, [mu])
    optimizer.apply_gradients(zip(gradients, [mu]))

# find the Hessian at that point
with tf.GradientTape(persistent=True) as tape:
    loss = tfm.negative(tfm.log(likelihood(mu)))
    tf.get_logger().addFilter(NestedGradientWarningFilter)
    gradients = tape.gradient(loss, [mu])
    tf.get_logger().removeFilter(NestedGradientWarningFilter)
hessians = tape.jacobian(gradients[0], mu)

sigma = tfm.sqrt(1/hessians)

print("  mean   sd 5.5% 94.5%")
print(f'p {float(mu):.2f} {float(sigma):.2f} {float(mu - 1.6*sigma):.2f} ' +
      f' {float(mu + 1.6*sigma):.2f}')
```

      mean   sd 5.5% 94.5%
    p 0.67 0.16 0.42  0.92

### 2.7

```r
%%R

W <- 6
L <- 3
curve(dbeta(x, W+1, L+1), from=0, to=1)
curve(dnorm(x, 0.67, 0.16), lty=2, add=TRUE)
```

```python
W = 6.
L = 3.
x = tf.linspace(0., 1., 100)
plt.plot(x, tfd.Beta(W + 1, L + 1).prob(x), 'k-')
plt.plot(x, tfd.Normal(mu, sigma).prob(x), 'k--');
```

### 2.8

```r
%%R

n_samples <- 1000
p <- rep(NA, n_samples)
p[1] <- 0.5
W <- 6
L <- 3
for (i in 2:n_samples) {
    p_new <- rnorm(1, p[i-1], 0.1)
    if (p_new < 0) p_new <- abs(p_new) # reflecting boundaries
    if (p_new > 1) p_new <- 2 - p_new
    q0 <- dbinom(W, W+L, p[i-1])
    q1 <- dbinom(W, W+L, p_new)
    p[i] <- ifelse(runif(1) < q1/q0, p_new, p[i-1])
}
```

```python
n_samples = 1000
p = [tf.constant(0.5)]
W = 6
L = 3
for i in range(1,n_samples):
    p_new = tfd.Normal(p[-1], 0.1).sample()
    p_new = tfm.abs(p_new)
    p_new = tfm.minimum(p_new, 2 - p_new)
    qprev = tfd.Binomial(W + L, probs=p[-1]).prob(W)
    qnew = tfd.Binomial(W + L, probs=p_new).prob(W)
    p.append(tf.where(tfd.Uniform().sample(p[-1].shape) < qnew/qprev,
                      p_new, p[-1]))
```

### 2.9

```r
%%R

dens(p, xlim=c(0,1))
curve(dbeta(x, W+1, L+1), lty=2, add=TRUE)
```

```python
import seaborn as sns
sns.distplot(p)
x = tf.linspace(0.,1,100)
plt.plot(x, tfd.Beta(W+1, L+1).prob(x), '--k')
```

### Extension of 2.8-2.9 with vectorized tensorflow code

The advantage of using tensorflow is that we can calculate many trajectories in parallel on one or more GPUs.  This code will calculate one billion samples from the posterior in less than 1 second on a consumer video card from 2018 (Nvidia 2080 Ti):

```python
# combine the entire step into a function to keep the data on the GPU
@tf.function()
def metropolis_step(p_old, W, L):
    p_new = tfd.Normal(p_old, 0.1).sample()
    p_new = tfm.abs(p_new)
    p_new = tfm.minimum(p_new, 2 - p_new)
    qprev = tfd.Binomial(W + L, probs=p_old).prob(W)
    qnew = tfd.Binomial(W + L, probs=p_new).prob(W)
    return tf.where(tfd.Uniform().sample(p_old.shape) < qnew/qprev,
                    p_new, p_old)

n_samples = 1000
n_replicates = 1000000
runin = 20
p = [0.5*tf.ones(n_replicates)]
W = 6
L = 3

for i in range(1,n_samples + runin):
    p.append(metropolis_step(p[-1], W, L))

p = tf.concat(p[runin:], 0)
```

Because we aren't using parallel code to plot, we'll just plot the first million samples.

```python
sns.distplot(p[:1000000])
x = tf.linspace(0.,1,100)
plt.plot(x, tfd.Beta(W+1, L+1).prob(x), '--k');
```

Here's a slightly messier example which uses 16 bit floating point (for speed/memory efficiency) and uses the real (uniform) prior for p.

```python
# combine the entire step into a function to keep the data on the GPU
@tf.function()
def metropolis_step(p_old, W, L):
    zero_f16 = tf.constant(0., dtype=tf.float16)
    one_f16 = tf.constant(1., dtype=tf.float16)
    step_sd = tf.constant(0.1, dtype=tf.float16)
    p_new = tfd.Normal(p_old, step_sd).sample()
    p_new = tfm.abs(p_new)
    p_new = tfm.minimum(p_new, 2 - p_new)
    qprev = tfd.Binomial(W + L, probs=p_old).prob(W)
    qnew = tfd.Binomial(W + L, probs=p_new).prob(W)
    return tf.where(
        tfd.Uniform(zero_f16, one_f16).sample(p_old.shape) < qnew/qprev,
        p_new,
        p_old)

n_samples = 1000
n_replicates = 1000000
runin = 100
p = [tfd.Uniform(tf.constant(0., dtype=tf.float16),
                 tf.constant(1., dtype=tf.float16)).sample(n_replicates)]
W = 6
L = 3

for i in range(1,n_samples + runin):
    p.append(metropolis_step(p[-1], W, L))

p = tf.concat(p[runin:], 0)
```

```python
import seaborn as sns
sns.distplot(p[:1000000])
x = tf.linspace(0.,1,100)
plt.plot(x, tfd.Beta(W+1, L+1).prob(x), '--k');
```

## Practice

### 2E1

2

### 2E2

3

### 2E3

1

### 2E4

Although in principle whether my finger lands on water is a deterministic process, my lack of knowledge of the details of the process leaves me with enough uncertainty that mutiple outcomes from the process seem credible.  If forced to quantify the degree of credibility of my finger landing on water, I might imagine assigning a weighted sum of credibility (based on both intuition and prior observations) across all models of the events of the globe tossing I can imagine. Then 70 percent of that credibility belongs to models of future events where my finger ends on on water.

### 2M1

```r
%%R -w 24 -h 9 -u cm

plot_posteriors = function (total_count, water_count, title, num_points=1000,
                        prior= function (p) {rep(1, length(p))}) {
    p_grid <- seq(from=0, to=1, length.out=num_points)
    prior_grid <- prior(p_grid)
    likelihood <- dbinom(water_count, total_count, p_grid)
    unnormalized_posterior <- likelihood * prior_grid
    posterior <- unnormalized_posterior / sum(unnormalized_posterior)
    plot(p_grid, posterior, type="l")
}

par(mfrow=c(1,3))
plot_posteriors(3, 3, "1) WWW")
plot_posteriors(4, 3, "2) WWWL")
plot_posteriors(7, 5, "3) LWWLWWW")
```

```python
def plot_posteriors(ax, total_count, water_count, title, num_points=1000,
                    prior=tf.ones_like):
    p_grid = tf.linspace(0., 1., num_points)
    prior_grid = prior(p_grid)
    likelihood = tfd.Binomial(total_count=total_count, probs=p_grid).prob(
        water_count)
    unnormalized_posterior = likelihood * prior_grid
    posterior = unnormalized_posterior / tfm.reduce_sum(unnormalized_posterior)
    ax.plot(p_grid, posterior, "-k")
    ax.set_xlabel("probability of water")
    ax.set_ylabel("posterior probability")
    ax.set_title(title);

fig, axs = plt.subplots(1, 3, figsize=(25,8))
plot_posteriors(axs[0], 3, 3, "1) WWW")
plot_posteriors(axs[1], 4, 3, "2) WWWL")
plot_posteriors(axs[2], 7, 5, "3) LWWLWWW")
```

### 2M2

```r
%%R -w 24 -h 9 -u cm

prior = function (p) { p > 0.5 }

par(mfrow=c(1,3))
plot_posteriors(3, 3, "1) WWW", prior=prior)
plot_posteriors(4, 3, "2) WWWL", prior=prior)
plot_posteriors(7, 5, "3) LWWLWWW", prior=prior)
```

```python
prior = lambda p : tf.where(p < 0.5, tf.zeros_like(p), tf.ones_like(p))

fig, axs = plt.subplots(1, 3, figsize=(25,8))
plot_posteriors(axs[0], 3, 3, "1) WWW", prior=prior)
plot_posteriors(axs[1], 4, 3, "2) WWWL", prior=prior)
plot_posteriors(axs[2], 7, 5, "3) LWWLWWW", prior=prior)
```

### 2M3

$$
\begin{align}
\mathrm{Pr}(\mathrm{Earth} | \mathrm{land})
    &= \frac{\mathrm{Pr}(\mathrm{land} | \mathrm{Earth}) \mathrm{Pr}(\mathrm{Earth})}{
        \mathrm{Pr}(\mathrm{land})} \\
    &= \frac{\mathrm{Pr}(\mathrm{land} | \mathrm{Earth}) \mathrm{Pr}(\mathrm{Earth})}{
        \mathrm{Pr}(\mathrm{land} | \mathrm{Earth}) \mathrm{Pr}(\mathrm{Earth}) +
        \mathrm{Pr}(\mathrm{land} | \mathrm{Mars}) \mathrm{Pr}(\mathrm{Mars})
        } \\
    &= \frac{\mathrm{Pr}(\mathrm{land} | \mathrm{Earth}) \mathrm{Pr}(\mathrm{Earth})}{
        \mathrm{Pr}(\mathrm{land} | \mathrm{Earth}) \mathrm{Pr}(\mathrm{Earth}) +
        \mathrm{Pr}(\mathrm{land} | \mathrm{Mars}) \mathrm{Pr}(\mathrm{Mars})
        } \\
    &= \frac{(1 - 0.7) \cdot (0.5)}{
        (1 - 0.7) \cdot (0.5) +
        (1) \cdot (0.5)
        } \\
    &= \frac{3}{13} \\
    &\approx 0.23
\end{align}
$$

```python
# simulation

n_trials = 1000000
earth = tfd.Binomial(1, probs=0.5).sample(n_trials)
land = tf.where(earth == 1., tfd.Binomial(1, probs= 0.3).sample(n_trials),
                tf.ones(n_trials))
earth_given_land = tf.boolean_mask(earth, land == 1)
print(f'{tfm.reduce_mean(earth_given_land):.3}')
```

    0.231

### 2M4

| Card | Side 1 | Side 2 |
|------|--------|--------|
|    0 | Black  | Black  |
|    0 | Black  | White  |
|    0 | White  | White  |

We assume we are equally likely to be viewing any of the three black sides of a card in the deck.  We can see that two out of those three black sides have a black side on the opposite side of the card, so the probability that the other side is black is 2/3.

```python
# simulation

n_trials = 1000000
sides_black = tf.constant([[1,1], [1,0], [0,0]])
num_cards = sides_black.shape[0]
first_cards = tf.random.categorical([tf.ones(num_cards)], n_trials)[0]
first_top_sides = tf.random.categorical([tf.ones(2)], n_trials)[0]
first_sides_black = tf.gather(sides_black, first_cards)
first_top_sides_black = tf.where(first_top_sides == 0, first_sides_black[:,0],
    first_sides_black[:,1])
first_bottom_sides_black = tf.where(first_top_sides == 1,
    first_sides_black[:,0], first_sides_black[:,1])
first_bottom_sides_black_given_top_sides_black = tf.boolean_mask(
    first_bottom_sides_black, first_top_sides_black == 1)
p = tfm.reduce_mean(
        tf.cast(first_bottom_sides_black_given_top_sides_black, tf.float32))
print(f'{p:.3}')
```

    0.668

### 2M5

Given 4 of the 5 black sides in the new deck are paired with a black side on the opposite side, the probability is 4/5.

```python
# simulation

n_trials = 1000000
sides_black = tf.constant([[1,1], [1,0], [0,0], [1,1]])
num_cards = sides_black.shape[0]
first_cards = tf.random.categorical([tf.ones(num_cards)], n_trials)[0]
first_top_sides = tf.random.categorical([tf.ones(2)], n_trials)[0]
first_sides_black = tf.gather(sides_black, first_cards)
first_top_sides_black = tf.where(first_top_sides == 0, first_sides_black[:,0],
    first_sides_black[:,1])
first_bottom_sides_black = tf.where(first_top_sides == 1,
    first_sides_black[:,0], first_sides_black[:,1])
first_bottom_sides_black_given_top_sides_black = tf.boolean_mask(
    first_bottom_sides_black, first_top_sides_black == 1)
p = tfm.reduce_mean(
    tf.cast(first_bottom_sides_black_given_top_sides_black, tf.float32))
print(f'{p:.3}')
```

    0.8

### 2M6

With the new arrangement we have 1 way to pull the single B/B card from the deck (accounting for 2 ways to see a black side) and 2 ways to pull the single B/W card from the bag (accounting for 2 ways to see a black side); thus the probability is 2/4 = 1/2.

```python
# simulation

n_trials = 1000000
sides_black = tf.constant([[1,1], [1,0], [0,0]])
num_cards = sides_black.shape[0]
first_cards = tf.random.categorical(tfm.log([[1.,2.,3.]]), n_trials)[0]
first_top_sides = tf.random.categorical([tf.ones(2)], n_trials)[0]
first_sides_black = tf.gather(sides_black, first_cards)
first_top_sides_black = tf.where(first_top_sides == 0, first_sides_black[:,0],
    first_sides_black[:,1])
first_bottom_sides_black = tf.where(first_top_sides == 1,
    first_sides_black[:,0], first_sides_black[:,1])
first_bottom_sides_black_given_top_sides_black = tf.boolean_mask(
    first_bottom_sides_black, first_top_sides_black == 1)
p = tfm.reduce_mean(
    tf.cast(first_bottom_sides_black_given_top_sides_black, tf.float32))
print(f'{p:.3}')
```

    0.501

### 2M7

Our possible draws are 2 $\times$ BB+BW (one for each face of BB), 2 $\times$ 2 $\times$ BB+WW (one for each face of each card), and 2 $\times$ BW+WW (one for each face of WW).  Thus the probability is 6/8, or 0.75.

```python
# simulation

n_trials = 1000000
sides_black = tf.constant([[1,1], [1,0], [0,0]])
num_cards = sides_black.shape[0]
first_cards = tf.random.categorical([tf.ones(num_cards)], n_trials)[0]
first_top_sides = tf.random.categorical([tf.ones(2)], n_trials)[0]
first_sides_black = tf.gather(sides_black, first_cards)
first_top_sides_black = tf.where(first_top_sides == 0, first_sides_black[:,0],
    first_sides_black[:,1])
first_bottom_sides_black = tf.where(first_top_sides == 1,
    first_sides_black[:,0], first_sides_black[:,1])
second_cards_post_draw1 = tf.random.categorical([tf.ones(num_cards-1)],
    n_trials)[0]
second_cards = tf.where(second_cards_post_draw1 >= first_cards,
    second_cards_post_draw1 + 1, second_cards_post_draw1)
second_top_sides = tf.random.categorical([tf.ones(2)], n_trials)[0]
second_sides_black = tf.gather(sides_black, second_cards)
second_top_sides_black = tf.where(second_top_sides == 0,
    second_sides_black[:,0], second_sides_black[:,1])

first_bottom_sides_black_given_top_sides_black_white = tf.boolean_mask(
    first_bottom_sides_black, tf.logical_and(first_top_sides_black == 1,
    second_top_sides_black == 0))
p = tfm.reduce_mean(
    tf.cast(first_bottom_sides_black_given_top_sides_black_white, tf.float32))
print(f'{p:.3}')
```

    0.75

### 2H1

Let $A$ and $\neg A$ represent the Panda as being species A or B and $T_1$ and $T_2$ represent twins on the first birth second birth, respectively. Then

$$
\begin{align}
Pr(T_2 | T_1) &= Pr(T_2 | A, T_1) Pr(A | T_1) + Pr(T_2 | \neg A, T_1) Pr(\neg A | T_1) \\
&= Pr(T_2 | A) Pr(A | T_1) + Pr(T_2 | \neg A) Pr(\neg A | T_1) \\
&= Pr(T_2 | A) \frac{Pr(T_1 | A) Pr(A)}{Pr(T_1)} + Pr(T_2 | \neg A) \frac{Pr(T_1 | \neg A) Pr(\neg A)}{Pr(T_1)} \\
&= \frac{Pr(T_2 | A) Pr(T_1 | A) Pr(A) + Pr(T_2 | \neg A) Pr(T_1 | \neg A) Pr(\neg A)}{Pr(T_1)} \\
&= \frac{Pr(T_2 | A) Pr(T_1 | A) Pr(A) + Pr(T_2 | \neg A) Pr(T_1 | \neg A) Pr(\neg A)}{Pr(T_1 | A) P(A) + Pr(T_1 | \neg A) Pr(\neg A)} \\
&= \frac{(10\%) (10\%) (50\%) + (20\%) (20\%) (50\%)}{(10\%) (50\%) + (20\%) (50\%)} \\
&= \frac{1}{6}.
\end{align}
$$

```python
# simulation

n_trials = 10000000
prob_twins_by_species = tf.constant([0.1, 0.2])
species = tf.random.categorical([tf.ones(2)], n_trials)[0]
prob_twins = tf.gather(prob_twins_by_species, species)
first_twins = tfd.Uniform().sample(n_trials) <= prob_twins
second_twins = tfd.Uniform().sample(n_trials) <= prob_twins

second_twins_given_first_twins = tf.boolean_mask(second_twins, first_twins)
p = tfm.reduce_mean(tf.cast(second_twins_given_first_twins, tf.float32))
print(f'Pr(T2|T1) = {p:.3}')
```

    Pr(T2|T1) = 0.167

### 2H2

$$
\begin{align}
Pr(A | T_1) & = \frac{Pr(T_1 | A) Pr(A)}{Pr(T_1)} \\
& = \frac{Pr(T_1 | A) Pr(A)}{Pr(T_1 | A) P(A) + Pr(T_1 | \neg A) Pr(\neg A)} \\
&= \frac{(10\%) (50\%)}{(10\%) (50\%) + (20\%) (50\%)} \\
&= \frac{1}{3}.
\end{align}
$$

```python
# simulation, continued
species_a_given_first_twins = tf.boolean_mask(species == 0, first_twins)
p = tfm.reduce_mean(tf.cast(species_a_given_first_twins, tf.float32))
print(f'Pr(A|T1) = {p:.3}')
```

    Pr(A|T1) = 0.333

### 2H3

$$
\begin{align}
Pr(A | T_1, \neg T2)
&= \frac{Pr( T_1, \neg T_2 | A ) Pr(A)}{Pr(T_1, \neg T_2)} \\
&= \frac{Pr( T_1, \neg T_2 | A ) Pr(A)}{Pr(T_1, \neg T_2 | A) Pr(A) + Pr(T_1, \neg T_2 | \neg A) Pr(\neg A)} \\
&= \frac{Pr( T_1 | A ) Pr( \neg T_2 | A) Pr(A)}{Pr( T_1 | A ) Pr( \neg T_2 | A) Pr(A) + Pr(T_1 | \neg A ) Pr( \neg T_2 | \neg A) Pr(\neg A)} \\
&= \frac{(10\%) (90\%) (50\%)}{(10\%) (90\%) (50\%) + (20\%) (80\%) (50\%)} \\
&= \frac{9}{25} \\
&= 36\%
\end{align}
$$

```python
# simulation, continued
species_a_given_first_twins_second_singleton = tf.boolean_mask(
    species == 0, tf.logical_and(first_twins, second_twins==False))
p = tfm.reduce_mean(tf.cast(species_a_given_first_twins_second_singleton,
                            tf.float32))
print(f'Pr(A|T1,~T2) = {p:.3}')
```

    Pr(A|T1,~T2) = 0.36

### 2H4

Let $S$ represent a positive test.

$$
\begin{align}
Pr(A|S) &= \frac{Pr(S|A) Pr(A)}{Pr(S)} \\
        &= \frac{Pr(S|A) Pr(A)}{Pr(S|A) Pr(A) + Pr(S|\neg A) Pr(\neg A)} \\
        &= \frac{(0.8) (0.5)}{(0.8) (0.5) + (0.35) (0.5)} \\
        &= \frac{16}{23} \\
        &\approx 0.70.
\end{align}
$$

Assuming that $S$ conditioned on $A$ is independent of $T_1$ and $T_2$, we also have

$$
\begin{align}
Pr(A|S,T_1,\neg T_2)
    &= \frac{Pr(S,T_1,\neg T_2|A) Pr(A)}{Pr(S, T_1, \neg T_2)} \\
    &= \frac{Pr(S|A) \Pr(T_1,\neg T_2|A) Pr(A)}{
        Pr(S, T_1, \neg T_2 | A) Pr(A) + Pr(S, T_1, \neg T_2 | \neg A) Pr(\neg A)} \\
    &= \frac{Pr(S|A) \Pr(T_1|A) \Pr(\neg T_2|A) Pr(A)}{
        Pr(S | A) Pr(T_1 | A) Pr(\neg T_2 | A) Pr(A) + Pr(S | \neg A) Pr(T_1 | \neg A) Pr(\neg T_2 | \neg A) Pr(\neg A)} \\
    &= \frac{ (0.8)(0.1)(0.9)(0.5) }{ (0.8)(0.1)(0.9)(0.5) + (0.35)(0.2)(0.8)(0.5) } \\
    &= \frac{9}{16} \\
    &\approx 0.56
\end{align}
$$

```python
# simulation, continued
prob_test_positive_by_species = tf.constant([0.8, 1 - 0.65])
prob_test_positive = tf.gather(prob_test_positive_by_species, species)
test_positive = tfd.Uniform().sample(n_trials) <= prob_test_positive

species_a_given_test_positive = tf.boolean_mask(species == 0, test_positive)
p = tfm.reduce_mean(tf.cast(species_a_given_test_positive, tf.float32))
print(f'Pr(A|S) = {p:.3}')

species_a_given_test_positive_and_births = tf.boolean_mask(species == 0,
    tf.logical_and(tf.logical_and(test_positive, first_twins),
                   second_twins==False))
p = tfm.reduce_mean(
    tf.cast(species_a_given_test_positive_and_births, tf.float32))
print(f'Pr(A|S,T1,~T2) = {p:.3}')
```

    Pr(A|S) = 0.696
    Pr(A|S,T1,~T2) = 0.562


# GPyTorch regression with derivative information

## Introduction
In this notebook, we show how to train a GP regression model in GPyTorch of an unknown function given function value and derivative observations. We consider modeling the function:

\begin{align}
              y &= \sin(2x) + cos(x) + \epsilon \\
  \frac{dy}{dx} &= 2\cos(2x) - \sin(x) + \epsilon \\
       \epsilon &\sim \mathcal{N}(0, 0.5)
\end{align}

using 50 value and derivative observations.

```python
import torch
import gpytorch
import math
from matplotlib import pyplot as plt
import numpy as np

%matplotlib inline
%load_ext autoreload
%autoreload 2
```

## Setting up the training data
We use 50 uniformly distributed points in the interval $[0, 5 \pi]$

```python
lb, ub = 0.0, 5*math.pi
n = 50

train_x = torch.linspace(lb, ub, n).unsqueeze(-1)
train_y = torch.stack([
    torch.sin(2*train_x) + torch.cos(train_x),
    -torch.sin(train_x) + 2*torch.cos(2*train_x)
], -1).squeeze(1)

train_y += 0.05 * torch.randn(n, 2)
```

## Setting up the model
A GP prior on the function values implies a multi-output GP prior on the function values and the partial derivatives, see 9.4 in http://www.gaussianprocess.org/gpml/chapters/RW9.pdf for more details. This allows using a `MultitaskMultivariateNormal` and `MultitaskGaussianLikelihood` to train a GP model from both function values and gradients. The resulting RBF kernel that models the covariance between the values and partial derivatives has been implemented in `RBFKernelGrad` and the extension of a constant mean is implemented in `ConstantMeanGrad`.

The `RBFKernelGrad` is generally worse conditioned than the `RBFKernel`, so we place a lower bound on the noise parameter to keep the smallest eigenvalues of the kernel matrix away from zero.

```python
class GPModelWithDerivatives(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super(GPModelWithDerivatives, self).__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ConstantMeanGrad()
        self.base_kernel = gpytorch.kernels.RBFKernelGrad()
        self.covar_module = gpytorch.kernels.ScaleKernel(self.base_kernel)

    def forward(self, x):
        mean_x = self.mean_module(x)
        covar_x = self.covar_module(x)
        return gpytorch.distributions.MultitaskMultivariateNormal(mean_x, covar_x)

likelihood = gpytorch.likelihoods.MultitaskGaussianLikelihood(num_tasks=2)  # Value + Derivative
model = GPModelWithDerivatives(train_x, train_y, likelihood)
```

The model training is similar to training a standard GP regression model

```python
# this is for running the notebook in our testing framework
import os
smoke_test = ('CI' in os.environ)
training_iter = 2 if smoke_test else 50

# Find optimal model hyperparameters
model.train()
likelihood.train()

# Use the adam optimizer
optimizer = torch.optim.Adam([
    {'params': model.parameters()},  # Includes GaussianLikelihood parameters
], lr=0.1)

# "Loss" for GPs - the marginal log likelihood
mll = gpytorch.mlls.ExactMarginalLogLikelihood(likelihood, model)

for i in range(training_iter):
    optimizer.zero_grad()
    output = model(train_x)
    loss = -mll(output, train_y)
    loss.backward()
    print('Iter %d/%d - Loss: %.3f   lengthscale: %.3f   noise: %.3f' % (
        i + 1, training_iter, loss.item(),
        model.covar_module.base_kernel.lengthscale.item(),
        model.likelihood.noise.item()
    ))
    optimizer.step()
```

    Iter 1/50 - Loss: 71.141   lengthscale: 0.693   noise: 0.693
    Iter 2/50 - Loss: 69.100   lengthscale: 0.744   noise: 0.644
    Iter 3/50 - Loss: 66.347   lengthscale: 0.797   noise: 0.598
    Iter 4/50 - Loss: 64.771   lengthscale: 0.845   noise: 0.554
    Iter 5/50 - Loss: 63.744   lengthscale: 0.886   noise: 0.513
    Iter 6/50 - Loss: 61.682   lengthscale: 0.928   noise: 0.474
    Iter 7/50 - Loss: 59.820   lengthscale: 0.961   noise: 0.437
    Iter 8/50 - Loss: 57.801   lengthscale: 0.987   noise: 0.402
    Iter 9/50 - Loss: 56.894   lengthscale: 1.004   noise: 0.370
    Iter 10/50 - Loss: 54.522   lengthscale: 1.010   noise: 0.340
    Iter 11/50 - Loss: 53.263   lengthscale: 1.006   noise: 0.311
    Iter 12/50 - Loss: 50.900   lengthscale: 0.998   noise: 0.285
    Iter 13/50 - Loss: 49.472   lengthscale: 0.986   noise: 0.260
    Iter 14/50 - Loss: 47.405   lengthscale: 0.980   noise: 0.238
    Iter 15/50 - Loss: 46.851   lengthscale: 0.982   noise: 0.217
    Iter 16/50 - Loss: 43.638   lengthscale: 0.991   noise: 0.198
    Iter 17/50 - Loss: 42.900   lengthscale: 1.002   noise: 0.180
    Iter 18/50 - Loss: 39.969   lengthscale: 1.021   noise: 0.164
    Iter 19/50 - Loss: 38.408   lengthscale: 1.040   noise: 0.149
    Iter 20/50 - Loss: 35.881   lengthscale: 1.059   noise: 0.135
    Iter 21/50 - Loss: 34.669   lengthscale: 1.078   noise: 0.122
    Iter 22/50 - Loss: 32.928   lengthscale: 1.097   noise: 0.111
    Iter 23/50 - Loss: 30.690   lengthscale: 1.113   noise: 0.100
    Iter 24/50 - Loss: 28.567   lengthscale: 1.127   noise: 0.091
    Iter 25/50 - Loss: 27.540   lengthscale: 1.138   noise: 0.082
    Iter 26/50 - Loss: 24.865   lengthscale: 1.142   noise: 0.074
    Iter 27/50 - Loss: 23.273   lengthscale: 1.141   noise: 0.067
    Iter 28/50 - Loss: 20.533   lengthscale: 1.147   noise: 0.061
    Iter 29/50 - Loss: 19.787   lengthscale: 1.144   noise: 0.055
    Iter 30/50 - Loss: 16.676   lengthscale: 1.146   noise: 0.050
    Iter 31/50 - Loss: 14.890   lengthscale: 1.151   noise: 0.045
    Iter 32/50 - Loss: 13.735   lengthscale: 1.158   noise: 0.040
    Iter 33/50 - Loss: 11.772   lengthscale: 1.171   noise: 0.036
    Iter 34/50 - Loss: 9.266   lengthscale: 1.182   noise: 0.033
    Iter 35/50 - Loss: 7.507   lengthscale: 1.193   noise: 0.030
    Iter 36/50 - Loss: 5.724   lengthscale: 1.195   noise: 0.027
    Iter 37/50 - Loss: 5.030   lengthscale: 1.195   noise: 0.024
    Iter 38/50 - Loss: 1.297   lengthscale: 1.207   noise: 0.022
    Iter 39/50 - Loss: -0.072   lengthscale: 1.211   noise: 0.020
    Iter 40/50 - Loss: -2.852   lengthscale: 1.208   noise: 0.018
    Iter 41/50 - Loss: -4.053   lengthscale: 1.200   noise: 0.016
    Iter 42/50 - Loss: -5.160   lengthscale: 1.198   noise: 0.014
    Iter 43/50 - Loss: -8.217   lengthscale: 1.208   noise: 0.013
    Iter 44/50 - Loss: -8.949   lengthscale: 1.216   noise: 0.012
    Iter 45/50 - Loss: -11.805   lengthscale: 1.228   noise: 0.011
    Iter 46/50 - Loss: -14.472   lengthscale: 1.230   noise: 0.010
    Iter 47/50 - Loss: -17.141   lengthscale: 1.228   noise: 0.009
    Iter 48/50 - Loss: -16.575   lengthscale: 1.215   noise: 0.008
    Iter 49/50 - Loss: -18.488   lengthscale: 1.204   noise: 0.007
    Iter 50/50 - Loss: -20.305   lengthscale: 1.207   noise: 0.007

Model predictions are also similar to GP regression with only function values, butwe need more CG iterations to get accurate estimates of the predictive variance

```python
# Set into eval mode
model.train()
model.eval()
likelihood.eval()

# Initialize plots
f, (y1_ax, y2_ax) = plt.subplots(1, 2, figsize=(12, 6))

# Make predictions
with torch.no_grad(), gpytorch.settings.max_cg_iterations(50):
    test_x = torch.linspace(lb, ub, 500)
    predictions = likelihood(model(test_x))
    mean = predictions.mean
    lower, upper = predictions.confidence_region()

# Plot training data as black stars
y1_ax.plot(train_x.detach().numpy(), train_y[:, 0].detach().numpy(), 'k*')
# Predictive mean as blue line
y1_ax.plot(test_x.numpy(), mean[:, 0].numpy(), 'b')
# Shade in confidence
y1_ax.fill_between(test_x.numpy(), lower[:, 0].numpy(), upper[:, 0].numpy(), alpha=0.5)
y1_ax.legend(['Observed Values', 'Mean', 'Confidence'])
y1_ax.set_title('Function values')

# Plot training data as black stars
y2_ax.plot(train_x.detach().numpy(), train_y[:, 1].detach().numpy(), 'k*')
# Predictive mean as blue line
y2_ax.plot(test_x.numpy(), mean[:, 1].numpy(), 'b')
# Shade in confidence
y2_ax.fill_between(test_x.numpy(), lower[:, 1].numpy(), upper[:, 1].numpy(), alpha=0.5)
y2_ax.legend(['Observed Derivatives', 'Mean', 'Confidence'])
y2_ax.set_title('Derivatives')

None
```

```python

```

```python

```


# Exercise 4, answers

```python
def f_constrained(x):
    return x[0]**2+x[1]**2+x[0]+2*x[1], [], [x[0]+x[1]-1]
```
Problem 1

```python
def alpha(x,f):
    (_,ieq,eq) = f(x)
    return sum([min([0,ieq_j])**2 for ieq_j in ieq])\
+sum([eq_k**2 for eq_k in eq])

def penalized_function(x,f,r):
    return f(x)[0] + r*alpha(x,f)
```

#### Let us solve the penalized problem with penalty term growin in a loop

```python
import numpy as np
from scipy.optimize import minimize
r = 0
x_old = np.array([float('inf')]*2)
x_new = [0,0]
while np.linalg.norm(x_new-x_old)>0.0001:
    res = minimize(lambda x:penalized_function(x,f_constrained,r),
               [0,0],method='Nelder-Mead')
    x_old = x_new
    x_new = np.array(res.x)
    r = r+1
print x_new, r
```

    [ 0.74340989  0.24336301] 95

## Problem 2

```python
def f_constrained_approx(x,epsilon):
    return x[0]**2+x[1]**2+x[0]+2*x[1], [x[0]+x[1]-1+epsilon,\
                                         epsilon-(x[0]+x[1]-1)], []
```

```python
def beta(x,f):
    _,ieq,_ = f(x)
    try:
        value=sum([1/max([0,ieq_j]) for ieq_j in ieq])
    except ZeroDivisionError:
        value = float("inf")
    return value
def function_with_barrier(x,f,r):
    return f(x)[0]+r*beta(x,f)
```

```python
import numpy as np
import ad
from scipy.optimize import minimize
r = 1.0
epsilon = 1.
x_old = np.array([float('inf')]*2)
x_new = [1,0]
while np.linalg.norm(x_new-x_old)>0.0001:
    g = lambda x: function_with_barrier(x,\
               lambda y: f_constrained_approx(y,epsilon),r)
    res = minimize(g,x_new,method='Newton-CG',jac=ad.gh(g)[0],\
                   hess=ad.gh(g)[1])
    x_old = x_new
    x_new = res.x
    r=r/2
    epsilon = epsilon/2
    print x_new, epsilon, r
```

    [ 0.5578683  0.0578683] 0.5 0.5
    [ 0.68545665  0.18545665] 0.25 0.25
    [ 0.73158293  0.23158293] 0.125 0.125
    [ 0.74519333  0.24519333] 0.0625 0.0625
    [ 0.74878417  0.24878417] 0.03125 0.03125
    [ 0.74969513  0.24969513] 0.015625 0.015625
    [ 0.74992373  0.24992373] 0.0078125 0.0078125
    [ 0.74998093  0.24998093] 0.00390625 0.00390625

## Problem 3

```python
import numpy as np
def project_vector(A,vector):

```

```python
import numpy as np
import ad
def projected_gradient_method(f,A,start,step,precision):
    f_old = float('Inf')
    x = np.array(start)
    steps = []
    f_new = f(x)
    while abs(f_old-f_new)>precision:
        f_old = f_new
        gradient = ad.gh(f)[0](x)
        grad_proj = project_vector(A,[-i for i in gradient])#The only changes to steepest..
        grad_proj = np.array(grad_proj.transpose())[0] #... descent are here!
        x = x+grad_proj*step
        f_new = f(x)
        steps.append(list(x))
    return x,f_new,steps
```

```python
projected_gradient_method(lambda x:f_constrained(x)[0],[[1,1]],[1,0]\
                          ,.5,0.000001)
```

    (array([ 0.75,  0.25]), 1.875, [[0.75, 0.25], [0.75, 0.25]])

## Problem 4

Need to show that there exists unique Lagrance multiplier vectors $\lambda^* = (\lambda^*_1,\ldots,\lambda_J^*)$ and $\mu^*=(\mu_1^*,\ldots,\mu_K^*)$ such that
$$
\begin{align}
&\nabla_xL(x,\lambda,\mu) = 0\\
&\mu_j^*\geq0,\text{ for all }j=1,\ldots,J\\
&\mu_j^*g_j(x)=0,\text{for all }j=1,\ldots,J,
\end{align}
$$
where $$L(x,\lambda,\mu) = f(x)- \sum_{k=1}^K\mu_kg_k(x) -\sum_{j=1}^J\lambda_jh_j(x)$$

Now,  $f(x) = x_1^2+x_2^2+x_1+2x_2$, $g(x) = 0$ and $h(x)=x_1+x_2-1$.

Thus, stability rule becomes $$
\left\{
\begin{align}
2x_1+1-\lambda = 0\\
2x_2+2-\lambda=0.
\end{align}
\right.
$$

We do not have a complementary rule, since we do not have inequality constraints!

```python
def check_KKT_eqc(x,tol):
    l = 2*x[0]+1
    if abs(2*x[1]+2-l)<=tol:
        print abs(2*x[1]+2-l)
        return True
    return False
```

```python
check_KKT_eqc([0.74998093,0.24998093],0.000001)
```

    0.0

    True


```
from __future__ import  division
%qtconsole
```

```
import sympy as S
import sympy.stats as st
```

```
u=S.symbols('u')
p=st.LogNormal('u',0,0.5)
pu=st.density(p)(u)
fx=S.lambdify(u,pu,'numpy')
x = linspace(0.001,4,100)
hist([st.sample(p) for i in range(1000)],bins=50,normed=1);
plot(x,fx(x),'r-',lw=3.)
```

```
pw=[i*(S.Heaviside(u-j/2)-S.Heaviside(u-j/2-1/2)) for i,j in zip(S.symbols('a:8'),range(8))]
pwf = lambda i: sum(pw).subs(u,i)

```

## Rectangle Wedge Tail Decomposition

$$ f(x) = \exp\left(-\frac{(x-1)^2}{2x} \right)(x+1)/12 $$

where $x>0$

```
x = linspace(0.001,10,100)
f= lambda x: (sqrt(1/x) + sqrt(x))/ (2.*pow(exp(1),pow(-sqrt(1/x) + sqrt(x),2)/ 2.)*sqrt(2*pi)*x)
fx = f(x)
plot(x,fx)
```

```
u=S.symbols('u')
p=S.Piecewise(*[(f(j),(i<u<=j)) for i,j in zip(range(10),range(1,11))]+[(0,True)])
plot(x,[p.subs(u,i) for i in x],x,fx)
```

```

```

```

```


## Python

```julia
from sympy import *
```

```julia
a = -oo
a
```

$\displaystyle -\infty$

```julia
b = -float('inf')
b
```

    -inf

```julia
type(a)
```

    sympy.core.numbers.NegativeInfinity

```julia
type(b)
```

    float

## Julia

```julia
using SymPy
a = -oo
```

$\begin{equation*}-\infty\end{equation*}$

```julia
using PyCall
b = py"""-float('inf')"""
```

    -Inf

```julia
typeof(a)
```

    Sym

```julia
typeof(b)
```

    Float64

```julia

```


```python
import numpy as np
from scipy.interpolate import interp1d
from simpegEM1D import EM1DAnalytics
from scipy.integrate import fixed_quad
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

```python
# from sympy import symbols, exp, diff, integrate
# f, t, tau, t0 = sympy.symbols('f, t tau t0')
# f = 1./tau * exp(1./tau*t)
# integrate(f, t)
```

```python
def v_step_exp(t, tau=2e-3):
    return 1./tau * np.exp(-t/tau)
def v_ramp_off_exp(t, t0, tau=2e-3):
    return 1./t0 * (- np.exp(-(t+t0)/tau) + np.exp(-(t)/tau))
def v_ramp_off_exp_on(t, t0, tau=2e-3):
    return 1./t0 * (- np.exp(-(t+t0)/tau) + 1.)
```

```python
time = np.logspace(-6, -3, 31) - 1e-5
t0 = 1e-3
on = v_ramp_off_exp_on(time[time<=0.], t0)
off = v_ramp_off_exp(time[time>0.], t0)
```

```python
output_fixed_exp_off = np.array([fixed_quad(v_step_exp, t, t+t0)[0] for t in time[time>0.]])/t0
output_fixed_exp_on = np.array([fixed_quad(v_step_exp, 0, t+t0)[0] for t in time[time<=0.]])/t0

```

```python
plt.plot(time[time<=0.], on)
plt.plot(time[time<=0.], output_fixed_exp_on, '.')
```

```python
plt.loglog(time[time>0.], off)
plt.loglog(time[time>0.], output_fixed_exp_off, '.')
```

```python
def v_step_e(t):
    return 1./t
def v_ramp_off_e(t, t0):
    return 1./t0 * np.log((t+t0)/t)

def v_step_l(t):
    return t**(-5./2)
def v_ramp_off_l(t, t0):
    F_t_t0 = 2*t * (1-(1+t0/t)**-1.5) / (3*t0)
    return t**(-5./2) * F_t_t0
def v_step(time):
    return EM1DAnalytics.dBzdtAnalCircT(
    10., time, 1e-1
    )
```

```python

```

```python
from scipy.integrate import fixed_quad, quadrature
t0 = 1e-3
time = np.logspace(-6, -2, 41)
output_fixed_exp = np.array([fixed_quad(v_step_exp, t, t+t0)[0] for t in time])
```

```python
%%time
output_fixed_exp = np.array([fixed_quad(v_step_exp, t, t+t0)[0] for t in time])
output_fixed_exp_on = np.array([fixed_quad(v_step_exp, 0, t)[0] for t in time])
output_fixed_e = np.array([fixed_quad(v_step_e, t, t+t0)[0] for t in time])
output_fixed_l = np.array([fixed_quad(v_step_l, t, t+t0)[0] for t in time])
```

    CPU times: user 5.77 ms, sys: 130 µs, total: 5.9 ms
    Wall time: 6.12 ms

```python
plt.loglog(time, output_fixed_exp/t0)
plt.loglog(time, v_ramp_off_exp(time, t0), '.')
```

```python
%%time
output_exp = np.array([quadrature(v_step_exp, t, t+t0)[0] for t in time])
output_e = np.array([quadrature(v_step_e, t, t+t0)[0] for t in time])
output_l = np.array([quadrature(v_step_l, t, t+t0)[0] for t in time])
```

    CPU times: user 70 ms, sys: 3.72 ms, total: 73.8 ms
    Wall time: 80.2 ms

    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 1.290315e-03
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 6.788594e-04
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 3.244263e-04
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 1.392809e-04
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 5.305166e-05
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 1.767873e-05
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 5.073385e-06
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 1.231816e-06
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 2.480692e-07
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 4.268409e+06
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 1.958448e+06
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 8.110523e+05
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 3.001735e+05
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 9.815537e+04
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 2.798599e+04
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 6.852884e+03
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 1.416508e+03
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 2.423819e+02
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 3.358745e+01
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 3.677560e+00
      AccuracyWarning)
    /Users/sgkang/anaconda3/lib/python3.7/site-packages/scipy/integrate/quadrature.py:233: AccuracyWarning: maxiter (50) exceeded. Latest difference = 3.095358e-01
      AccuracyWarning)

```python
plt.loglog(time, v_step_exp(time))
plt.loglog(time, v_ramp_off_exp(time, 1e-3))
plt.loglog(time, output_exp/t0, '.')
plt.loglog(time, output_fixed_exp/t0, 'x')
```

```python
plt.loglog(time, v_step_e(time))
plt.loglog(time, v_ramp_off_e(time, 1e-3))
plt.loglog(time, output_e/t0, '.')
plt.loglog(time, output_fixed_e/t0, 'x')
```

```python
plt.loglog(time, v_step_l(time))
plt.loglog(time, v_ramp_off_l(time, 1e-3))
plt.loglog(time, output_l/t0, '.')
plt.loglog(time, output_fixed_l/t0, 'x')
```

```python
from simpegEM1D import skytem_HM_2015
wave = skytem_HM_2015()
```

```python
plt.plot(wave.current_times[-7:], wave.currents[-7:], 'k.-')
```

```python
wave.current_times[-7:].max() * 1e3
```

    0.196368

```python
t0s = np.diff(wave.currents[-7:]) / np.diff(wave.current_times[-7:])
Tp = wave.current_times[-7:].max()-wave.current_times[-7:].min()
```

```python
plt.plot(wave.current_times[-7:][1:], t0s, 'kx-')
```

```python
step_func = v_step_exp
# T = 1./25.
t_measured = np.logspace(-6, -2, 31)
t_currents = np.r_[0., 0.002, 0.004]
T = t_currents.max()
currents = np.r_[0., 1., 0.]

def piecewise_ramp(step_func, t_measured, t_currents, currents, n=20, eps=1e-10):
    """
    Computes response from piecewise linear current waveform with a single pulse.
    This basically evaluates the convolution between dI/dt and step-off response.

    step_func: function handle to evaluate step-off response
    t_measured: time channels when the current is off
    currents: input source currents
    n: Gaussian quadrature order
    """
    dt = np.diff(t_currents)
    dI = np.diff(currents)
    dIdt = dI/dt
    nt = t_currents.size
    response = np.zeros(t_measured.size, dtype=float)
    pulse_time = t_currents.max()

    for i in range(1, nt):
        t_lag = pulse_time - t_currents[i]
        time = t_lag + t_measured
        t0 = dt[i-1]
        const = -dIdt[i-1]
        if abs(const) > eps:
            for j, t in enumerate(time):
                # on-time
                if t < 0.:
                    response[j] += fixed_quad(step_func, 0, t+t0, n=20)[0] * const
                # off-time
                else:
                    response[j] += fixed_quad(step_func, t, t+t0, n=20)[0] * const

    return response

def double_pulse(step_func, t_measured, t_currents, currents, T, n=20):
    """
    Computes response from double pulses (negative then positive)
    T: Period (e.g. 25 Hz base frequency, 0.04 s period)
    """
    response_double = (
        piecewise_ramp(step_func, t_measured, t_currents, currents, n=n)
        -piecewise_ramp(step_func, t_measured+T/2., t_currents, currents, n=n) * 0.5
    )
    return response_double
```

# Test for off-time

```python
t_off = np.logspace(-6, -2, 31)
# single pulse
response_single = piecewise_ramp(v_step_exp, t_off, t_currents, currents)

t_off = np.logspace(-6, -2, 31)
# single pulse
response_single = piecewise_ramp(v_step_exp, t_off, t_currents, currents)
response_single_true = v_ramp_off_exp(t_off, 2e-3)-v_ramp_off_exp(t_off+0.002, 2e-3)
# double pulse
response_double = double_pulse(v_step_exp, t_off, t_currents, currents, T)

response_double_true = (
    v_ramp_off_exp(t_off, 2e-3)-v_ramp_off_exp(t_off+0.002, 2e-3)+
    -(v_ramp_off_exp(t_off+T/2., 2e-3)-v_ramp_off_exp(t_off+0.002+T/2, 2e-3)) * 0.5
)
plt.loglog(t_off, response_double, 'k')
plt.loglog(t_off, response_double_true, 'r.')

plt.loglog(t_off, response_single, 'k--')
plt.loglog(t_off, response_single_true, 'r.')
```

```python
t_off = np.logspace(-6, -2, 31)
# single pulse
response_single = piecewise_ramp(v_step_exp, t_off, t_currents, currents)
response_single_true = v_ramp_off_exp(t_off, 2e-3)-v_ramp_off_exp(t_off+0.002, 2e-3)
# double pulse
response_double = double_pulse(v_step_exp, t_off, t_currents, currents, T)

response_double_true = (
    v_ramp_off_exp(t_off, 2e-3)-v_ramp_off_exp(t_off+0.002, 2e-3)+
    -(v_ramp_off_exp(t_off+T/2., 2e-3)-v_ramp_off_exp(t_off+0.002+T/2, 2e-3)) * 0.5
)
plt.loglog(t_off, response_double, 'k')
plt.loglog(t_off, response_double_true, 'r.')

plt.loglog(t_off, response_single, 'k--')
plt.loglog(t_off, response_single_true, 'r.')
```

## Test for on-time

```python
time = np.logspace(-6, -2, 33) - 1e-5
response_single = piecewise_ramp(v_step_exp, time, t_currents, currents)
response_single_true_off = (
    v_ramp_off_exp(time[time>0.], 2e-3)-
    v_ramp_off_exp(time[time>0.]+0.002, 2e-3)
)
response_single_true_on = (
    v_ramp_off_exp_on(time[time<=0.], 2e-3)-
    v_ramp_off_exp(time[time<=0.]+0.002, 2e-3)
)

```

```python
plt.plot(time[time>0.], response_single[time>0.], 'k')
plt.plot(time[time>0.], response_single_true_off, 'ro')

```

```python
plt.loglog(time+2e-3, response_single)
```

```python
plt.plot(time[time<=0.], response_single[time<=0.], 'k')
plt.plot(time[time<=0.], response_single_true_on, 'ro')
```

```python
t_off = np.logspace(-6, -2, 31)
# single pulse
response_single = piecewise_ramp(v_step_exp, t_off, t_currents, currents)
response_single_true = v_ramp_off_exp(t_off, 2e-3)-v_ramp_off_exp(t_off+0.002, 2e-3)
# double pulse
response_double = double_pulse(v_step_exp, t_off, t_currents, currents, T)

response_double_true = (
    v_ramp_off_exp(t_off, 2e-3)-v_ramp_off_exp(t_off+0.002, 2e-3)+
    -(v_ramp_off_exp(t_off+T/2., 2e-3)-v_ramp_off_exp(t_off+0.002+T/2, 2e-3)) * 0.5
)
plt.loglog(t_off, response_double, 'k')
plt.loglog(t_off, response_double_true, 'r.')

plt.loglog(t_off, response_single, 'k--')
plt.loglog(t_off, response_single_true, 'r.')
```

```python
%%time
response_single = piecewise_ramp(v_step_l, t_off, t_currents, currents)
```

    CPU times: user 1.54 ms, sys: 42 µs, total: 1.58 ms
    Wall time: 1.59 ms

```python
# single pulse
t_off = np.logspace(-5, -2, 31)
response_single = piecewise_ramp(v_step_l, t_off, t_currents, currents)
response_single_true = v_ramp_off_l(t_off, 2e-3)-v_ramp_off_l(t_off+0.002, 2e-3)
# double pulse
response_double = double_pulse(v_step_l, t_off, t_currents, currents, T)
response_double_true = (
    v_ramp_off_l(t_off, 2e-3)-v_ramp_off_l(t_off+0.002, 2e-3)+
    -(v_ramp_off_l(t_off+T/2., 2e-3)-v_ramp_off_l(t_off+0.002+T/2, 2e-3)) * 0.5
)
plt.loglog(t_off, response_double, 'k')
plt.loglog(t_off, response_double_true, 'r.')

plt.loglog(t_off, response_single, 'k--')
plt.loglog(t_off, response_single_true, 'r.')
```

```python
%%time
response_single_true = piecewise_ramp(v_step, t_off, t_currents, currents, n=5)
```

    CPU times: user 3.54 ms, sys: 83 µs, total: 3.62 ms
    Wall time: 4.79 ms

```python
%%time
response_single_true = piecewise_ramp(v_step, t_off, t_currents, currents, n=20)
```

    CPU times: user 3.02 ms, sys: 46 µs, total: 3.07 ms
    Wall time: 3.07 ms

```python
# single pulse
response_single = piecewise_ramp(v_step, t_off, t_currents, currents, n=20)
plt.loglog(t_off, -response_single, 'k')
plt.loglog(t_off, -response_single_true, 'r.')
```

```python
# single pulse
response_single = piecewise_ramp(v_step, t_off, t_currents, currents, n=20)
response_double = double_pulse(v_step, t_off, t_currents, currents, T)
plt.loglog(t_off, -response_single, 'k--')
plt.loglog(t_off, -response_double, 'k')
```


# A quick introduction to PyClaw
PyClaw is a solver for hyperbolic PDEs, based on [Clawpack](www.clawpack.org).  You can read more about PyClaw in [this paper](http://epubs.siam.org/doi/abs/10.1137/110856976) (free version [here](http://arxiv.org/abs/1111.6583).

In this notebook, we explore some basic PyClaw functionality.  Before running the notebook, you should [install Clawpack](http://clawpack.github.io/doc/installing.html).  The quick way is to just

    pip install clawpack

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from clawpack import pyclaw
from clawpack import riemann
```

## Setting up a problem
To solve a problem, we'll need to create the following:

- A controller, which handles the running, output, and can be used for plotting (you don't absolutely need a controller, but it makes life simpler)
- A solver, which is responsible for actually evolving the solution in time.  Here we'll need to specify the equations to be solved and the boundary conditions.
- A domain over which to solve the problem
- A solution, where we will provide the initial data.  After running, the solution will contain -- you guessed it! -- the solution.

Let's start by creating a controller and specifying the simulation end time:

```python
claw = pyclaw.Controller()
claw.tfinal = 1.0

claw.keep_copy = True       # Keep solution data in memory for plotting
claw.output_format = None   # Don't write solution data to file
claw.num_output_times = 50  # Write 50 output frames
```

## Riemann solvers

Like many solvers for nonlinear hyperbolic PDEs, PyClaw uses Riemann solvers.  By specifying a Riemann solver, we will specify the system of PDEs that we want to solve.

Place your cursor at the end of the line in the box below and hit 'Tab' (for autocompletion).  You'll see a dropdown list of all the Riemann solvers currently available in PyClaw.  The ones with 'py' at the end of the name are written in pure Python; the others are Fortran, wrapped with f2py.

Note that this won't work if you're viewing the notebook online as HTML; you need to actually be running it.

```python
riemann.
```

We'll solve the one-dimensional acoustics equations:
$$\begin{align}
p_t + K u_x & = 0 \\
u_t + \frac{1}{\rho} p_x & = 0.
\end{align}$$
Here $p, u$ are the pressure and velocity as functions of $x,t$, while $\rho, K$ are constants representing the density and bulk modulus of the material transmitting the waves.  We'll specify these constants later.

We can do this using the first solver in the list.  Notice that the solver we create here belongs to the controller that we created above.

```python
riemann_solver = riemann.acoustics_1D
claw.solver = pyclaw.ClawSolver1D(riemann_solver)
```

We also need to specify boundary conditions.  We'll use periodic BCs, so that waves that go off one side of the domain come back in at the other:

```python
claw.solver.all_bcs = pyclaw.BC.periodic
```

## The problem domain
Next we need to specify the domain and the grid.  We'll solve on the unit line $[0,1]$ using 100 grid cells.  Note that each argument to the Domain constructor must be a tuple:

```python
domain = pyclaw.Domain( (0.,), (1.,), (100,))
```

## The initial solution
Next we create a solution object that belongs to the controller and extends over the domain we specified:

```python
claw.solution = pyclaw.Solution(claw.solver.num_eqn,domain)
```

The initial data is specified in an array named $q$.  The pressure is contained in `q[0,:]` and the velocity in `q[1,:]`.  We'll specify a wavepacket for the pressure and zero velocity.

```python
x=domain.grid.x.centers
bet=100; gam=5; x0=0.75
claw.solution.q[0,:] = np.exp(-bet * (x-x0)**2) * np.cos(gam * (x - x0))
claw.solution.q[1,:] = 0.

plt.plot(x, claw.solution.q[0,:],'-o')
```

## Problem-specific parameters
The Riemann solver we've chosen requires some physical parameters to be specified.  Press 'Tab' in the box below and you'll see what they are.

```python
riemann_solver.cparam.
```

Two of these parameters are $\rho$ and $K$ in the equations above.  The other two are the impedance $Z = \sqrt{\rho K}$ and sound speed $c = \sqrt{K/\rho}$.  We specify these parameters in a dictionary that belongs to the solution object:

```python
import numpy as np

density = 1.0
bulk_modulus = 1.0
impedance = np.sqrt(density*bulk_modulus)
sound_speed = np.sqrt(density/bulk_modulus)

claw.solution.state.problem_data = {
                              'rho' : density,
                              'bulk': bulk_modulus,
                              'zz'  : np.sqrt(density*bulk_modulus),
                              'cc'  : np.sqrt(bulk_modulus/density)
                              }
```

Finally, let's run the simulation.

```python
status = claw.run()
```

## Plotting
Now we'll plot the results, which are contained in `claw.frames[:]`.  It's simple to plot a single frame with matplotlib:

```python
pressure = claw.frames[50].q[0,:]
plt.plot(x,pressure,'-o')
```

To examine the evolution more thoroughly, it's nice to see all the frames in sequence.  We can do this as follows.

**NOTE:** The JSAnimation does not work below.  If you execute this cell and try to start the animation, the javascript goes into an infinite loop.

```python
from matplotlib import animation
import matplotlib.pyplot as plt
from clawpack.visclaw.JSAnimation import IPython_display
import numpy as np

fig = plt.figure()
ax = plt.axes(xlim=(0, 1), ylim=(-0.2, 1.2))

frame = claw.frames[0]
pressure = frame.q[0,:]
line, = ax.plot([], [], lw=2)

def fplot(frame_number):
    frame = claw.frames[frame_number]
    pressure = frame.q[0,:]
    line.set_data(x,pressure)
    return line,

animation.FuncAnimation(fig, fplot, frames=len(claw.frames), interval=30)
```

That's it!  Here are some things you might try for fun:

- Change the boundary conditions to reflecting or outflow (hint: type `pyclaw.BC.`+[Tab] to get a list of boundary conditions available).
- Change the grid to use a larger or smaller number of grid cells.  How does this affect the final solution?
- Use higher-order methods by instantiating a `SharpClawSolver1D` instead of a `ClawSolver1D`.  How does this affect the final solution?  You can read more about the methods in SharpClaw in [this paper](http://epubs.siam.org/doi/abs/10.1137/110830320).

```python

```


```
# default_exp kelly_criterion
```

# Kelly Criterion

> Exploration into Kelly Criterion

```
%matplotlib inline

from ipywidgets.widgets import interact
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp
import sympy.stats as ss
import pandas as pd

plt.rcParams['figure.figsize'] = 10, 6
sp.init_printing()
```

# Exploring the Kelly criterion

Kelly criterion according to Wikipedia
$$ k = \frac{\text{expected net winnings}}{\text{net winnings if you win}}
     = \frac{pg - (1-p)l}{g}
$$
Defining $b = g/l$ and dividing nominator and denominator by $l$:
$$   k = \frac{pb - (1-p)}{b} = p - \frac{1 - p}{b}
       = \frac{p(b+1) - 1}{b}
$$

### Derivation

**Warning: some sign confusion about $r_l$ ahead**

Iteration: Starting with value $v_0 = 1$, we iterate

$$
v_{n+1} = v_{n} (1 + l r_w)^{W_1} (1 - l r_l)^{L_1}
$$

Where $W_1, L_1 \in [0, 1]$, depending on won or lost,
$l$ fraction of total funds bet, $r_w$, $r_l$ fraction of bet payed out or discarded (win/loss).

$$
v_{n} = v_{0} (1 + l r_w)^{W} (1 - l r_l)^{L} \approx (1 + l r_w)^{pN} (1 - l r_l)^{(1-p)N}
$$
$$
l^* = \operatorname*{argmax}_l \frac{v_n}{v_0} =
\operatorname*{argmax}_l \log\frac{v_n}{v_0} = \operatorname*{argmax}_l ( pN \log(1 + l r_w) + (1-p)N \log(1 - l r_l))
$$
$$
l^* = -\frac{pr_w+ (1-p)r_l}{r_lr_w} = -\frac{p}{r_l} - \frac{1-p}{r_w}
$$

Compared to the Wikipedia result, we see that the Wikipedia result is a special case for $r_l = -1$ (the whole bet is forfeited at loss).

Note that we did NOT introduce the logarithm by external intuition. It is merely a mathematical helper for easier solution of the argmax problem! In our derivation, we maximize the gain in each round. But optimizing "the gain" is a bit sloppy language, that we will clean up using the random variable formalism down below.

# Extensions to multiple outcomes on the same bet

We go from (win, loss) to a continuum of outcomes, e.g. 20% full loss, 20% half-loss, 40% half-win, 20% full-win. In this case, the argmax equation
is still easy to construct:

$$
l^* = \operatorname*{argmax}_l \frac{v_n}{v_0} =
 \operatorname*{argmax}_l \sum_i p_i \log(1 + l r_i)
$$

Where the $i$'s go over all possible outcomes of a single game. This is not analytically solvable for $l^*$ any more. Fortunately, numerical solutions are easy to find.

Let's consider a game with outcomes $r \in \{-1, -0.5, 0, 1, 2\}$, all with equal probability $p = p_i = 0.2$.

```
class Kelly:
    def __init__(self, returns, probabilities):
        self.returns = returns
        self.probabilities = probabilities

        try:
            self.min_return = min(ri for ri, pi in zip(returns, probabilities) if pi != 0)
            self.max_return = max(ri for ri, pi in zip(returns, probabilities) if pi != 0)

            self.min_l = -1 / self.max_return
            self.max_l = -1 / self.min_return
        except Exception:
            pass

        self.l = sp.symbols('l')
        self.G = sum(pi*sp.ln(1+self.l * ri) for ri, pi in zip(returns, probabilities))
        self.G1 = sp.diff(self.G, self.l)
        self.G2 = sp.diff(self.G1, self.l)

    def solve(self):
        l_solution = sp.nsolve(self.G1, 0.5)
        G_solution = self.G.subs(self.l, l_solution)

        assert sp.im(G_solution) == 0, "Could not find a solution over the reals"
        assert self.G2.subs(self.l, l_solution) < 0, "Found a stationary point, but no maximum."

        return float(l_solution), sp.exp(G_solution)

    def plot(self, rounds=1):
        y = sp.exp(self.G) ** rounds
        sp.plotting.plot(y,
                         (self.l, self.min_l, self.max_l),
                         axis_center=(self.min_l, 0),
                         xlabel='Fraction of available money bet (l)',
                         ylabel='Averaged ROI')

    def _iteration_step(self, v, l):
        ret = np.random.choice(self.returns, p=self.probabilities, size=v.shape)
        return v * (1 + l * ret)

    def simulate(self, l, num_players, rounds):
        valuations = np.ones((num_players, rounds + 1))
        for i in range(rounds):
            valuations[:, i+1] = self._iteration_step(valuations[:, i], l)
        return valuations

p = [0.2] * 5
r = [-1, -0.5, 0, 1, 2]

k = Kelly(r, p)
print("ROI of one round:")
k.plot()
print("ROI of 250 rounds:")
k.plot(250)
print('Optimal l: %.03f, expected ROI per round: %.03f' % k.solve())
```

```
Kelly([-0.5,0.5], [0.4, 0.6]).plot()
```

```
def cumulative_plot(row, title=None):
    bins = 200
    xmin, xmax = -0.5, 2
    fig = plt.figure(figsize=(10,6))
    ax = fig.add_subplot(111)
    ax.hist(row, bins=np.linspace(xmin, xmax, bins), density=True)
    ax.set_xlim(xmin, xmax)
    ax.axes.get_yaxis().set_visible(False)
    if title:
        ax.set_title(title)
    return fig

def plot_simulation(k, l, num_players, rounds, title=None):
    arr = k.simulate(l, num_players, rounds)
    result = arr[:, -1]**(1/rounds)
    fig = cumulative_plot(result, title=title)
    print("Mean: %.04f, Median: %.04f, kelly-l: %0.02f, gain: %0.04f" % (
        np.mean(result),
        np.median(result),
        k.solve()[0],
        k.solve()[1],
    ))
    return fig

def plot_simulation_rp(l, rounds):
    plot_simulation(Kelly(r,p), l, num_players=1000, rounds=rounds)

interact(plot_simulation_rp, l=(0, 1, 0.05), rounds=(1, 500));
```

    interactive(children=(FloatSlider(value=0.0, description='l', max=1.0, step=0.05), IntSlider(value=250, descri…

```
def plot_simulation_binary(l, p, r0, r1, rounds):
    k = Kelly([r0, r1], [p, 1-p])
    plot_simulation(k, l, num_players=1000, rounds=rounds)

interact(plot_simulation_binary, l=(0, 1, 0.05), p=(0., 1., 0.1), r0=+1.0, r1=-1.0, rounds=(1, 500));
```

    interactive(children=(FloatSlider(value=0.0, description='l', max=1.0, step=0.05), FloatSlider(value=0.5, desc…

```
def plot_simulation_binary_video(rounds):
    l = 0.3
    p = 0.8
    r0 = 0.4
    r1 = -1
    k = Kelly([r0, r1], [p, 1-p])
    fig = plot_simulation(k, l, num_players=1000, rounds=rounds,
                          title="Round %d" % rounds)

plot_simulation_binary_video(10)
```

# Playing around with random variables and expectation values

We regard $\frac{v_{n+1}}{v_n}$ as a random variable in its own right, depending on a random variable $X$, the outcome of our game. If $X = -1$ all bet money is lost, $X=0$ represents no gain/loss, $X=3$ means: we get our bet money + 3 times as much.

Calculating expectation value of $\frac{v_{n+1}}{v_n}$:

$$
f_1 = \frac{v_{n+1}}{v_n} = (1-l) + l(1+X) = 1 + lX
$$

```
l = sp.Symbol('l')
f1 = lambda Z: 1+l*Z  # f1 = 1-l + l(1+Z) = 1 + lZ

# X is the outcome of the gamble, with 70% chance you loose all, with 30% you win 3x your bet
X = ss.FiniteRV('X', {-1: 0.7, 3:0.3})

ss.E(f1(X))
```

So the expectation value is increasing linearly with $l$ - the more you can invest, the more you will gain - interesting. This is in contrast to
what Kelly tells us to do.

### Equivalence between linear and exponential representation

We define a new Bernoulli random variable $Y$, which captures only the probability (30% $1$, 70% $0$). We encode the outcomes (-1, 3) into our formula $f_2$ instead of into the random variable:

```
Y = ss.Bernoulli('Y', 0.3)
f2 = (1+l*3)**Y * (1-l)**(1-Y)
ss.E(f2)
```

```
#ss.density(f1)
```

```
ss.density(f2)
```

```
#ss.density(f1) == ss.density(f2)
```

Thus, the two formulations $f_1, f_2$ describe of the same phenomenon and lead to the same outcomes.

### Multiple rounds of games

Let's look at the density of $\frac{v_{n+10}}{v_n}$:

```
ss.density(f1(X)**10)
```

**Eek! We need to be careful here. The same X value was taken in each multiplication, missing the mixed powers. This is wrong!**

So we need to take care not to take the power of $f1$ which would mean taking exactly the same $X$ (same outcome). We need multiple $X_i$, which should be drawn from the same distribution, but have differing outcomes.
$$
\prod_{i=1}^{N} (1+lX_i) \ne (1+lX)^N
$$

Let's see how it looks with independent x_i.

```
N = 10
fN = sp.prod([f1(ss.FiniteRV('X_%d' % i, {-1: 0.7, 3:0.3})) for i in range(N)])
fN
```

This gives us the correct density which includes mixed powers:

```
dens_fN = ss.density(fN)
dens_fN
```

```
sp.plot(ss.E(fN), (l, -1, 1), axis_center=(0,1.0));
```

```
sp.expand(ss.E(fN))
```

$E(f_N)$ is still growing with $l$?!

Let's explore the density in more detail:

```
def density_df(dens_fn, l_actual):
    dens_fn_concrete = [(k.subs(l, l_actual), v) for k, v in dens_fn.dict.items()]
    df = pd.DataFrame(sorted(dens_fn_concrete), columns=('outcome', 'probability'))
    df = df.apply(lambda series: series.apply(float))
    df['outcome'] = df.outcome.apply(lambda v: max(v, 1E-3))
    df = df.groupby(df.outcome).sum().reset_index()
    df['E_contribution'] = df.prod(axis=1)
    return df

def plot_density(l_actual):
    df = density_df(dens_fN, l_actual)
    df.plot.scatter('outcome', 'probability', logx=True, xlim=[1E-4, 1E5])

    sum_series = df.sum()
    sum_series.name = 'SUM'
    return df.append(sum_series)
    return df

interact(plot_density, l_actual=(0.0, 1.0, 0.01));
```

    interactive(children=(FloatSlider(value=0.5, description='l_actual', max=1.0, step=0.01), Output()), _dom_clas…

Here, SUM(E_contribution) is the expectation value.

As can be seen, the highest contributors are the entries with low probabilities.
As $l$ increases, the largest outcomes start to dominate, despite their miniscule probabilities.

Because of the correspondence between expectation value and center of mass, we can think of the highest outcome as a sand grain (small probability) sitting on an extremely long lever arm (large outcome), which tips the balance of mass to the right.

This seems somehow related to the [St. Petersburg paradox](https://en.wikipedia.org/wiki/St._Petersburg_paradox). Another way to look at it is to say that the expecation value doesn't correspond to our intuition, since it weighs improbable events too heavily.

### Understanding the dichotomy between expectation value and Kelly

Since the expectation value doesn't lead to a sensible $l$, how did we come up with our Kelly formula for $l^*$ in the introduction? The tricky step was that we did not maximize the expectation value, but the mode of the probability distribution. The key tricky step is here:

$$ v_{n} = v_{0} (1 + l r_w)^{W} (1 - l r_l)^{N-W} $$
$$ v_{n} \approx (1 + l r_w)^{pN} (1 - l r_l)^{(1-p)N} $$

In the first line, $W$ can of course be interpreted as the *number* of wins in the $N$ games played. But crucially, it can also be regarded as a *random variable* describing the number of games won! So the first equation can be seen as a random variable $v_n$ being defined!

In contrast, in the second equation, we have substituted the variable $W$ for its mode $pN$! This resonates with

# Multiple investment opportunities

Simple case:
$$
v_{n+1} = v_n \prod_{i} (1 + l r_i)^{p_i^*} = v_n (1+lr_w)^W (1+lr_l)^L = v_n + v_n l r_w W + v_n l r_l L = (1-l)v_n + lv_n + v_n (l r_w W + l r_l L)
$$
$$
= (1-l)v_n + lv_n (1 + r_w W + r_l L) = (1-l) v_n + lv_n (1+r_w)^W (1+r_l)^L = v_n \left( (1-l) + l (1+r_w)^W (1+r_l)^L \right)
$$
$$
= v_n \left( (1-l) + l R_w^W R_l^L \right)
$$
If the money we don't invest ($v_n(1-l)$) gives us rent $R_0$, where $p_i^* \in \{0,1\}$ is 1 for the single outcome $i$ that manifests.
$$
\frac{v_{n+1}}{v_n} = R_0 - lR_0 + l \prod_i R_i^{p_i^*} = R_0 + l \prod_i (R_i - R_0)^{p_i^*}
$$
If we have multiple games we can play in simultaneously, we get:
$$
\frac{v_{n+1}}{v_n} = R_0 + \sum_j \left( l_j \prod_i (R_i - R_0)^{p_i^*} \right)
$$
Thinking about it some more, we can also define $R_j^*$ as the outcome of investment $j$, simplifying things.
$$
\frac{v_{n+1}}{v_n} = R_0 + \sum_j l_j (R_j^* - R_0)
$$
In this formula, we can also interpret $R_j^*$ not as outcome, but as random variable, turning $v_{n+1}/v_n$ into a random variable as well.

$$ $$

Requirements on $l_j$: depends. Must be greater or equal to zero (?), otherwise it should mean something like "offer this deal to someone else acting as bank",
which is most likely invalid in this scenario.  $\sum l_j \in [0, 1]$, otherwise you would borrow money to invest, changing $R_0$.

$$ $$

Kelly tells us to optimize for the largest gain of $v_N/v_0$ for large $N$:

$$
\frac{v_N}{v_0} = \left( R_0 + \sum_j l_j (R_j^* - R_0) \right)^N
$$

Formulated a bit more rigurously in terms of random variables $R_{i, j}$ describing the return of the $j$'th stock in the $i$'th round of investment:
$$
\frac{v_N}{v_0} = \prod_i^N \left( R_0 + \sum_j l_j (R_{i,j} - R_0) \right) = \prod_i^N \left( R_0 + \vec l \cdot (\vec R_i - R_0 \vec 1) \right)
$$

Similarly to the previous discussion, lets look for the mode of $\vec R_i$ --- the most likely outcome will be amplified by the iteration. We can use the same approach as before to split returns and probabilities:

$$
\frac{v_N}{v_0} = \prod_i \left( R_0 + \vec l \cdot (\vec R_i - R_0 \vec 1) \right)^{p(\vec R_i)N}
$$

Here, again, our interpretation changed: $\vec R_i$ are now all possible outcomes of the investments, with associated $p(\vec R_i)$, and the product goes over the whole "outcome space", instead of over the iteration count. $p(\vec R_i)N$ is the number of times the outcome $R_i$ manifests in the $N$ iterations.

$$
l^* = \operatorname*{argmax}_l \frac{v_N}{v_0} = \operatorname*{argmax}_l \log \frac{v_N}{v_0} =
 \operatorname*{argmax}_l \sum_i p(\vec R_i) \log \left( R_0 + \vec l \cdot (\vec R_i - R_0 \vec 1) \right)
$$

$$
\vec 0 = \sum_i p(\vec R_i) \frac{(\vec R_i - R_0 \vec 1)}{R_0 + \vec l \cdot (\vec R_i - R_0 \vec 1)}
$$

This is not directly soluble for $l$, but we can probably sample or something.

## References
* [Kelly Criterion by Christian Aichinger](https://greek0.net/blog/2018/04/16/kelly_criterion1/)


In this example, an equation for a contour of an axially symmetric
beam propagating in free space is checked.

Suppose there is an axially symmetric charged particles beam propagating
in free space.
Under certain set of approximations, trajectory of a particle on the edge
of the beam can be described by the following expressions:

<!--  -->
\begin{align}
  & r = r(0) e^{u^2}
  \\
  & z =
  \left( \dfrac{m v^3}{q I} \right)^{1/2}
  r(0)
  \int_{0}^{u} e^{u^2} du
\end{align}

where $u$ is a parameter, $v$ is speed of particles along the $z$ axis,
$r(0)$ is initial radius and $I$ is full current of the beam.
Derivation of these relations can be found in supplementary notebook [link].

These equations define a contour of the beam $r(z)$. It is possible to perform
a numerical simulation of a beam propagating in free space and
plot particles coordinates on the X-Z or Y-Z plane.
This will allow to compare the numerical profile
with the analytical expression.

The beam will become $e = 2.71$ times wider than the initial at $u = 1$.
For this value of the parameter, the integral in the $z$ equation
[approximately equals to](https://www.wolframalpha.com/input/?i=integrate%28+e^u^2,+u,+0,+1+%29) $1.46$.
Since the speed in the $z$-direction is assumed constant, it is possible to estimate the time needed for
particles to pass this distance as

<!--  -->
\begin{align}
  t \approx
  1.5 \left( \dfrac{m v}{q I} \right)^{1/2}
  r(0)
\end{align}

Let's substitute some numerical values (see the cell below). Suppose there is an electron beam ($q = 4.8 \cdot 10^{-10} ~ [\mbox{cgs}], ~ m = 9.1 \cdot 10^{-28} ~ [\mbox{g}]$) with full current $I = 0.1 ~ [\mbox{A}] = 2.998 \cdot 10^{8} ~ [\mbox{cgs}]$ and initial radius $r(0) = 0.5 ~ [\mbox{cm}]$.
Energy of it's particles equals to 1 keV, so that speed is $v = 1.808 \cdot 10^{9} ~ [\mbox{cm/s}]$.
For this beam to get $2.71$ times wider (from $0.5 ~ [\mbox{cm}]$ to $1.36 ~ [\mbox{cm}]$), it takes $t = 2.63 \cdot 10^{-9} ~ [\mbox{s}]$ seconds.
This happens over a distance $z = 4.632 ~ [\mbox{cm}]$.

```python
from math import *

m = 9.8e-28
q = 4.8e-10
print( "q = {:.3e} [cgs]".format( q ) )
print( "m = {:.3e} [g]".format( m ) )

r_0 = 0.5
print( "beam_radius = {:.3e} [cm]".format( r_0 ) )

ampere_to_cgs = 2997924536.8431
I = 0.1 * ampere_to_cgs
print( "I = {:.3e} [A] = {:.3e} [cgs]".format( I / ampere_to_cgs, I ) )

ev_to_cgs = 1.60218e-12
E = 1000 * ev_to_cgs
v = sqrt( 2 * E / m )
print( "E = {:.3e} [eV] = {:.3e} [erg]".format( E / ev_to_cgs, E ) )
print( "v = {:.3e} [cm/s]".format( v ) )

r0_e_times_wider = e * r_0
z_e_times_wider = 1.46 * sqrt( m * v**3 / q / I ) * r_0
t_e_times_wider = 1.5 * sqrt( m * v / q / I ) * r_0
print( "r0_e_times_wider = {:.3e} [cm]".format( r0_e_times_wider ) )
print( "t_e_times_wider = {:.3e} [s]".format( t_e_times_wider ) )
print( "z_e_times_wider = {:.3e} [cm]".format( z_e_times_wider ) )
```

    q = 4.800e-10 [cgs]
    m = 9.800e-28 [g]
    beam_radius = 5.000e-01 [cm]
    I = 1.000e-01 [A] = 2.998e+08 [cgs]
    E = 1.000e+03 [eV] = 1.602e-09 [erg]
    v = 1.808e+09 [cm/s]
    r0_e_times_wider = 1.359e+00 [cm]
    t_e_times_wider = 2.632e-09 [s]
    z_e_times_wider = 4.632e+00 [cm]

These estimates can be used as a basis for config file parameters.

Full simulation time is set to 3.0e-9 [s]. There are 100 time steps, with each 10th step written to file.

```python
from ef.util.inject import configure_application
configure_application(solver="amgx", backend="cupy")

from ef.config.components import *

sim_time = 3.0e-9
n_of_steps = 100
dt = sim_time / n_of_steps
save_each_step = 10
dt_save = dt * save_each_step
time_grid = TimeGridConf(total=sim_time, step=dt, save_step=dt_save)
print(time_grid)
```

    ### TimeGridConf:
    total = 3e-09
    save_step = 3e-10
    step = 3e-11

The simulation domain is defined as

```python
mesh = SpatialMeshConf(size=(5, 5, 10), step=(.1, .1, .1))
print(mesh)
```

    ### SpatialMeshConf:
    size = array([ 5,  5, 10])
    step = array([0.1, 0.1, 0.1])

with z size approximately two times the estimated value, with 100 nodes in that direction. X and y sizes are 10 times the radius with 50 nodes in each direction.

The source is centered along the x and y axes and is close to origin along the z. For a time step dt = 3.00e-11 [s], to provide a current I = 0.1 [A] the source has to generate $n = I ~ dt ~/~ q = 1.87 \cdot 10^7$ particles each time step. This is not computationally feasible, so instead let's fix an amount of generated particles to 5000 at each step. The charge of macroparticles should be $Q = I ~ dt ~/~ n = 1.799 \cdot 10^{-6} ~ [\mbox{cgs}]$ To preserve charge-to-mass ratio, mass of the macroparticles should be set to $M = Q ~/~ q_e ~ m_e = 3.672 \cdot 10^{-24} ~ [\mbox{g}]$. To have the same initial velocity as electrons, mean momentum should be set to $p = M ~ v = 6.641 \cdot 10^{-15} ~ [\mbox{g * cm / s}]$.

```python
num_of_real_particles = I * dt / q
print( "num_of_real_particles = {:.3e}".format( num_of_real_particles ) )

num_of_macro_particles = 5000
macro_q = I * dt / num_of_macro_particles
macro_m = macro_q / q * m
macro_mean_momentum = macro_m * v
print( "num_of_macro_particles = {:d}".format( num_of_macro_particles ) )
print( "macro_q = {:.3e} [cgs]".format( macro_q ) )
print( "macro_m = {:.3e} [g]".format( macro_m ) )
print( "macro_mean_momentum = {:.3e} [g * cm / s]".format( macro_mean_momentum ) )

source = ParticleSourceConf(
    name = "cathode_emitter",
    shape = Cylinder(start=(mesh.size[0]/2., mesh.size[1]/2., 0.52),
                     end=(mesh.size[0]/2., mesh.size[1]/2., 0.51),
                     radius=0.5),
    initial_particles = num_of_macro_particles,
    particles_to_generate_each_step = num_of_macro_particles,
    momentum = (0, 0, macro_mean_momentum),
    temperature = 0.0,
    charge = -macro_q,
    mass = macro_m
)
```

    num_of_real_particles = 1.874e+07
    num_of_macro_particles = 5000
    macro_q = 1.799e-06 [cgs]
    macro_m = 3.672e-24 [g]
    macro_mean_momentum = 6.641e-15 [g * cm / s]

```python
from ef.config.config import Config
from ef.config.visualizer import Visualizer3d
axially_symmetric_beam_conf = Config(time_grid, mesh, sources=[source])
vis = Visualizer3d()
axially_symmetric_beam_conf.visualize_all(vis)
```

    <Figure size 640x480 with 1 Axes>

Potential is zero on each boundary of the domain (by default).

Other config parameters:

```python
output_file = OutputFileConf(prefix = "contour_jup_", suffix = ".h5")
particle_interaction_model = ParticleInteractionModelConf(model = "PIC")
```

```python
axially_symmetric_beam_conf = Config(time_grid, mesh, sources=[source],
                                     particle_interaction_model=particle_interaction_model,
                                     output_file=output_file)
print(axially_symmetric_beam_conf.export_to_string())
```

    [TimeGrid]
    total_time = 3e-09
    time_save_step = 3e-10
    time_step_size = 3e-11

    [SpatialMesh]
    grid_x_size = 5
    grid_y_size = 5
    grid_z_size = 10
    grid_x_step = 0.1
    grid_y_step = 0.1
    grid_z_step = 0.1

    [ParticleSourceCylinder.cathode_emitter]
    cylinder_axis_start_x = 2.5
    cylinder_axis_start_y = 2.5
    cylinder_axis_start_z = 0.52
    cylinder_axis_end_x = 2.5
    cylinder_axis_end_y = 2.5
    cylinder_axis_end_z = 0.51
    cylinder_radius = 0.5
    initial_number_of_particles = 5000
    particles_to_generate_each_step = 5000
    mean_momentum_x = 0.0
    mean_momentum_y = 0.0
    mean_momentum_z = 6.640708217582056e-15
    temperature = 0.0
    charge = -1.7987547221058602e-06
    mass = 3.672457557632798e-24

    [OutputFilename]
    output_filename_prefix = contour_jup_
    output_filename_suffix = .h5

    [BoundaryConditions]
    boundary_phi_right = 0.0
    boundary_phi_left = 0.0
    boundary_phi_bottom = 0.0
    boundary_phi_top = 0.0
    boundary_phi_near = 0.0
    boundary_phi_far = 0.0

    [ParticleInteractionModel]
    particle_interaction_model = PIC

```python
from ef.config.config import Config
from ef.config.visualizer import Visualizer3d
axially_symmetric_beam_conf.visualize_all(Visualizer3d())
```

```python

from ef.runner import Runner
Runner(axially_symmetric_beam_conf.make(),
       output_writer=axially_symmetric_beam_conf.output_file.make()).start()
```

    Writing initial fields to file
    Writing to file contour_jup_fieldsWithoutParticles.h5
    Writing step 0 to file
    Writing to file contour_jup_0000000.h5
    Time step from 9 to 10 of 100
    Writing step 10 to file
    Writing to file contour_jup_0000010.h5
    Time step from 19 to 20 of 100
    Writing step 20 to file
    Writing to file contour_jup_0000020.h5
    Time step from 29 to 30 of 100
    Writing step 30 to file
    Writing to file contour_jup_0000030.h5
    Time step from 39 to 40 of 100
    Writing step 40 to file
    Writing to file contour_jup_0000040.h5
    Time step from 49 to 50 of 100
    Writing step 50 to file
    Writing to file contour_jup_0000050.h5
    Time step from 59 to 60 of 100
    Writing step 60 to file
    Writing to file contour_jup_0000060.h5
    Time step from 69 to 70 of 100
    Writing step 70 to file
    Writing to file contour_jup_0000070.h5
    Time step from 79 to 80 of 100
    Writing step 80 to file
    Writing to file contour_jup_0000080.h5
    Time step from 89 to 90 of 100
    Writing step 90 to file
    Writing to file contour_jup_0000090.h5
    Time step from 99 to 100 of 100
    Writing step 100 to file
    Writing to file contour_jup_0000100.h5

```python
import h5py
import numpy as np
import matplotlib.pyplot as plt
import scipy.integrate

filename = 'contour_jup_0000100.h5'
h5file = h5py.File( filename, mode = "r" )

def get_source_current( h5file ):
    time_step = h5file["/TimeGrid"].attrs["time_step_size"]
    charge = h5file["/ParticleSources/cathode_emitter"].attrs["charge"]
    particles_per_step = h5file[
        "/ParticleSources/cathode_emitter"].attrs["particles_to_generate_each_step"]
    current = particles_per_step * charge / time_step
    return current

def get_source_geometry( h5file ):
    axis_start_x = \
        h5file["/ParticleSources/cathode_emitter"].attrs["cylinder_axis_start_x"]
    axis_start_z = \
        h5file["/ParticleSources/cathode_emitter"].attrs["cylinder_axis_start_z"]
    radius = h5file["/ParticleSources/cathode_emitter"].attrs["cylinder_radius"]
    return ( axis_start_x, axis_start_z, radius )

def get_source_particle_parameters( h5file ):
    mass = h5file["/ParticleSources/cathode_emitter"].attrs["mass"]
    charge = h5file["/ParticleSources/cathode_emitter"].attrs["charge"]
    momentum_z = h5file["/ParticleSources/cathode_emitter"].attrs["mean_momentum_z"]
    return ( mass, charge, momentum_z )

def beam_radius( u, r_0 ):
    return r_0 * np.exp( u ** 2 )

def beam_z( u, m, v, q, I, r_0 ):
    coeff = np.sqrt( m * v**3 / q / I ) * r_0
    subint = lambda t: np.exp( t * t )
    low_lim = 0
    up_lim = u
    integral_value = scipy.integrate.quad( subint, low_lim, up_lim )[0]
    return coeff * integral_value

beam_axis_x_pos, emitter_z_pos, r_0 = get_source_geometry( h5file )
I = get_source_current( h5file )
m, q, p = get_source_particle_parameters( h5file )
v = p / m

u_min = 0; u_max = 2; num_u_points = 100  # for u = 1, r = r(0) * 2.71812
u = np.linspace( u_min, u_max, num_u_points )
r_an = [ beam_radius( x, r_0 ) for x in u ]
r_an_upper = r_an + beam_axis_x_pos
r_an_lower = beam_axis_x_pos - r_an
z_an = [ beam_z( x, m = m, v = v, q = q, I = I, r_0 = r_0 ) for x in u ]
z_an = z_an + emitter_z_pos

r_num = h5file["/ParticleSources/cathode_emitter/position_x"]
z_num = h5file["/ParticleSources/cathode_emitter/position_z"]

z_volume_size = h5file["/SpatialMesh"].attrs["z_volume_size"]
x_volume_size = h5file["/SpatialMesh"].attrs["x_volume_size"]
plt.xlabel( "Z [cm]" )
plt.ylabel( "X [cm]" )
plt.ylim( 0, x_volume_size )
plt.xlim( 0, z_volume_size )
plt.plot( z_num, r_num, '.', label = "num" )
plt.plot( z_an, r_an_upper, label = "theory", color = "g" )
plt.plot( z_an, r_an_lower, color = "g" )
plt.legend()
h5file.close()

#plt.savefig( "beam_contour.png" )
plt.show()
```

```python

```


```python
import numpy as np
import sympy as sym
import matplotlib as mpl
from matplotlib import pyplot as plt
import scipy.integrate as integrate
from scipy.integrate import quad, dblquad
from scipy.special import ellipe
from scipy.special import ellipk

G = 4.300e-6                                        #gravitational constant (kpc/solar mass*(km/s)^2)
h = 8.9                                           #radial scale-length (kpc) [FOR NGC 5533]
#d = 0.99                                            #cut-off length upper limits(kpc)
d = 0.2*h
mu0 = 1.85e9                                        #central surface density [FOR NGC 5533]
z0 = 0.2*h                                          #half-thickness (kpc)
R = 4*h                                             #cut-off radius (kpc)

#mass distribution
rho00 = mu0/(2*z0)

def rho0(r, R, h, d):
    condlist = [r <= R, (r > R) & (r <= (R+d)), r > (R+d)]
    funclist = [lambda r: rho00*np.exp(-r/h), lambda r: rho00*np.exp(-R/h)*(1-((r-R)/d)), lambda r: 0]
    return np.piecewise(r, condlist, funclist)

rho_rz = lambda r,z: rho0(r, R, h, d)*(np.power(sym.cosh(z/z0), (-2)))

intf = lambda r,z: quad(rho_rz, 0, np.inf, args=(r))[0]

MAS = ans, err = dblquad(rho_rz, 0, np.inf,
                   lambda z: 0,
                   lambda z: np.inf)
#print(ans)
```

```python
    #luminosity
L0 = 7.9e9                                                  #units: solar luminosities, from eq15 in Casertano paper

x = np.linspace(10, 50, 200)

Lrz = lambda r,z: L0*np.exp(-r/h)*(sym.cosh(z/z0))**-2

ans, err = dblquad(Lrz, 0, np.inf,
                   lambda z: 0,
                   lambda z: np.inf)
print(ans)
```

    125151800000.00006

```python
mu0 = 8096794855.39802/79210000000.0
print(ans)
```

    125151800000.00006

```python

```

binder/testing/Altering/Disk/epsilon (mass to light ratio).ipynb