# Music Machine Learning - Sampling

### Author: Philippe Esling (esling@ircam.fr)

In this course we will cover
1. Defining [sampling](#sampling) procedure with Monte-Carlo.
2. Motivating the use of [Markov chains](#markov) in sampling.
2. Implementing the [Gibbs sampling](#gibbs) algorithm.

We begin with the standard imports

```python
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()
import numpy as np
from helper_plot import hdr_plot_style
hdr_plot_style()
```

## Sampling

The main idea of sampling, is that we have a very complicated distribution, for which we do not know either the closed-form or its parameters. Although it is hard to find an adequate estimation of this distribution, we can try to solve simpler problems, by estimating _expected values_ of this distribution. To do so, we will define an _estimator_, for which we know that it will provide an accurate evaluation if we have a sufficient number of samples.

#### Example with $\pi$

A simple example to understand the underlying logic is that of trying to approximate $\pi$. We know that we have the following relations, which means that we can estimate $\pi$ as a form of expectation, by drawing _random samples_

$$
\begin{align}
\frac{\pi}{4} &= \mathbb{E}\left[ x^{2} + y^{2} \leq 1 \right] \\
&\approx \frac{1}{M} \sum_{s=1}^{M} \left[ x_{s}^{2} + y_{s}^{2} \leq 1 \right]
\end{align}
$$

with $x_{s},y_{s}\sim \mathcal{U}(0,1)$

```python
import random as r
import math as m
# Number of darts that land inside.
inside = 0
inside_pts = []
outside_pts = []
# Total number of darts to throw.
total = 10000
# Iterate for the number of darts.
for i in range(0, total):
    # Generate random x, y in [0, 1].
    x2 = r.random() ** 2
    y2 = r.random() ** 2
    # Increment if inside unit circle.
    if (x2 + y2) <= 1.0:
        inside += 1
        inside_pts.append([m.sqrt(x2), m.sqrt(y2)])
    else:
        outside_pts.append([m.sqrt(x2), m.sqrt(y2)])
# Compute estimator
pi = (float(inside) / total) * 4
print(pi)
# Plot the points
inside_pts = np.array(inside_pts)
outside_pts = np.array(outside_pts)
plt.figure(figsize=(10,8))
plt.scatter(inside_pts[:, 0], inside_pts[:, 1], s=10)
plt.scatter(outside_pts[:, 0], outside_pts[:, 1], s=10, c='r');
```

Advantages of Monte-Carlo
- Very simple to program
- Universally applicable to lots of problems
- Scalable to parallelization

Problems
- Usually very slow
- Better solutions might exist

## Formal introduction

The goal of Monte-Carlo methods is to _estimate expected values by sampling_
So we can define
$$
\mathbb{E}_{p(x)} f(x) \approx \frac{1}{M} \sum_{s=1}^{M} f(x_{s})
$$
with $x_{s} \sim p(x)$. This has nice co-properties
- This is an _unbiased_ estimator
- Guarantees on the convergence

### Why estimate expected values ?

Remember the goal of full Bayesian inference
$$
\begin{align}
p(y \vert x, Y_{train}, X_{train}) &= \int p(y \vert x, w) p(w \vert y_{train}, x_{train}) dw \\
&= \mathbb{E}_{p(w\vert Y_{train},X_{train})} p(y \vert x, w)
\end{align}
$$
Here we can use sample to simplify the problem by _sampling_

The same situation applies for the M-step of the EM-algorithm
$$
\underset{\theta}{\max} \mathbb{E}_{q} \log p(\mathbf{x},\mathbf{t} \vert \theta)
$$

## Sampling from 1d distributions

We start from the simplest case, where we have a discrete 1-d distribution. Imagine we want to sample from a Gaussian, so we can use the central limit theorem
$$
z = \sum_{i=1}^{12} x_{i} - 6
$$
If we take $x_{i} \sim \mathcal{U}[0,1]$, then we have $p(z)\approx\mathcal{N}(0,1)$.
Of course this is a rather crude approximation, we can rather use a more refined process

Now imagine we have a more complex distribution like this

```python
import math
import scipy.stats as stats
x = np.linspace(-7, 8, 100)
plt.figure(figsize=(10,8))
plt.plot(x, stats.norm.pdf(x, -1, 1.1) + stats.norm.pdf(x, 2, 0.9), linewidth = 5)
```

We can upper-bound our distribution by an approximation, for instance we have that
$$
\begin{align}
q(x) &= \mathcal{N}(1,3) \\
p(x) &\leq 2q(x)
\end{align}
$$
We use the constant $2$ as it is impossible to upper bound a distribution without multiplying it with a constant.

```python
import math
import scipy.stats as stats
x = np.linspace(-7, 8, 100)
plt.plot(x, 0.5 * stats.norm.pdf(x, -1, 1.1) + 0.5 * stats.norm.pdf(x, 2, 0.9), linewidth=3)
plt.plot(x, stats.norm.pdf(x, 1, 3) * 2, c='r', linewidth=3)
plt.savefig('sampling_approx.pdf')
```

- Now note that we can sample from this approximation $q$ such that $\tilde{x}\sim q(x)$
- So how can we sample from our distribution $p$
- We now that we have a higher probability in $q$
- Therefore we need to *reject* some of the points
- The rule to reject should be probabilistic
    - Should be proportional to the height of the curves
    - So if we sample $\tilde{x}$ and take its coordinates $y \sim \mathcal{U}[0, 2q(\tilde{x})]$.
    - We can accept $\tilde{x}$ with probability $\frac{p(x)}{2q(x)}$ if $y\leq p(x)$
How much points do we accept ? Knowing that we have $p(x)\leq Mq(x)$, we accept $\frac{1}{M}$ points on average

Note that we can even use this _rejection sampling_ method if we know the distribution up to a normalization factor as
$$
\begin{align}
\frac{\hat{p}(x)}{Z} &\leq Mq(x) \\
\hat{p}(x) &\leq ZMq(x) \\
\end{align}
$$
For instance, we can define our distributions $p$ and $q$ as follows

```python
import scipy.stats as st
def p(x):
    return st.norm.pdf(x, loc=30, scale=10) + st.norm.pdf(x, loc=80, scale=20)
def q(x):
    return st.norm.pdf(x, loc=50, scale=30)
```

Here, you can implement the algorithm for rejection sampling and test it out

```python
x = np.arange(-50, 151)
k = max(p(x) / q(x))

def rejection_sampling(iter=1000):
    samples = []

    ######################
    # YOUR CODE GOES HERE
    ######################

    return np.array(samples)

plt.plot(x, p(x))
plt.plot(x, k*q(x))
plt.show()
s = rejection_sampling(iter=10000)
sns.distplot(s);
```

Summary of rejection sampling
+ Pro : Works for most distributions
- Con : If $q$ and $p$ too different ($M$ large), rejects most points
- Con : $M$ is large for d-dimensional distributions

## Markov chains

**Here explain the principle of Markov chains**

Using Markov chain for sampling
- We want to sample from p(x)
- Build a Markov chain that converges to $p(x)$
- Start from any $x^{0}$
- For $k=0,1,\cdots$
$$
x^{k+1}\sim T(x^{k}\rightarrow x^{k+1})
$$
- Eventually $x^{k}$ will look like samples from p(x)

### Do chains always converge ?

- Imagine a binary distribution
- Will never converge
- We need chains that do converge

**Definition** : A distribution $\pi$ is called _stationary_ if
$$
\pi(x')=\sum_{x} T(x\rightarrow x') \pi(x)
$$

**Theorem** : If $T(x\rightarrow x') > 0$ for all $x, x'$, then there exists a unique $\pi$ such that
$$
\pi(x')=\sum_{x} T(x\rightarrow x') \pi(x)
$$
And the Markov chain converges to $\pi$ from any starting point.

## Gibbs sampling

One of the easiest method to construct Markov chains to sample from a distribution. Say we have a 3-d distribution that we know up to norrmalization.
$$
p(x_{1}, x_{2}, x_{3}) = \frac{\hat{p}(x_{1}, x_{2}, x_{3})}{Z}
$$
The method of Gibbs sampling starts with $(x^{0}_{1}, x^{0}_{2}, x^{0}_{3})$ that we can set to $(0,0,0)$ for instance.

Then we are going to sample each dimension at a time by sampling from the conditional
$$
\begin{align}
x^{1}_{1} \sim &p(x_{1} \vert x_{2} = x^{0}_{2}, x_{3} = x^{0}_{3}) \\
&= \frac{\hat{p}(x_{1}, x^{0}_{2}, x^{0}_{3})}{Z}
\end{align}
$$
Then we reapply the same idea for the next dimensions.
$$
\begin{align}
x^{1}_{2} &\sim p(x_{2} \vert x_{1} = x^{1}_{1}, x_{3} = x^{0}_{3}) \\
x^{1}_{3} &\sim p(x_{3} \vert x_{1} = x^{1}_{1}, x_{2} = x^{1}_{2}) \\
\end{align}
$$
Note that in this case, we cannot parallelize these sampling steps.

We can prove that Gibbs sampling indeed converges to the desired distribution $p$. So we want to prove
$$
p(x',y',z') = \sum_{x,y,z} q(x,y,z\rightarrow x',y',z') p(x,y,z)
$$

Pros :
- Reduce multidimensional sampling to sequence of 1d sampling
- A few lines of code
Cons :
- Highly correlated samples
- Slow convergence (mixing)
- Cannot be parallelized

## Metropolis-Hastings

- Sometimes Gibbs samples are too correlated
- Idea of MH is to apply rejection sampling to Markov chains
Let's start with any Markov chain $Q$
- Sample $x'$ from a _wrong_ $Q(x^{k}\rightarrow x')$ for $k=1,2,\cdots$
- Use a critic to accept proposal $x'$ with probabiility $A(x^{k}\rightarrow x')$
- Otherwise stay at $x^{k}$, such that $x^{k+1}=x^{k}$
So overall, the scheme works as
$$
\begin{align}
T(x\leftarrow x') &= Q(x\leftarrow x')A(x\leftarrow x'), \forall x\neq x'
T(x\leftarrow x) &= Q(x\leftarrow x)A(x\leftarrow x)\\
&+ \sum_{x\neq x'} Q(x\leftarrow x')(1 - A(x\leftarrow x'))
\end{align}
$$

How to choose A : $\pi(x')=\sum_{x} T(x\rightarrow x') \pi(x)$, we need to introduce the _detailed balance_ condition

## [Bonus] Gibbs Sampling implementation

The Gibbs sampler is the simplest and most prevalent MCMC algorithm. If a posterior has $k$ parameters to be estimated, we may condition each parameter on current values of the other $k-1$ parameters, and sample from the resultant distributional form (usually easier), and repeat this operation on the other parameters in turn. This procedure generates samples from the posterior distribution. Note that we have now combined Markov chains (conditional independence) and Monte Carlo techniques (estimation by simulation) to yield Markov chain Monte Carlo.

Here is a stereotypical Gibbs sampling algorithm:

1.  Choose starting values for states (parameters):
    ${\bf \theta} = [\theta_1^{(0)},\theta_2^{(0)},\ldots,\theta_k^{(0)}]$.

2.  Initialize counter $j=1$.

3.  Draw the following values from each of the $k$ conditional
    distributions:

    $$\begin{aligned}
    \theta_1^{(j)} &\sim \pi(\theta_1 | \theta_2^{(j-1)},\theta_3^{(j-1)},\ldots,\theta_{k-1}^{(j-1)},\theta_k^{(j-1)}) \\
    \theta_2^{(j)} &\sim \pi(\theta_2 | \theta_1^{(j)},\theta_3^{(j-1)},\ldots,\theta_{k-1}^{(j-1)},\theta_k^{(j-1)}) \\
    \theta_3^{(j)} &\sim \pi(\theta_3 | \theta_1^{(j)},\theta_2^{(j)},\ldots,\theta_{k-1}^{(j-1)},\theta_k^{(j-1)}) \\
    \vdots \\
    \theta_{k-1}^{(j)} &\sim \pi(\theta_{k-1} | \theta_1^{(j)},\theta_2^{(j)},\ldots,\theta_{k-2}^{(j)},\theta_k^{(j-1)}) \\
    \theta_k^{(j)} &\sim \pi(\theta_k | \theta_1^{(j)},\theta_2^{(j)},\theta_4^{(j)},\ldots,\theta_{k-2}^{(j)},\theta_{k-1}^{(j)})\end{aligned}$$

4.  Increment $j$ and repeat until convergence occurs.

As we can see from the algorithm, each distribution is conditioned on the last iteration of its chain values, constituting a Markov chain as advertised. The Gibbs sampler has all of the important properties outlined in the previous section: it is aperiodic, homogeneous and ergodic. Once the sampler converges, all subsequent samples are from the target distribution. This convergence occurs at a geometric rate.

#### Example: Inferring patterns in UK coal mining disasters

Let's try to model a more interesting example, a time series of recorded coal mining
disasters in the UK from 1851 to 1962.

Occurrences of disasters in the time series is thought to be derived from a
Poisson process with a large rate parameter in the early part of the time
series, and from one with a smaller rate in the later part. We are interested
in locating the change point in the series, which perhaps is related to changes
in mining safety regulations.

```python
disasters_array = np.array([4, 5, 4, 0, 1, 4, 3, 4, 0, 6, 3, 3, 4, 0, 2, 6,
                            3, 3, 5, 4, 5, 3, 1, 4, 4, 1, 5, 5, 3, 4, 2, 5,
                            2, 2, 3, 4, 2, 1, 3, 2, 2, 1, 1, 1, 1, 3, 0, 0,
                            1, 0, 1, 1, 0, 0, 3, 1, 0, 3, 2, 2, 0, 1, 1, 1,
                            0, 1, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 1, 1, 0, 2,
                            3, 3, 1, 1, 2, 1, 1, 1, 1, 2, 4, 2, 0, 0, 1, 4,
                            0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1])

n_count_data = len(disasters_array)
```

We are going to use Poisson random variables for this type of count data. Denoting year $i$'s accident count by $y_i$,

$$y_i \sim \text{Poisson}(\lambda).$$

For those unfamiliar, Poisson random variables look like this:

```python
plt.figure(figsize=(10, 8))
for l in [1, 5, 12, 25]:
    plt.hist(np.random.poisson(l, 1000), alpha=0.5)
```

The modeling problem is about estimating the values of the $\lambda$ parameters. Looking at the time series above, it appears that the rate declines over time.

A **changepoint model** identifies a point (here, a year) after which the parameter $\lambda$ drops to a lower value. Let us call this point in time $\tau$. So we are estimating two $\lambda$ parameters:
$\lambda = \lambda_1$ if $t \lt \tau$ and $\lambda = \lambda_2$ if $t \geq \tau$.

We need to assign prior probabilities to both $\{\lambda_1, \lambda_2\}$. The gamma distribution not only provides a continuous density function for positive numbers, but it is also *conjugate* with the Poisson sampling distribution.

```python
lambda1_lambda2 = [(0.1, 100), (1, 100), (1, 10), (10, 10)]
```

```python
plt.figure(figsize=(10, 8))
for p in lambda1_lambda2:
    plt.hist(np.random.gamma(*p, size=1000), alpha=0.5)
```

We will specify suitably vague hyperparameters $\alpha$ and $\beta$ for both priors:

\begin{align}
\lambda_1 &\sim \text{Gamma}(1, 10), \\
\lambda_2 &\sim \text{Gamma}(1, 10).
\end{align}

Since we do not have any intuition about the location of the changepoint (unless we visualize the data), we will assign a discrete uniform prior over the entire observation period [1851, 1962]:

\begin{align}
&\tau \sim \text{DiscreteUniform(1851, 1962)}\\
&\Rightarrow P(\tau = k) = \frac{1}{111}.
\end{align}

### Implementing Gibbs sampling

We are interested in estimating the joint posterior of $\lambda_1, \lambda_2$ and $\tau$ given the array of annnual disaster counts $\mathbf{y}$. This gives:

$$
 P( \lambda_1, \lambda_2, \tau | \mathbf{y} ) \propto P(\mathbf{y} | \lambda_1, \lambda_2, \tau ) P(\lambda_1, \lambda_2, \tau)
$$

To employ Gibbs sampling, we need to factor the joint posterior into the product of conditional expressions:

$$
 P(\lambda_1, \lambda_2, \tau | \mathbf{y}) \propto P(y_{t \lt \tau} | \lambda_1, \tau) P(y_{t \geq \tau} | \lambda_2, \tau) P(\lambda_1) P(\lambda_2) P(\tau)
$$

which we have specified as:

$$\begin{aligned}
P( \lambda_1, \lambda_2, \tau | \mathbf{y} ) &\propto \left[\prod_{t=1851}^{\tau} \text{Poi}(y_t|\lambda_1) \prod_{t=\tau+1}^{1962} \text{Poi}(y_t|\lambda_2) \right] \text{Gamma}(\lambda_1|\alpha,\beta) \text{Gamma}(\lambda_2|\alpha, \beta) \frac{1}{111} \\
&\propto \left[\prod_{t=1851}^{\tau} e^{-\lambda_1}\lambda_1^{y_t} \prod_{t=\tau+1}^{1962} e^{-\lambda_2} \lambda_2^{y_t} \right] \lambda_1^{\alpha-1} e^{-\beta\lambda_1} \lambda_2^{\alpha-1} e^{-\beta\lambda_2} \\
&\propto \lambda_1^{\sum_{t=1851}^{\tau} y_t +\alpha-1} e^{-(\beta+\tau)\lambda_1} \lambda_2^{\sum_{t=\tau+1}^{1962} y_i + \alpha-1} e^{-\beta\lambda_2}
\end{aligned}$$

So, the full conditionals are known, and critically for Gibbs, can easily be sampled from.

$$\lambda_1 \sim \text{Gamma}(\sum_{t=1851}^{\tau} y_t +\alpha, \tau+\beta)$$
$$\lambda_2 \sim \text{Gamma}(\sum_{t=\tau+1}^{1962} y_i + \alpha, 1962-\tau+\beta)$$
$$\tau \sim \text{Categorical}\left( \frac{\lambda_1^{\sum_{t=1851}^{\tau} y_t +\alpha-1} e^{-(\beta+\tau)\lambda_1} \lambda_2^{\sum_{t=\tau+1}^{1962} y_i + \alpha-1} e^{-\beta\lambda_2}}{\sum_{k=1851}^{1962} \lambda_1^{\sum_{t=1851}^{\tau} y_t +\alpha-1} e^{-(\beta+\tau)\lambda_1} \lambda_2^{\sum_{t=\tau+1}^{1962} y_i + \alpha-1} e^{-\beta\lambda_2}} \right)$$

Implementing this in Python requires random number generators for both the gamma and discrete uniform distributions. We can leverage NumPy for this:

```python
# Function to draw random gamma variate
rgamma = np.random.gamma

def rcategorical(probs, n=None):
    # Function to draw random categorical variate
    return np.array(probs).cumsum().searchsorted(np.random.sample(n))
```

Next, in order to generate probabilities for the conditional posterior of $\tau$, we need the kernel of the gamma density:

\\[\lambda^{\alpha-1} e^{-\beta \lambda}\\]

```python
dgamma = lambda lam, a, b: lam**(a - 1) * np.exp(-b * lam)
```

Diffuse hyperpriors for the gamma priors on $\{\lambda_1, \lambda_2\}$:

```python
alpha, beta = 1., 10
```

For computational efficiency, it is best to pre-allocate memory to store the sampled values. We need 3 arrays, each with length equal to the number of iterations we plan to run:

```python
# Specify number of iterations
n_iterations = 1000
# Initialize trace of samples
lambda1, lambda2, tau = np.empty((3, n_iterations + 1))
```

The penultimate step initializes the model paramters to arbitrary values:

```python
lambda1[0] = 6
lambda2[0] = 2
tau[0] = 50
```

Now we can run the Gibbs sampler.

```python
# Sample from conditionals
for i in range(n_iterations):

    ######################
    # YOUR CODE GOES HERE
    ######################

```

Plotting the trace and histogram of the samples reveals the marginal posteriors of each parameter in the model.

```python
plt.figure()
plt.plot(lambda1)
plt.figure()
plt.hist(lambda1)
plt.figure()
plt.plot(lambda2)
plt.figure()
plt.hist(lambda2)
plt.figure()
plt.plot(tau)
plt.figure()
plt.hist(tau)
```


```python
import sympy
sympy.init_printing()
```

```python
s = sympy.Symbol('s')
x = sympy.Symbol('x')
cf1, rf1, cc1, rc1 = sympy.symbols(r"C_f1, R_f1, C_c1, R_c1")
cf2, rf2, cc2, rc2 = sympy.symbols(r"C_f2, R_f2, C_c2, R_c2")
cf3, rf3, cc3, rc3 = sympy.symbols(r"C_f3, R_f3, C_c3, R_c3")

```

Foster matrix

_cv2, _cv3 means 2x1 column vector, and 3x1 column vector respectively.

```python
af1_0_cv2 = sympy.Matrix([rf1,0])
af1_1_cv2 = sympy.Matrix([0,rf1])
bf1_cv2 = sympy.Matrix([cf1*rf1,1])
bf1_0_cv3 = sympy.zeros(3,1); bf1_0_cv3[0:2,0] = bf1_cv2
bf1_1_cv3 = sympy.zeros(3,1); bf1_1_cv3[1:3,0] = bf1_cv2
```

```python
af1_0_cv2, af1_1_cv2, bf1_cv2, bf1_0_cv3, bf1_1_cv3
```

```python
af2_cv2 = cf2*rf2*af1_0_cv2 + af1_1_cv2 + rf2 * bf1_cv2
bf2_cv3 = cf2 * rf2 * bf1_0_cv3 + bf1_1_cv3
```

```python
af2_cv2,bf2_cv3
```

```python
af2_0_cv3 = sympy.zeros(3,1); af2_0_cv3[0:2,0] = af2_cv2
af2_1_cv3 = sympy.zeros(3,1); af2_1_cv3[1:3,0] = af2_cv2
bf2_0_cv4 = sympy.zeros(4,1); bf2_0_cv4[0:3,0] = bf2_cv3
bf2_1_cv4 = sympy.zeros(4,1); bf2_1_cv4[1:4,0] = bf2_cv3
```

```python
af2_0_cv3, af2_1_cv3, bf2_0_cv4, bf2_1_cv4
```

```python
af3_cv3 = cf3 * rf3 * af2_0_cv3 + af2_1_cv3 + rf3 * bf2_cv3
bf3_cv4 = cf3 * rf3 * bf2_0_cv4 + bf2_1_cv4
```

```python
af3_cv3, bf3_cv4
```

```python
svector3 = sympy.Matrix([s**3, s**2, s, 1])
svector2 = sympy.Matrix([s**2, s, 1])
svector2, svector3
```

```python
pf3 = sympy.Poly(sympy.transpose(af3_cv3).dot(svector2),s)
pf3
```

```python
qf3 = sympy.Poly(sympy.transpose(bf3_cv4).dot(svector3),s)
qf3
```

```python
Zf3 = pf3/qf3
Zf3
```

```python
Yf3 = 1/Zf3
Yf3
```

```python
cc3 = bf3_cv4[0]/af3_cv3[0]
cc3
```

```python
Yf3Aster = (Yf3 - cc3*s).cancel()
```

```python
(qf3_1, pf3_1) = sympy.fraction(Yf3Aster)
qf3_1 = sympy.Poly(qf3_1, s)
pf3_1 = sympy.Poly(pf3_1, s)
```

pf3_1.coeff_monomial(s)

pf3_1.nth(0)

```python
rc3 = pf3_1.nth(2)/qf3_1.nth(2)
rc3
```

```python
Zf3Aster_2 = (1/Yf3Aster  - rc3).cancel()
```

```python
(pf3_2, qf3_2) = sympy.fraction(Zf3Aster_2)
qf3_2 = sympy.Poly(qf3_2, s)
pf3_2 = sympy.Poly(pf3_2, s)
```

```python
cc2 = qf3_2.nth(2)/pf3_2.nth(1)
cc2
```

```python
Yf3Aster_3 = (1/Zf3Aster_2 - cc2 * s).cancel()
```

```python
(qf3_3, pf3_3) = sympy.fraction(Yf3Aster_3)
qf3_3 = sympy.Poly(qf3_3, s)
pf3_3 = sympy.Poly(pf3_3, s)
```

```python
rc2 = pf3_3.nth(1)/qf3_3.nth(1)
rc2
```

```python
Zf3Aster_3 = (1/Yf3Aster_3  - rc2).cancel()
```

```python
(pf3_4, qf3_4) = sympy.fraction(Zf3Aster_3)
qf3_4 = sympy.Poly(qf3_4, s)
pf3_4 = sympy.Poly(pf3_4, s)
```

```python
cc1 = qf3_4.nth(1)/pf3_4.nth(0)
cc1
```

```python
Yf3Aster_4 = (1/Zf3Aster_3 - cc1 * s).cancel()
(qf3_5, pf3_5) = sympy.fraction(Yf3Aster_4)
qf3_5 = sympy.Poly(qf3_5, s)
pf3_5 = sympy.Poly(pf3_5, s)
```

```python
rc1 = pf3_5.nth(0)/qf3_5.nth(0)
rc1
```

```python
(rc3+rc2+rc1).cancel()
```

```python
(cc1+cc2+cc3).cancel()
```

```python
float(rc1.subs({rf1: 1, rf2: 2, rf3: 3, cf1: 1, cf2: 1, cf3: 1}))
```

```python
float(rc2.subs({rf1: 1, rf2: 2, rf3: 3, cf1: 1, cf2: 1, cf3: 1}))
```

```python
float(rc3.subs({rf1: 1, rf2: 2, rf3: 3, cf1: 1, cf2: 1, cf3: 1}))
```

```python
float(cc1.subs({rf1: 1, rf2: 2, rf3: 3, cf1: 1, cf2: 1, cf3: 1}))
```

```python
float(cc2.subs({rf1: 1, rf2: 2, rf3: 3, cf1: 1, cf2: 1, cf3: 1}))
```

```python
float(cc3.subs({rf1: 1, rf2: 2, rf3: 3, cf1: 1, cf2: 1, cf3: 1}))
```

```python
cc1, cc2, cc3
```

```python
rc1, rc2, rc3
```

```python

```


# Modelo dinámico de un circuito RLC

```python
import sympy as sym
sym.init_printing()
import matplotlib.pyplot as plt
%matplotlib inline
```

| Elemento | Dominio del tiempo  (EDO)|
|:---:|:---:|
|  | $u(t) = R \cdot i(t)$ | $U(s) = R \cdot I(s)$ | $R$ |
|  | $\begin{matrix} u(t) = L \frac{d}{dt} i(t) \\ i(t) = \frac{1}{L} \int_{0}^{t} u(\tau) d\tau + i(0+) \epsilon(t) \end{matrix}$ | $\begin{matrix} U(s) = s L I(s) - L i(0+) \\ I(s) = \frac{1}{s L} U(s) + \frac{1}{s} i(0+) \end{matrix}$ | $s L$ |
|  | $\begin{matrix} u(t) = \frac{1}{C} \int_{0}^{t} i(\tau) d\tau + u(0+) \epsilon(t) \\ i(t) = C \frac{d}{dt} u(t) \end{matrix}$ | $\begin{matrix} U(s) = \frac{1}{s C} I(s) + \frac{1}{s} u(0+) \\ I(s) = s C U(s) - C u(0+) \end{matrix}$ | $\frac{1}{s C}$

Considere el circuito que se muestra en la figura. Suponga que la entrada es el voltaje $x(t)$ y la salida es el voltaje $y(t)$. Suponga además que no hay energía almacenada en los elementos que almacenan energía en $t<0$. Además, considere que $x(t)=0 \forall t<0$.

El voltaje $x(t)$ es igual a los voltajes que caen en la red $RLC$.

\begin{align}
v_R(t) + v_L(t) + v_C(t) &= x(t) \\
R i(t) + L \frac{di}{dt} + \frac{1}{C} \int_{0}^{t} i(\tau) d\tau &= x(t) \\
\end{align}

La salida $y(t)$ es el voltaje del capacitor.

\begin{equation}
y(t) = \frac{1}{C} \int_{0}^{t} i(\tau) d\tau
\end{equation}

De donde se obtiene la corriente $i(t)$

\begin{equation}
C \frac{dy}{dt} = i(t)
\end{equation}

Incorporando las expresiones de la salida $y(t)$ y la variable interna $i(t)$ en la ecuación de la malla se obtiene

\begin{align}
R i(t) + L \frac{di}{dt} + \frac{1}{C} \int_{0}^{t} i(\tau) d\tau &= x(t) \\
C R \frac{d y(t)}{dt} + C L \frac{d^2 y(t)}{dt^2} + y(t)& = x(t) \\
\end{align}

```python
t, L, R, C = sym.symbols('t L R C', real=True)
x = sym.Function('x')(t)
y = sym.Function('y')(t)

ode = sym.Eq(C*R*y.diff(t) + C*L*y.diff(t, 2) +  y, x)
ode
```

Dada una entrada $x(t)$, se puede calcular la solución del sistema. Observe que al usar `dsolve` obtiene la solución general de la homogénea relacionada junto a la solución particular.

```python
u = sym.Heaviside(t)
# u = sym.Heaviside(t)*t
# sol = sym.dsolve( ode.subs(x,u))
sol = sym.dsolve( ode.subs(x,u))
sol
```

$\displaystyle y{\left(t \right)} = C_{1} e^{\frac{t \left(- R - \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} + C_{2} e^{\frac{t \left(- R + \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} - \frac{\left(\begin{cases} \begin{cases} \frac{2 C L}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\0 & \text{otherwise} \end{cases} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R + \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}} + \frac{\left(\begin{cases} \begin{cases} \frac{2 C L}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\0 & \text{otherwise} \end{cases} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R - \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}} + \frac{\left(\begin{cases} \begin{cases} \frac{2 C L e^{\frac{R t}{2 L} - \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\t & \text{otherwise} \end{cases} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L e^{\frac{R t}{2 L} - \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R + \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}} - \frac{\left(\begin{cases} \begin{cases} \frac{2 C L e^{\frac{R t}{2 L} + \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\t & \text{otherwise} \end{cases} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L e^{\frac{R t}{2 L} + \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R - \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}}$

Las constantes $C_1$ y $C_2$ deben calcularse para satisfacer las condiciones iniciales  $y(t) = 0$ and $\frac{d y(t)}{dt} = 0$ para $t<0$.

```python
C_int = sym.solve((sol.rhs.limit(t, 0, '-'),
                   sol.rhs.diff(t).limit(t, 0, '-')),
                  ['C1', 'C2'])
C_int
```

```python
sol = sol.subs(C_int)
sol
```

$\displaystyle y{\left(t \right)} = - \frac{\left(\begin{cases} \begin{cases} \frac{2 C L}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\0 & \text{otherwise} \end{cases} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R + \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}} + \frac{\left(\begin{cases} \begin{cases} \frac{2 C L}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\0 & \text{otherwise} \end{cases} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R - \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}} + \frac{\left(\begin{cases} \begin{cases} \frac{2 C L e^{\frac{R t}{2 L} - \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\t & \text{otherwise} \end{cases} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L e^{\frac{R t}{2 L} - \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R + \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}} - \frac{\left(\begin{cases} \begin{cases} \frac{2 C L e^{\frac{R t}{2 L} + \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\t & \text{otherwise} \end{cases} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} = 0 \\\frac{2 C L e^{\frac{R t}{2 L} + \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) e^{\frac{t \left(- R - \frac{\sqrt{C \left(C R^{2} - 4 L\right)}}{C}\right)}{2 L}} \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}}$

```python
RLC = {R: 10,
       L: 0.005,
       C: .002}
RLC
```

```python
tlaps = (t,-10e-3,10e-2)
plt.rcParams['figure.figsize'] = 9, 3
sym.plot(sol.rhs.subs(RLC), tlaps, line_color='red',
         ylabel = '$y(t)$',);
sym.plot(u, tlaps, line_color='blue',
         ylabel = '$x(t)$',);

```

```python
sol.rhs.subs(RLC)
```

```python

```

```python

```


# Kerbal Space Program kOS Hover PID planner

So we're going to try and write a [kOS](http://ksp-kos.github.io/KOS_DOC/) script for Kerbal Space Program (KSP) to control a single-stage rocket engine to hover at a set altitude. Our intrepid little spaceship consists of a single engine, some landing legs and a parachute in case (when) our controller goes haywire.

Towards this, we're going to implement, you guessed it, a [PID controller](https://en.wikipedia.org/wiki/PID_controller). As a first step for hovering, we'll have our controller change in velocity by limiting our g_force to 0 via $gforce_{goal}=1.0$.

$$
\begin{align}
    gforce &= \|\mathbf{a}\| / g \Longrightarrow gforce_{measured} =  \|\mathbf{a_{measured}}\| / g_{measured} \\
    \Delta thrott &= Kp(gforce_{goal} - gforce_{measured}) = Kp(1.0 - gforce_{measured}) \\
    thrott &= thrott + \Delta thrott
\end{align}
$$

Where $\|\mathbf{a_{measured}}\|$ is our measured current max acceleration using an onboard accelerometer and $g_{measured}$ is our onboard gravitational acceleration magnitude using our graviola detector.

And in the running controller loop, we update $thrott = thrott + \Delta thrott$

    SET thrott to thrott + dthrott.

Where thrott is the throttle percentage from 0 to 1 or no thrust to full thrust. This has the effect of trying to move gforce to 1.0, matching the gravitational force.

## Setup & Helper functions
First let's set up some helper functions for loading data and plotting.

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from numpy import genfromtxt
from matplotlib.font_manager import FontProperties
from pylab import rcParams
fontP = FontProperties()
fontP.set_size('small')

def loadData(filename):
    return genfromtxt(filename, delimiter=' ')

def plotData(data):
    rcParams['figure.figsize'] = 10, 6 # Set figure size to 10" wide x 6" tall
    t = data[:,0]
    altitude = data[:,1]
    verticalspeed = data[:,2]
    acceleration = data[:,3] # magnitude of acceleration
    gforce = data[:,4]
    throttle = data[:,5] * 100
    dthrottle_p = data[:,6] * 100
    dthrottle_d = data[:,7] * 100

    # Top subplot, position and velocity up to threshold
    plt.subplot(3, 1, 1)
    plt.plot(t, altitude, t, verticalspeed, t, acceleration)
    plt.axhline(y=100,linewidth=1,alpha=0.5,color='r',linestyle='--',label='goal');
    plt.text(max(t)*0.9, 105, 'Goal Altitude', fontsize=8);
    plt.title('Craft Altitude over Time')
    plt.ylabel('Altitude (meters)')
    plt.legend(['Altitude (m)','Vertical Speed (m/s)', 'Acceleration (m/s^2)'], "best", prop=fontP, frameon=False)

    # Middle subplot, throttle & dthrottle
    plt.subplot(3, 1, 2)
    plt.plot(t, throttle, t, dthrottle_p, t, dthrottle_d)
    plt.legend(['Throttle%','P','D'], "best",  prop=fontP, frameon=False) # Small font, best location
    plt.ylabel('Throttle %')

    # Bottom subplot, gforce
    plt.subplot(3, 1, 3)
    plt.plot(t, gforce)
    plt.axhline(y=1,linewidth=1,alpha=0.5,color='r',linestyle='--',label='goal');
    plt.text(max(t)*0.9, 1.2, 'Goal g-force', fontsize=8);
    plt.legend(['gforce'], "best", bbox_to_anchor=(1.0, 1.0), prop=fontP, frameon = False) # Small font, best location
    plt.xlabel('Time (seconds)')
    plt.ylabel('G-force');
    plt.show();
```

# G-force Control

As a first test, we'll start from the launchpad, thrust at full throttle till we hit $altitude_{goal} > 100\ \text{meters}$ and then use a proportional gain of $Kp = 0.05$ to keep $gforce \sim 1.0$.

    LOCK dthrott_p TO Kp * (1.0 - gforce).
    LOCK dthrott TO dthrott_p.

```python
data = loadData('collected_data\\gforce.txt')
plotData(data)
```

Pretty cool! Once it passes 100m altitude the controller starts, the throttle controls for gforce, bringing it oscillating down around 1g. This zeros our acceleration but not our existing velocity, so the position continues to increase. We could add some derivative gain to damp down the gforce overshoot, but it won't solve this problem yet.

The kOS script to run this test is located in [`hover1.ks`](kOS_scripts/hover1.ks) and called on [`gforce.txt`](collected_data/gforce.txt) by `RUN hover1(gforce.txt,20).`

# Vertical Speed Control
Let's try to get our vertical speed to zero now, causing our position to stay in one spot: $verticalspeed_{goal} = 0$

Instead of using $\Delta thrott = Kp * (1.0 - gforce)$ we'll control for vertical speed with

$$\Delta thrott = Kp(verticalspeed_{goal} - verticalspeed_{measured}) = Kp(-verticalspeed_{measured})$$

    LOCK dthrott_p TO Kp * (0 - SHIP:VERTICALSPEED).
    LOCK dthrott TO dthrott_p.

And use the same proportional gain of $Kp = 0.05$.

```python
data = loadData('collected_data\\vspeed.txt')
plotData(data)
```

Awesome! The controller drops the velocity to a stable oscillation around 0 m/s, and the position seems to flatten off, but it isn't perfect. Maybe it's because of the oscillations? In the game I can see the engine spurt on and off rythmically. It seems to try and stay at roughly 0 m/s, but the position is not 100m and it drifts.

The kOS script to run this test is located in [`hover2.ks`](kOS_scripts/hover2.ks) and called on [`vspeed.txt`](collected_data/vspeed.txt) by `RUN hover2(vspeed.txt,20).`

# Position Control
Instead of gforce or velocity, lets try controlling for position next.

$$ \Delta thrott = K_p(altitude_{goal} - altitude_{current}) $$

Using $Kp = 0.05$ and  $altitude_{goal} = 100$.

    LOCK dthrott_p TO Kp * (goal_altitude - SHIP:ALTITUDE).
    LOCK dthrott TO dthrott_p.

**Note:** *From now on we start the controller directly from the launch pad (instead of burning till passing 100m).*

```python
# To run in kOS console: RUN hover3(pos0.txt,20,0.05,0).
data = loadData('collected_data\\pos0.txt')
plotData(data)
```

Well, we crashed.

Turns out an ideal stable oscillation (which Proportional only controllers tend to do) starting from ground level (around 76m from where the accelerometer is located on the landed craft) would necessarily come back to that point...

Lets try adding some derivative gain to damp that out, the derivative of altitude is just the vertical speed.

$$
\Delta thrott = K_p(altitude_{goal} - altitude_{current}) + K_d(verticalspeed_{goal} - verticalspeed_{current}) \\
thrott = thrott + \Delta thrott
$$

    LOCK dthrott_p TO Kp * (goal_altitude - SHIP:ALTITUDE).
    LOCK dthrott_d TO Kd * (0 - SHIP:VERTICALSPEED).
    LOCK dthrott TO dthrott_p + dthrott_d.

Using $Kp = 0.05,\ \ Kd = 0.05$, $altitude_{goal} = 100\ \text{meters}$ and $verticalspeed_{goal} = 0$

```python
# To run in kOS console: RUN hover3(pos1.txt,20,0.05,0).
data = loadData('collected_data\\pos1.txt')
plotData(data)
```

Great! The controller burned us about 100m and then tried staying there, but there is quite a lot of bounce, maybe if we tweak our gains some.

Let's try gains of $Kp = 0.08,\ \ Kd = 0.04$.

```python
# To run in kOS console: RUN hover3(pos2.txt,20,0.08,0.04).
data = loadData('collected_data\\pos2.txt')
plotData(data)
```

Hmm, after trying a few other combinations, it seems like there's a conceptual error here keeping us from getting to a smooth point.

We've been trying to build our controller with $thrott = thrott + \Delta thrott$$ which means it takes time to overcome our previous throttle, introducing this lag between our current position/velocity and goal one, that shows up as an oscillation.

The kOS script used is [`hover3.ks`](kOS_scripts/hover3.ks) and these tests are run by calling `RUN hover3(posN.txt,20,Kp,Kd)`. For example for $Kp=0.08$ and $Kd=0.04$ the command is `RUN hover3(pos2.txt,20,0.08,0.04)`, for data [`pos2.txt`](collected_data/pos2.txt).

# Hover set-point
Instead let's figure out how to set our throttle point for hovering, and set $thrott = thrott_{hover} + \Delta thrott$

We know our ship's current max thrust with 100%, which we will call $Thrust_{available}$. What we want to find is our $Throttle_\%$ which would be

$$Throttle_\% = \frac {Thrust_{desired}}{Thrust_{available}}$$

Where $Throttle_\%$ is between 0 and 1 (0 - 100%). Our $Thrust_{desired}$ is when we have a thrust to weight ratio of 1, matching gravitational acceleration.

$$
\begin{equation}
TWR = \frac{F}{mg} = 1\\
F_{thrust} = mg \\
g = \frac{\mu _{planet}}{(PLANET:RADIUS)^2} \\
Thrust_{desired} = SHIP:MASS * g \\
\end{equation}
$$

Finally that gives us

$$Throttle_\% = \frac {SHIP:MASS * \frac{\mu _{planet}}{(PLANET:RADIUS)^2}}{Thrust_{available}}$$

    LOCK hover_throttle_level TO MIN(1, MAX(0, SHIP:MASS * g / MAX(0.0001, curr_engine:AVAILABLETHRUST))).

Okay, moment of truth. Let's start small with $Kp = 0.01,\ \ Kd = 0.001$ and set  $altitude_{goal} = 100\ \text{meters}$. Shown with the solid red line across.

```python
# To run in kOS console: RUN hover4(hover0.txt,60,0.01,0.001).
data = loadData('collected_data\\hover0.txt')
plotData(data)
```

It's stably oscillating! This is a good sign, showing our hover setpoint is doing it's job, the proportional gain is there, and there's barely any derivative gain.

Let's bump up the derivative gain to $Kd = 0.01$.

```python
# To run in kOS console: RUN hover4(hover1.txt,60,0.01,0.01).
data = loadData('collected_data\\hover1.txt')
plotData(data)
```

Woohoo! It overshoots a little but stablizes smoothly at 100m! Great to see this going in the game, looks a bit like the SpaceX grasshopper.

The kOS script used is [`hover4.ks`](kOS_scripts/hover4.ks) and these tests are run by calling `RUN hover4(hoverN.txt,20,Kp,Kd)`.

Some datasets:

* [`hover0.txt`](collected_data/hover0.txt)
* [`hover1.txt`](collected_data/hover1.txt)
* [`hover2.txt`](collected_data/hover2.txt)
* [`hover3.txt`](collected_data/hover3.txt)

# Tweaking gains

Now let's try and optimize the gains to reduce the rise time.

We take a quick shot by increasing both gains to $Kp = 0.1,\ \ Kd = 0.1$

```python
# To run in kOS console: RUN hover4(hover2.txt,10,0.1,0.1).
data = loadData('collected_data\\hover2.txt')
plotData(data)
```

Much faster! What happens if we change the altitude to say 300m?

```python
# To run in kOS console: RUN hover5(hover3.txt,10,0.1,0.1,300).
data = loadData('collected_data\\hover3.txt')
plotData(data)
```

Hmm some pretty big overshoot, but it does it! Next up is to apply some standardized techniques for finding gain values.

To be continued...


#Imports

```
import numpy as np
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from IPython import display
plt.style.use('seaborn-white')
```

# Read and process data.

Download the file from this URL: https://drive.google.com/file/d/1UWWIi-sz9g0x3LFvkIZjvK1r2ZaCqgGS/view?usp=sharing

```
import gdown
gdown.download('https://drive.google.com/uc?id=1UWWIi-sz9g0x3LFvkIZjvK1r2ZaCqgGS','text.txt', quiet=False)
```

    Downloading...
    From: https://drive.google.com/uc?id=1UWWIi-sz9g0x3LFvkIZjvK1r2ZaCqgGS
    To: /content/text.txt
    100%|██████████| 10.3k/10.3k [00:00<00:00, 2.91MB/s]

    'text.txt'

```
data = open('text.txt', 'r').read()
```

Process data and calculate indices

```
chars = list(set(data))
data_size, X_size = len(data), len(chars)
print("Corona Virus article has %d characters, %d unique characters" %(data_size, X_size))
char_to_idx = {ch:i for i,ch in enumerate(chars)}
idx_to_char = {i:ch for i,ch in enumerate(chars)}
```

    Corona Virus article has 10223 characters, 75 unique characters

# Constants and Hyperparameters

```
Hidden_Layer_size = 100 #size of the hidden layer
Time_steps = 40 # Number of time steps (length of the sequence) used for training
learning_rate = 1e-1 # Learning Rate
weight_sd = 0.1 #Standard deviation of weights for initialization
z_size = Hidden_Layer_size + X_size #Size of concatenation(H, X) vector
```

# Activation Functions and Derivatives

```
def sigmoid(x): # sigmoid function
  return 1/(1+np.exp(-x))

def dsigmoid(y): # derivative of sigmoid function
  return y * (1-y)

def tanh(x): # tanh function
  return np.tanh(x)

def dtanh(y): # derivative of tanh
  return 1-y*y
```

# Quiz Question 1

What is the value of sigmoid(0) calculated from  your code? (Answer up to 1 decimal point, e.g. 4.2 and NOT 4.29999999, no rounding off).

# Quiz Question 2

What is the value of dsigmoid(sigmoid(0)) calculated from your code?? (Answer up to 2 decimal point, e.g. 4.29 and NOT 4.29999999, no rounding off).

# Quiz Question 3

What is the value of tanh(dsigmoid(sigmoid(0))) calculated from your code?? (Answer up to 5 decimal point, e.g. 4.29999 and NOT 4.29999999, no rounding off).

# Quiz Question 4

What is the value of dtanh(tanh(dsigmoid(sigmoid(0)))) calculated from your code?? (Answer up to 5 decimal point, e.g. 4.29999 and NOT 4.29999999, no rounding off).

```
print('Quiz 1', sigmoid(0))
print('Quiz 2', dsigmoid(sigmoid(0)))
print('Quiz 3', tanh(dsigmoid(sigmoid(0))))
print('Quiz 4', dtanh(tanh(dsigmoid(sigmoid(0)))))
```

    Quiz 1 0.5
    Quiz 2 0.25
    Quiz 3 0.24491866240370913
    Quiz 4 0.940014848806378

# Parameters

```
class Param:
    def __init__(self, name, value):
      self.name = name
      self.v = value # parameter value
      self.d = np.zeros_like(value) # derivative
      self.m = np.zeros_like(value) # momentum for Adagrad
```

We use random weights with normal distribution (0, weight_sd) for  tanh  activation function and (0.5, weight_sd) for  `sigmoid`  activation function.

Biases are initialized to zeros.

# LSTM
You are making this network, please note f, i, c and o (also "v") in the image below:

Please note that we are concatenating the old_hidden_vector and new_input.

# Quiz Question 4

In the class definition below, what should be size_a, size_b, and size_c? ONLY use the variables defined above.

```
size_a = Hidden_Layer_size
size_b = z_size
size_c = X_size

class Parameters:
    def __init__(self):
        self.W_f = Param('W_f', np.random.randn(size_a, size_b) * weight_sd + 0.5)
        self.b_f = Param('b_f', np.zeros((size_a, 1)))

        self.W_i = Param('W_i', np.random.randn(size_a, size_b) * weight_sd + 0.5)
        self.b_i = Param('b_i', np.zeros((size_a, 1)))

        self.W_C = Param('W_C', np.random.randn(size_a, size_b) * weight_sd)
        self.b_C = Param('b_C', np.zeros((size_a, 1)))

        self.W_o = Param('W_o', np.random.randn(size_a, size_b) * weight_sd + 0.5)
        self.b_o = Param('b_o', np.zeros((size_a, 1)))

        #For final layer to predict the next character
        self.W_v = Param('W_v', np.random.randn(X_size, size_a) * weight_sd)
        self.b_v = Param('b_v', np.zeros((size_c, 1)))

    def all(self):
        return [self.W_f, self.W_i, self.W_C, self.W_o, self.W_v,
               self.b_f, self.b_i, self.b_C, self.b_o, self.b_v]

parameters = Parameters()
```

Look at these operations which we'll be writing:

**Concatenation of h and x:**

$z\:=\:\left[h_{t-1},\:x\right]$

$f_t=\sigma\left(W_f\cdot z\:+\:b_f\:\right)$

$i_i=\sigma\left(W_i\cdot z\:+\:b_i\right)$

$\overline{C_t}=\tanh\left(W_C\cdot z\:+\:b_C\right)$

$C_t=f_t\ast C_{t-1}+i_t\ast \overline{C}_t$

$o_t=\sigma\left(W_o\cdot z\:+\:b_i\right)$

$h_t=o_t\ast\tanh\left(C_t\right)$

**Logits:**

$v_t=W_v\cdot h_t+b_v$

**Softmax:**

$\hat{y}=softmax\left(v_t\right)$

```
def forward(x, h_prev, C_prev, p = parameters):
    assert x.shape == (X_size, 1)
    assert h_prev.shape == (Hidden_Layer_size, 1)
    assert C_prev.shape == (Hidden_Layer_size, 1)

    z = np.row_stack((h_prev, x))
    f = sigmoid(np.dot(parameters.all()[0].v, z)+ parameters.all()[5].v)
    i = sigmoid(np.dot(parameters.all()[1].v, z)+ parameters.all()[6].v)
    C_bar = tanh(np.dot(parameters.all()[2].v, z)+ parameters.all()[7].v)

    C = f*C_prev + i*C_bar
    o = sigmoid(np.dot(parameters.all()[3].v, z)+parameters.all()[8].v)
    h = o*tanh(C)

    v = np.dot(parameters.all()[4].v, h)+parameters.all()[9].v
    y = np.exp(v) / np.sum(np.exp(v)) #softmax

    return z, f, i, C_bar, C, o, h, v, y
```

You must finish the function above before you can attempt the questions below.

# Quiz Question 5

What is the output of 'print(len(forward(np.zeros((X_size, 1)), np.zeros((Hidden_Layer_size, 1)), np.zeros((Hidden_Layer_size, 1)), parameters)))'?

```
print(len(forward(np.zeros((X_size, 1)), np.zeros((Hidden_Layer_size, 1)), np.zeros((Hidden_Layer_size, 1)), parameters)))
```

    9

# Quiz Question 6.

Assuming you have fixed the forward function, run this command:
z, f, i, C_bar, C, o, h, v, y = forward(np.zeros((X_size, 1)), np.zeros((Hidden_Layer_size, 1)), np.zeros((Hidden_Layer_size, 1)))

Now, find these values:

1.   print(z.shape)
2.   print(np.sum(z))
3.   print(np.sum(f))

Copy and paste exact values you get in the logs into the quiz.

```
z, f, i, C_bar, C, o, h, v, y = forward(np.zeros((X_size, 1)), np.zeros((Hidden_Layer_size, 1)), np.zeros((Hidden_Layer_size, 1)))
```

```
print(z.shape)
print(np.sum(z))
print(np.sum(f))
```

    (175, 1)
    0.0
    50.0

# Backpropagation

Here we are defining the backpropagation. It's too complicated, here is the whole code. (Please note that this would work only if your earlier code is perfect).

```
def backward(target, dh_next, dC_next, C_prev,
             z, f, i, C_bar, C, o, h, v, y,
             p = parameters):

    assert z.shape == (X_size + Hidden_Layer_size, 1)
    assert v.shape == (X_size, 1)
    assert y.shape == (X_size, 1)

    for param in [dh_next, dC_next, C_prev, f, i, C_bar, C, o, h]:
        assert param.shape == (Hidden_Layer_size, 1)

    dv = np.copy(y)
    dv[target] -= 1

    p.W_v.d += np.dot(dv, h.T)
    p.b_v.d += dv

    dh = np.dot(p.W_v.v.T, dv)
    dh += dh_next
    do = dh * tanh(C)
    do = dsigmoid(o) * do
    p.W_o.d += np.dot(do, z.T)
    p.b_o.d += do

    dC = np.copy(dC_next)
    dC += dh * o * dtanh(tanh(C))
    dC_bar = dC * i
    dC_bar = dtanh(C_bar) * dC_bar
    p.W_C.d += np.dot(dC_bar, z.T)
    p.b_C.d += dC_bar

    di = dC * C_bar
    di = dsigmoid(i) * di
    p.W_i.d += np.dot(di, z.T)
    p.b_i.d += di

    df = dC * C_prev
    df = dsigmoid(f) * df
    p.W_f.d += np.dot(df, z.T)
    p.b_f.d += df

    dz = (np.dot(p.W_f.v.T, df)
         + np.dot(p.W_i.v.T, di)
         + np.dot(p.W_C.v.T, dC_bar)
         + np.dot(p.W_o.v.T, do))
    dh_prev = dz[:Hidden_Layer_size, :]
    dC_prev = f * dC

    return dh_prev, dC_prev
```

# Forward and Backward Combined Pass

Let's first clear the gradients before each backward pass

```
def clear_gradients(params = parameters):
    for p in params.all():
        p.d.fill(0)
```

Clip gradients to mitigate exploding gradients

```
def clip_gradients(params = parameters):
    for p in params.all():
        np.clip(p.d, -1, 1, out=p.d)
```

Calculate and store the values in forward pass. Accumulate gradients in backward pass and clip gradients to avoid exploding gradients.

input, target are list of integers, with character indexes.
h_prev is the array of initial h at  h−1  (size H x 1)
C_prev is the array of initial C at  C−1  (size H x 1)
Returns loss, final  hT  and  CT

```
def forward_backward(inputs, targets, h_prev, C_prev):
    global paramters

    # To store the values for each time step
    x_s, z_s, f_s, i_s,  = {}, {}, {}, {}
    C_bar_s, C_s, o_s, h_s = {}, {}, {}, {}
    v_s, y_s =  {}, {}

    # Values at t - 1
    h_s[-1] = np.copy(h_prev)
    C_s[-1] = np.copy(C_prev)

    loss = 0
    # Loop through time steps
    assert len(inputs) == Time_steps
    for t in range(len(inputs)):
        x_s[t] = np.zeros((X_size, 1))
        x_s[t][inputs[t]] = 1 # Input character

        (z_s[t], f_s[t], i_s[t],
        C_bar_s[t], C_s[t], o_s[t], h_s[t],
        v_s[t], y_s[t]) = \
            forward(x_s[t], h_s[t - 1], C_s[t - 1]) # Forward pass

        loss += -np.log(y_s[t][targets[t], 0]) # Loss for at t

    clear_gradients()

    dh_next = np.zeros_like(h_s[0]) #dh from the next character
    dC_next = np.zeros_like(C_s[0]) #dh from the next character

    for t in reversed(range(len(inputs))):
        # Backward pass
        dh_next, dC_next = \
            backward(target = targets[t], dh_next = dh_next,
                     dC_next = dC_next, C_prev = C_s[t-1],
                     z = z_s[t], f = f_s[t], i = i_s[t], C_bar = C_bar_s[t],
                     C = C_s[t], o = o_s[t], h = h_s[t], v = v_s[t],
                     y = y_s[t])

    clip_gradients()

    return loss, h_s[len(inputs) - 1], C_s[len(inputs) - 1]
```

# Sample the next character

```
def sample(h_prev, C_prev, first_char_idx, sentence_length):
    x = np.zeros((X_size, 1))
    x[first_char_idx] = 1

    h = h_prev
    C = C_prev

    indexes = []

    for t in range(sentence_length):
        _, _, _, _, C, _, h, _, p = forward(x, h, C)
        idx = np.random.choice(range(X_size), p=p.ravel())
        x = np.zeros((X_size, 1))
        x[idx] = 1
        indexes.append(idx)

    return indexes
```

# Training (Adagrad)

Update the graph and display a sample output

```
def update_status(inputs, h_prev, C_prev):
    #initialized later
    global plot_iter, plot_loss
    global smooth_loss

    # Get predictions for 200 letters with current model

    sample_idx = sample(h_prev, C_prev, inputs[0], 200)
    txt = ''.join(idx_to_char[idx] for idx in sample_idx)

    # Clear and plot
    plt.plot(plot_iter, plot_loss)
    display.clear_output(wait=True)
    plt.show()

    #Print prediction and loss
    print("----\n %s \n----" % (txt, ))
    print("iter %d, loss %f" % (iteration, smooth_loss))
```

# Update Parameters

\begin{align}
\theta_i &= \theta_i - \eta\frac{d\theta_i}{\sum dw_{\tau}^2} \\
d\theta_i &= \frac{\partial L}{\partial \theta_i}
\end{align}

```
def update_paramters(params = parameters):
    for p in params.all():
        p.m += p.d * p.d # Calculate sum of gradients
        #print(learning_rate * dparam)
        p.v += -(learning_rate * p.d / np.sqrt(p.m + 1e-8))
```

To delay the keyboard interrupt to prevent the training from stopping in the middle of an iteration

```
# Exponential average of loss
# Initialize to a error of a random model
smooth_loss = -np.log(1.0 / X_size) * Time_steps

iteration, pointer = 0, 0

# For the graph
plot_iter = np.zeros((0))
plot_loss = np.zeros((0))
```

# Training Loop

```
iter = 50000
while iter > 0:
  # Reset
  if pointer + Time_steps >= len(data) or iteration == 0:
      g_h_prev = np.zeros((Hidden_Layer_size, 1))
      g_C_prev = np.zeros((Hidden_Layer_size, 1))
      pointer = 0

  inputs = ([char_to_idx[ch]
              for ch in data[pointer: pointer + Time_steps]])
  targets = ([char_to_idx[ch]
              for ch in data[pointer + 1: pointer + Time_steps + 1]])

  loss, g_h_prev, g_C_prev = \
      forward_backward(inputs, targets, g_h_prev, g_C_prev)
  smooth_loss = smooth_loss * 0.999 + loss * 0.001

  # Print every hundred steps
  if iteration % 100 == 0:
      update_status(inputs, g_h_prev, g_C_prev)

  update_paramters()

  plot_iter = np.append(plot_iter, [iteration])
  plot_loss = np.append(plot_loss, [loss])

  pointer += Time_steps
  iteration += 1
  iter = iter -1
```

# Quiz Question 7.

Run the above code for 50000 iterations making sure that you have 100 hidden layers and time_steps is 40. What is the loss value you're seeing?

```
iter = 50000
while iter > 0:
  # Reset
  if pointer + Time_steps >= len(data) or iteration == 0:
      g_h_prev = np.zeros((Hidden_Layer_size, 1))
      g_C_prev = np.zeros((Hidden_Layer_size, 1))
      pointer = 0

  inputs = ([char_to_idx[ch]
              for ch in data[pointer: pointer + Time_steps]])
  targets = ([char_to_idx[ch]
              for ch in data[pointer + 1: pointer + Time_steps + 1]])

  loss, g_h_prev, g_C_prev = \
      forward_backward(inputs, targets, g_h_prev, g_C_prev)
  smooth_loss = smooth_loss * 0.999 + loss * 0.001

  # Print every hundred steps
  if iteration % 100 == 0:
      update_status(inputs, g_h_prev, g_C_prev)

  update_paramters()

  plot_iter = np.append(plot_iter, [iteration])
  plot_loss = np.append(plot_loss, [loss])

  pointer += Time_steps
  iteration += 1
  iter = iter -1
```


### Calculates price-equilibrium in the market for blockchain records,  with and without the lightning network.

### Includes symbolic calculations and plots for specific parameter values.

### Focuses on symmetric transfer-rate and power-law transfer-size.

```python
import numpy as np
import sympy
sympy.init_printing(use_unicode=True)
from sympy import *
from sympy.solvers import solve
from IPython.display import display
from typing import Callable
from sympy.utilities.lambdify import lambdify, implemented_function

%matplotlib inline
import matplotlib.pyplot as plt

def simplified(exp, title=None):
    simp = simplify(exp)
    if simplified.LOG:
        if title: display(title,simp)
        else: display(simp)
    return simp
simplified.LOG = True

def firstOrderCondition(exp, var):
    diffExp = simplified(diff(exp, var))
    solutions = solve(diffExp, var)
    if firstOrderCondition.LOG:
        display(solutions)
    return solutions
firstOrderCondition.LOG = True

class Result(object):   # a class for holding results of calculations
    def __repr__(self): return self.__dict__.__repr__()
    def display(self):
        for k,v in sorted(self.__dict__.items()):
            display(k,v)
    def subs(self, params):
        ans = Result()
        for k,v in sorted(self.__dict__.items()):
            if hasattr(v,"subs"):
                ans.__dict__[k] = v.subs(params).evalf()
            else:
                ans.__dict__[k] = v
        return ans
```

```python
a,p,r,b,vmax,zmin,zmax,beta = symbols('a \\phi r z v_{\max} z_{\min} z_{\max} \\beta', positive=True,finite=True,real=True)
w,T,D,L,n,Supply = symbols('w T \\Delta \\ell n \\tau', positive=True,finite=True,real=True)

params = {
    L: 10,           # total transfers per day
    D: 6,            # delta transfers per day
    beta: 0.01,      # value / transfer-size
    r: 4/100/365,    # interest rate per day
    a: 1.1,          # records per reset tx
    Supply: 288000,  # records per day
    zmin: 0.001,     # min transfer size (for power law distribution)
    zmax: 1,         # max transfer size (for uniform distribution)
}
```

```python
tNL = 27 * a * r**2 / L**2 / beta**3 * p
tNB = p/beta
tLB = L*p/(27*a*r**2)**(Rational(1,2))
(tNL, tNL.subs(params), tNB, tNB.subs(params), tLB, tLB.subs(params).evalf())
```

# Demand curves

```python
### Calculate the demand-curves assuming power-law distribution:
def calculateDemandsPowerlaw():
    result = Result()
    result.demandWithoutLightning = Piecewise(
        (L, tNB < zmin),
        (L*zmin/tNB, True))
    result.demandWithLightning = Piecewise(
        (L, tLB < zmin),
        (3 * zmin * (L*a*r**2/p**2)**Rational(1,3)*(zmin**Rational(-1,3)-tLB**Rational(-1,3)) + L*zmin/tLB, tNL < zmin),
        (3 * zmin * (L*a*r**2/p**2)**Rational(1,3)*( tNL**Rational(-1,3)-tLB**Rational(-1,3)) + L*zmin/tLB, True))
    result.txsBlockchain = Piecewise(
        (L, tLB < zmin),
        (L*zmin/tLB, True))
    result.txsLightning = Piecewise(
        (0, tLB < zmin),
        (L*(1-zmin/tLB), tNL < zmin),
        (L*(zmin/tNL-zmin/tLB), True))
    result.txsTotal = result.txsBlockchain + result.txsLightning
    return result

demandsPowerlaw = calculateDemandsPowerlaw()
demandsPowerlaw.display()
demandsPowerlawNumeric = demandsPowerlaw.subs(params)
demandsPowerlawNumeric.display()
```

# Price curves

```python
Demand2 = 3 * zmin * (L*a*r**2/p**2)**Rational(1,3)*(zmin**Rational(-1,3)-tLB**Rational(-1,3)) + L*zmin/tLB
Price2 = simplified(solve (n/2*Demand2 - Supply, p)[0])
Condition2a = simplified(solve(tLB.subs(p, Price2) - zmin,n)[0])
Condition2b = simplified(solve(zmin - tNL.subs(p, Price2),n)[0])

Demand3 = 3 * zmin * (L*a*r**2/p**2)**Rational(1,3)*( tNL**Rational(-1,3)-tLB**Rational(-1,3))
Price3 = simplified(solve (n/2*Demand3 - Supply, p)[0])
Condition3a = simplified(solve(tNL.subs(p, Price2) - zmin,n)[0])
```

```python
### Calculate the demand-curves assuming power-law distribution:
def calculatePricesPowerlaw():
    result = Result()
    result.priceWithoutLightning = Piecewise(
        (0, n/2 < Supply/L),
        (n*L*beta*zmin/2/Supply, True))
    result.priceWithLightning = Piecewise(
        (0, n/2 < Supply/L),
        (3*sqrt(6)*r*zmin/4/Supply**(3/2)*sqrt(L*a)*n**(3/2), n/2 < beta**2*L*Supply/27/a/r**2),
        (n*zmin/2/Supply*(beta*L - 3*sqrt(3*a)*r), True))
#     result.txsBlockchain = Piecewise(
#         (L, tLB < zmin),
#         (L*zmin/tLB, True))
#     result.txsLightning = Piecewise(
#         (0, tLB < zmin),
#         (L*(1-zmin/tLB), tNL < zmin),
#         (L*(zmin/tNL-zmin/tLB), True))
#     result.txsTotal = result.txsBlockchain + result.txsLightning
    return result

pricesPowerlaw = calculatePricesPowerlaw()
pricesPowerlaw.display()
pricesPowerlawNumeric = pricesPowerlaw.subs(params)
pricesPowerlawNumeric.display()
```

# Plots

```python
plotSymmetric = True
plotAsymmetric = False

linewidth = 3
def plotSymbolic(xRange, yExpression, xVariable, style, label):
    plt.plot(xRange, [yExpression.subs(xVariable,xValue) for xValue in xRange], style, label=label, linewidth=linewidth)

def plotDemandCurves(priceRange, demandWithoutLightning, demandWithLightning):
    """
    Plot the expected demand of a single pair vs. the price.
    """
    global plotSymmetric, plotAsymmetric
    plotSymbolic(priceRange, demandWithoutLightning, p, "r.",label="w.o. lightning")
    plotSymbolic(priceRange, demandWithLightning, p,  "b--",label="with lightning")
    plt.gca().set_ylim(-1,11)
    plt.xlabel("blockchain fee $\\phi$  [bitcoins]")
    plt.ylabel("Demand of a single pair [records/day]")
    plt.legend(loc=0)

def plotTxsCurves(priceRange, demandWithoutLightning, demandWithLightning):
    """
    Plot the expected transfer-count of a single pair vs. the price.
    """
    plotSymbolic(priceRange, demandWithoutLightning, p, "r.",label="w.o. lightning")
    plotSymbolic(priceRange, demandWithLightning.txsBlockchain,  p,  "g-.",label="with lightning; blockchain")
    plotSymbolic(priceRange, demandWithLightning.txsLightning, p,  "b--",label="with lightning; lightning")
    plotSymbolic(priceRange, demandWithLightning.txsTotal, p,  "k-",label="with lightning; total")
    plt.gca().set_ylim(-1,11)
    plt.xlabel("blockchain fee $\\phi$  [bitcoins]")
    plt.ylabel("# Transactions per day")
    plt.legend(loc=0)

def plotPriceCurves(nRange, priceWithoutLightning, priceWithLightning):
    """
    Plot the price vs. the number of users.
    """
    global plotSymmetric, plotAsymmetric
    plotSymbolic(nRange, priceWithoutLightning, n, "r.",label="w.o. lightning")
    plotSymbolic(nRange, priceWithLightning, n,  "b--",label="with lightning")
    plt.xlabel("Number of users $n$")
    plt.ylabel("Market-equilibrium price $\\phi$ [bitcoins/record]")
    plt.legend(loc=0)

def plotMarketTxsCurves(nRange, demandWithoutLightning, demandWithLightning, priceWithoutLightning, priceWithLightning):
    """
    Plot the expected transfer-count in the entire market vs. the number of users.
    """
    plotSymbolic(nRange, (n/2)*demandWithoutLightning.subs(p,priceWithoutLightning), n, "r.",label="w.o. lightning")
    plotSymbolic(nRange, (n/2)*demandWithLightning.txsBlockchain.subs(p,priceWithLightning), n,  "g-.",label="with lightning; blockchain")
    plotSymbolic(nRange, (n/2)*demandWithLightning.txsLightning.subs (p,priceWithLightning), n,  "b--",label="with lightning; lightning")
    plotSymbolic(nRange, (n/2)*demandWithLightning.txsTotal.subs (p,priceWithLightning), n,  "k-",label="with lightning; total")
    plt.xlabel("Number of users $n$")
    plt.ylabel("# Transactions per day")
    plt.legend(loc=0)

def plotSymbolic3(xRange, yExpression, xVariable, style, label):
    plt.plot(xRange, [yExpression.subs(xVariable,xValue)*params[Supply] for xValue in xRange], style, label=label)

def plotRevenueCurves(nRange, priceWithoutLightning, priceAsymmetric, priceSymmetric):
    global plotSymmetric, plotAsymmetric
    plotSymbolic3(nRange, priceWithoutLightning, n, "r-",label="no lightning")
    if plotAsymmetric and priceAsymmetric:
        plotSymbolic3(nRange, priceAsymmetric, n,  "b.",label="asymmetric")
    if plotSymmetric and priceSymmetric:
        plotSymbolic3(nRange, priceSymmetric, n,  "g--",label="symmetric")
    plt.xlabel("Number of users $n$")
    plt.ylabel("Miners' revenue [bitcoins/day]")
    plt.legend(loc=0)

```

```python
priceRange = np.linspace(0,1e-6,200)
plotDemandCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric.demandWithLightning)
plt.title("Demand curves, power-law-distributed transfer-size")
plt.axes().get_xaxis().set_visible(False)
plt.savefig('../graphs/demand-curves-powerlaw-small-price.pdf', format='pdf', dpi=1000); plt.show()
plotTxsCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric)
plt.savefig('../graphs/txs-pair-powerlaw-small-price.pdf', format='pdf', dpi=1000); plt.show()
```

```python
priceRange = np.linspace(0,2.4e-5,160)
plotDemandCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric.demandWithLightning)
plt.title("Demand curves, power-law-distributed transfer-size")
#plt.axes().get_xaxis().set_visible(False)
plt.savefig('../graphs/demand-curves-powerlaw-medium-price.pdf', format='pdf', dpi=1000); plt.show()
plotTxsCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric)
plt.savefig('../graphs/txs-pair-powerlaw-medium-price.pdf', format='pdf', dpi=1000); plt.show()
```

```python
priceRange = np.linspace(0,0.01,300)
plotDemandCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric.demandWithLightning)
plt.title("Demand curves, power-law-distributed transfer-size")
plt.axes().get_xaxis().set_visible(False)
plt.gca().set_ylim(-0.01,0.1)
plt.savefig('../graphs/demand-curves-powerlaw-large-price.pdf', format='pdf', dpi=1000); plt.show()
plotTxsCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric)
plt.savefig('../graphs/txs-pair-powerlaw-large-price.pdf', format='pdf', dpi=1000); plt.show()
```

```python
priceRange = np.linspace(0,1,300)
plotDemandCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric.demandWithLightning)
plt.title("Demand curves, power-law-distributed transfer-size")
#plt.axes().get_xaxis().set_visible(False)
plt.gca().set_ylim(-0.0001,0.01)
plt.savefig('../graphs/demand-curves-powerlaw-huge-price.pdf', format='pdf', dpi=1000); plt.show()
plotTxsCurves(priceRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric)
plt.savefig('../graphs/txs-pair-powerlaw-huge-price.pdf', format='pdf', dpi=1000); plt.show()
```

```python
nRange = np.linspace(0,2e5,100)
plotPriceCurves(nRange, pricesPowerlawNumeric.priceWithoutLightning, pricesPowerlawNumeric.priceWithLightning)
plt.title("Price curves, power-law-distributed transfer-size")
plt.savefig('../graphs/price-curves-powerlaw-smalln.pdf', format='pdf', dpi=1000)
```

```python
nRange = np.linspace(0,2e7,100)
plotPriceCurves(nRange, pricesPowerlawNumeric.priceWithoutLightning, pricesPowerlawNumeric.priceWithLightning)
plt.title("Price curves, power-law-distributed transfer-size")
plt.savefig('../graphs/price-curves-powerlaw-mediumn.pdf', format='pdf', dpi=1000)
```

```python
nRange = np.linspace(0,2e8,100)
plotPriceCurves(nRange, pricesPowerlawNumeric.priceWithoutLightning, pricesPowerlawNumeric.priceWithLightning)
plt.title("Price curves, power-law-distributed transfer-size")
plt.savefig('../graphs/price-curves-powerlaw-largen.pdf', format='pdf', dpi=1000)
```

```python
nRange = np.linspace(0,2e9,100)
plotPriceCurves(nRange, pricesPowerlawNumeric.priceWithoutLightning, pricesPowerlawNumeric.priceWithLightning)
plt.title("Price curves, power-law-distributed transfer-size")
plt.savefig('../graphs/price-curves-powerlaw-hugen.pdf', format='pdf', dpi=1000)
```

```python
nRange = np.linspace(0,2e5,100)
plotMarketTxsCurves(nRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric, pricesPowerlawNumeric.priceWithoutLightning, pricesPowerlawNumeric.priceWithLightning)
plt.title("Txs, powerlaw transfer-size, symmetric")
plt.savefig('../graphs/txs-market-powerlaw-symmetric-smalln.pdf', format='pdf', dpi=1000)
plt.show()
nRange = np.linspace(0,2e8,100)
plotMarketTxsCurves(nRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric, pricesPowerlawNumeric.priceWithoutLightning, pricesPowerlawNumeric.priceWithLightning)
plt.title("Txs, powerlaw transfer-size, symmetric")
plt.savefig('../graphs/txs-market-powerlaw-symmetric-largen.pdf', format='pdf', dpi=1000)
plt.show()
nRange = np.linspace(0,2e9,100)
plotMarketTxsCurves(nRange, demandsPowerlawNumeric.demandWithoutLightning, demandsPowerlawNumeric, pricesPowerlawNumeric.priceWithoutLightning, pricesPowerlawNumeric.priceWithLightning)
plt.title("Txs, powerlaw transfer-size, symmetric")
plt.savefig('../graphs/txs-market-powerlaw-symmetric-hugen.pdf', format='pdf', dpi=1000)
plt.show()
```