# Electrical Power Generation 1

## Objective and Prerequisites

Major electric power companies around the world utilize mathematical optimization to manage the flow of energy across their electrical grids. In this example, you’ll discover the power of mathematical optimization in addressing a common energy industry problem: electrical power generation. We’ll show you how to figure out the optimal set of power stations to turn on in order to satisfy anticipated power demand over a 24-hour time horizon.

This model is example 15 from the fifth edition of Model Building in Mathematical Programming by H. Paul Williams on pages 270 – 271 and 325 – 326.

This example is at the intermediate level, where we assume that you know Python and the Gurobi Python API and that you have some knowledge of building mathematical optimization models.

**Download the Repository**

You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip).

---
## Problem Description

In this problem, power generation units are grouped into three distinct types, with different characteristics for each type (power output, cost per megawatt hour, startup cost, etc.).  A unit can be on or off, with a startup cost associated with transitioning from off to on, and power output that can fall anywhere between a specified minimum and maximum value when the unit is on.  A 24-hour time horizon is divided into 5 discrete time periods, each with an expected total power demand.  The model decides which units to turn on, and when, in order to satisfy demand for each time period.  The model also captures a reserve requirement, where the selected power plants must be capable of increasing their output, while still respecting their maximum output, in order to cope with the situation where actual demand exceeds predicted demand.

A set of generators is available to satisfy power demand for the following day.  Anticipated demand is as follows:

| Time Period | Demand (megawatts) |
| --- | --- |
| 12 pm to 6 am | 15000 |
| 6 am to 9 am | 30000 |
| 9 am to 3 pm | 25000 |
| 3 pm to 6 pm | 40000 |
| 6 pm to 12 pm | 27000 |

Generators are grouped into three types, with the following minimum and maximum output for each type (when they are on):

| Type | Number available | Minimum output (MW) | Maximum output (MW) |
| --- | --- | --- | --- |
| 0 | 12 |  850 | 2000 |
| 1 | 10 | 1250 | 1750 |
| 2 | 5 | 1500 | 4000 |

There are costs associated with using a generator: a cost per hour when the generator is on (and generating its minimum output), a cost per megawatt hour above its minimum, and a startup cost for turning a generator on:

| Type | Cost per hour (when on) | Cost per MWh above minimum | Startup cost |
| --- | --- | --- | --- |
| 0 | $\$1000$ | $\$2.00$ | $\$2000$ |
| 1 | $\$2600$ | $\$1.30$ | $\$1000$ |
| 2 | $\$3000$ | $\$3.00$ | $\$500$ |

Generators must meet predicted demand, but they must also have sufficient reserve capacity to be able to cope with the situation where actual demand exceeds predicted demand.  For this model, the set of selected generators must be able to produce as much as 115% of predicted demand.

Which generators should be committed to meeting anticipated demand in order to minimize total cost?

---
## Model Formulation

### Sets and Indices

$t \in \text{Types}=\{0,1,2\}$: Types of generators.

$p \in \text{Periods}=\{0,1,2,3,4\}$: Time periods.

### Parameters

$\text{period_hours}_p \in \mathbb{N}^+$: Number of hours in each time period.

$\text{generators}_t \in \mathbb{N}^+$: Number of generators of type $t$.

$\text{demand}_p \in \mathbb{R}^+$: Total power demand for time period $p$.

$\text{start0} \in \mathbb{N}^+$: Number of generators that are on at the beginning of the time horizon (and available in time period 0 without paying a startup cost).

$\text{min_output}_t \in \mathbb{R}^+$: Minimum output for generator type $t$ (when on).

$\text{max_output}_t \in \mathbb{R}^+$: Maximum output for generator type $t$.

$\text{base_cost}_t \in \mathbb{R}^+$: Minimum operating cost (per hour) for a generator of type $t$.

$\text{per_mw_cost}_t \in \mathbb{R}^+$: Cost to generate one additional MW (per hour) for a generator of type $t$.

$\text{startup_cost}_t \in \mathbb{R}^+$: Startup cost for generator of type $t$.

### Decision Variables

$\text{ngen}_{t,p} \in \mathbb{N}^+$: Number of generators of type $t$ that are on in time period $p$.

$\text{output}_{t,p} \in \mathbb{R}^+$: Total power output from generators of type $t$ in time period $p$.

$\text{nstart}_{t,p} \in \mathbb{N}^+$: Number of generators of type $t$ to start in time period $p$.

### Objective Function

- **Cost**: Minimize the cost (in USD) to satisfy the predicted electricity demand.

\begin{equation}
\text{Minimize} \quad Z_{on} + Z_{extra} + Z_{startup}
\end{equation}

\begin{equation}
Z_{on} = \sum_{(t,p) \in \text{Types} \times \text{Periods}}{\text{base_cost}_t*\text{ngen}_{t,p}}
\end{equation}
\begin{equation}
Z_{extra} = \sum_{(t,p) \in \text{Types} \times \text{Periods}}{\text{per_mw_cost}_t*(\text{output}_{t,p} - \text{min_load}_t})
\end{equation}
\begin{equation}
Z_{startup} = \sum_{(t,p) \in \text{Types} \times \text{Periods}}{\text{startup_cost}_t*\text{nstart}_{t,p}}
\end{equation}

### Constraints

- **Available generators**: Number of generators used must be less than or equal to the number available.

\begin{equation}
\text{ngen}_{t,p} \leq \text{generators}_{t} \quad \forall (t,p) \in \text{Types} \times \text{Periods}
\end{equation}

- **Demand**: Total power generated across all generator types must meet anticipated demand for each time period $p$.

\begin{equation}
\sum_{t \in \text{Types}}{\text{output}_{t,p}} \geq \text{demand}_p \quad \forall p \in \text{Periods}
\end{equation}

- **Min/max generation**: Power generation must respect generator min/max values.

\begin{equation}
\text{output}_{t,p} \geq \text{min_output}_t*\text{ngen}_{t,p} \quad \forall (t,p) \in \text{Types} \times \text{Periods}
\end{equation}

\begin{equation}
\text{output}_{t,p} \leq \text{max_output}_t*\text{ngen}_{t,p} \quad \forall (t,p) \in \text{Types} \times \text{Periods}
\end{equation}

- **Reserve**: Selected generators must be able to satisfy demand that is as much as 15% above the prediction.

\begin{equation}
\sum_{t \in \text{Types}}{\text{max_output}_t*\text{ngen}_{t,p}} \geq 1.15 * \text{demand}_p \quad \forall p \in \text{Periods}
\end{equation}

- **Startup**: Establish relationship between number of active generators and number of startups (use $start0$ for period before the time horizon starts)

\begin{equation}
\text{ngen}_{t,p} \leq \text{ngen}_{t,p-1} + \text{startup}_{t,p} \quad \forall (t,p) \in \text{Types} \times \text{Periods}
\end{equation}

---
## Python Implementation

We import the Gurobi Python Module and other Python libraries.

```python
%pip install gurobipy
```

```python
import numpy as np
import pandas as pd

import gurobipy as gp
from gurobipy import GRB

# tested with Python 3.7.0 & Gurobi 9.0
```

## Input Data
We define all the input data of the model.

```python
# Parameters

ntypes = 3
nperiods = 5
maxstart0 = 5

generators = [12, 10, 5]
period_hours = [6, 3, 6, 3, 6]
demand = [15000, 30000, 25000, 40000, 27000]
min_load = [850, 1250, 1500]
max_load = [2000, 1750, 4000]
base_cost = [1000, 2600, 3000]
per_mw_cost = [2, 1.3, 3]
startup_cost = [2000, 1000, 500]
```

## Model Deployment

We create a model and the variables. For each time period, we have: an integer decision variable to capture the number of active generators of each type (ngen), an integer variable to capture the number of generators of each type we must start (nstart), and a continuous decision variable to capture the total power output for each generator type (output).

```python
model = gp.Model('PowerGeneration')
ngen = model.addVars(ntypes, nperiods, vtype=GRB.INTEGER, name="ngen")
nstart = model.addVars(ntypes, nperiods, vtype=GRB.INTEGER, name="nstart")
output = model.addVars(ntypes, nperiods, vtype=GRB.CONTINUOUS, name="genoutput")
```

    Using license file c:\gurobi\gurobi.lic

Next we insert the constraints:

The number of active generators can't exceed the number of generators.

```python
# Generator count

numgen = model.addConstrs(ngen[type, period] <= generators[type]
                         for type in range(ntypes) for period in range(nperiods))
```

Total power output for a generator type depends on the number of generators of that type that are active.

```python
# Respect minimum and maximum output per generator type

min_output = model.addConstrs((output[type, period] >= min_load[type] * ngen[type, period])
                              for type in range(ntypes) for period in range(nperiods))

max_output = model.addConstrs((output[type, period] <= max_load[type] * ngen[type, period])
                              for type in range(ntypes) for period in range(nperiods))
```

Total output for each time period must meet predicted demand.

```python
# Meet demand

meet_demand = model.addConstrs(gp.quicksum(output[type, period] for type in range(ntypes)) >= demand[period]
                               for period in range(nperiods))
```

Selected generators must be able to cope with an excess of demand.

```python
# Provide sufficient reserve capacity

reserve = model.addConstrs(gp.quicksum(max_load[type]*ngen[type, period] for type in range(ntypes)) >= 1.15*demand[period]
                    for period in range(nperiods))
```

Connect the decision variables that capture active generators with the decision variables that count startups.

```python
# Startup constraint

startup0 = model.addConstrs((ngen[type,0] <= maxstart0 + nstart[type,0])
                            for type in range(ntypes))

startup = model.addConstrs((ngen[type,period] <= ngen[type,period-1] + nstart[type,period])
                           for type in range(ntypes) for period in range(1,nperiods))
```

Objective: minimize total cost.  Cost consists of three components: the cost for running active generation units, the cost to generate power beyond the minimum for each unit, and the cost to start up generation units.

```python
# Objective: minimize total cost

active = gp.quicksum(base_cost[type]*period_hours[period]*ngen[type,period]
                    for type in range(ntypes) for period in range(nperiods))

per_mw = gp.quicksum(per_mw_cost[type]*period_hours[period]*(output[type,period] - min_load[type]*ngen[type,period])
                       for type in range(ntypes) for period in range(nperiods))

startup_obj = gp.quicksum(startup_cost[type]*nstart[type,period]
                         for type in range(ntypes) for period in range(nperiods))

model.setObjective(active + per_mw + startup_obj)
```

Next, we start the optimization and Gurobi finds the optimal solution.

```python
model.write('junk.lp')
model.optimize()
```

    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)
    Thread count: 4 physical cores, 8 logical processors, using up to 8 threads
    Optimize a model with 70 rows, 45 columns and 147 nonzeros
    Model fingerprint: 0x08cf02b0
    Variable types: 15 continuous, 30 integer (0 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 4e+03]
      Objective range  [4e+00, 9e+03]
      Bounds range     [0e+00, 0e+00]
      RHS range        [5e+00, 5e+04]
    Presolve removed 16 rows and 1 columns
    Presolve time: 0.00s
    Presolved: 54 rows, 44 columns, 130 nonzeros
    Variable types: 0 continuous, 44 integer (0 binary)
    Found heuristic solution: objective 1123913.3000
    Found heuristic solution: objective 1084300.0000

    Root relaxation: objective 9.995143e+05, 23 iterations, 0.00 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0 999514.286    0    7 1084300.00 999514.286  7.82%     -    0s
    H    0     0                    1009430.8000 999514.286  0.98%     -    0s
    H    0     0                    1006720.0000 999514.286  0.72%     -    0s
    H    0     0                    1005970.0000 999514.286  0.64%     -    0s
    H    0     0                    1003290.0000 999514.286  0.38%     -    0s
    H    0     0                    1002540.0000 999514.286  0.30%     -    0s

    Cutting planes:
      MIR: 4
      StrongCG: 1

    Explored 1 nodes (23 simplex iterations) in 0.06 seconds
    Thread count was 8 (of 8 available processors)

    Solution count 7: 1.00254e+06 1.00329e+06 1.00597e+06 ... 1.12391e+06

    Optimal solution found (tolerance 1.00e-04)
    Best objective 1.002540000000e+06, best bound 1.002540000000e+06, gap 0.0000%

---
## Analysis

The anticipated demand for electricity over the 24-hour time window can be met for a total cost of $\$1,002,540$. The detailed plan for each time period is as follows.

### Unit Commitments

The following table shows the number of generators of each type that are active in each time period in the optimal solution:

```python
rows = ["Type" + str(t) for t in range(ntypes)]
units = pd.DataFrame(columns=range(nperiods), index=rows, data=0.0)

for t in range(ntypes):
    for p in range(nperiods):
        units.loc["Type"+str(t), p] = ngen[t,p].x
units
```

      0
      1
      2
      3
      4

      Type0
      12.0
      12.0
      12.0
      12.0
      12.0

      Type1
      3.0
      8.0
      8.0
      9.0
      9.0

      Type2
      -0.0
      -0.0
      -0.0
      2.0
      -0.0

The following shows the number of generators of each type that must be started in each time period to achieve this plan (recall that the model assumes that 5 are available at the beginning of the time horizon):

```python
startups = pd.DataFrame(columns=range(nperiods), index=rows, data=0.0)

for t in range(ntypes):
    for p in range(nperiods):
        startups.loc["Type"+str(t), p] = int(nstart[t,p].x)
startups
```

      0
      1
      2
      3
      4

      Type0
      7.0
      0.0
      0.0
      0.0
      0.0

      Type1
      0.0
      5.0
      0.0
      1.0
      0.0

      Type2
      0.0
      0.0
      0.0
      2.0
      0.0

---
## References

H. Paul Williams, Model Building in Mathematical Programming, fifth edition.

Copyright © 2020 Gurobi Optimization, LLC

```python

```


## Assignment I: AI318 Reinforcement Learning

>- **Name:** **MAROUF** Imad Eddine

>- **Email:** imad.marouf@telecom-paris.fr

>- **Due to:** 24/01/2021

----------------------------------------------------------------------------

# Lab on Stochastic Linear Bandits :

We provide the environment to run a standard linear bandit experiment. The objective of this lab session is to understand how to implement LinUCB, the algorithm seen in class and its variant LinTS. We shall see that in practice there are some shortcomings in the implementation to make it efficient so we will guide you to obtain a working version.

Questions are inline in the notebook and some reserved space are allocated for answers, but feel free to add cells for remarks and run your own experiments to test hypotheses you may have.

```python
import numpy as np
from model import Environment, Agent
from display import plot_regret

from scipy.stats import bernoulli
from math import log

import random
import sys

import matplotlib.pyplot as plt
%matplotlib inline

import warnings
warnings.filterwarnings("ignore")

import seaborn as sns
```

# Action generators

```python
seed = 42

def randomActionsGenerator(K,d):
    """
    K: int -- number of action vectors to be generated
    d : int -- dimension of the action space
    returns : an array of K vectors uniformly sampled on the unit sphere in R^d
    """
    vecs = np.random.multivariate_normal(np.zeros(d), np.eye(d), size =K)
    norms = np.linalg.norm(vecs,axis=1)
    return vecs / norms[:,np.newaxis]
```

```python
a = randomActionsGenerator(100,2)
```

```python
plt.scatter(a[:,0],a[:,1])
```

# Environment Class

```python
class LinearBandit(Environment):

    def __init__(self, theta, K, model='gaussian', var=1.):
        """
        theta: d-dimensional vector (bounded) representing the hidden parameter
        K: number of actions per round (random action vectors generated each time)
        """
        self.model = model
        self.d = np.size(theta)
        self.theta = theta
        self.K = K
        self.var = var
        self.current_action_set = np.zeros(self.d)

    def get_action_set(self):
        self.current_action_set = randomActionsGenerator(self.K, self.d)
        return self.current_action_set

    def get_reward(self, action):

        """ sample reward given action and the model of this bandit environment
        action: d-dimensional vector (action chosen by the learner)
        """
        mean = np.dot(action, self.theta)
        if self.model == 'gaussian':
            return np.random.normal(mean, scale=self.var)
        else:
            # Bernoulli distributed rewards
            #rewardBern = np.random.binomial(n=1, p=params[arm], size=size)
            return rewardBern

    def get_means(self):
        return np.dot(self.current_action_set, self.theta)
```

# Linear Bandit Agents

## LinUCB : Implementing optimism in $R^d$

As seen in class, the actions are now vectors in $R^d$, representing contextual features, and the environment is assumed to generate rewards according to some hidden linear function $f_\theta(a) = a^\top \theta$. The goal of the learner is thus to estimate $\theta$ while keeping a measure of the uncertainty in all the directions of the feature space.

* 1 - **Baselines**: First implement a LinUniform Agent that returns one of the action vectors of the action set, chosen uniformly at random. Then, implement an LinEpsilonGreedy agent as seen in the previous Lab.

> - **Do you think these agents can have a sublinear regret ?**
>> - Yes, theoretically, we know that LinUniform has linear regret and LinEpsilonGreedy has sublinear regret.

> - **Open question: Are they good baselines ?**
>> - **Answer:** Yes, we could consider them as good baselines to build first intuitions about our bandit problem (Although, they are not very sophisticated when it comes to providing optimal balance between exploration-exploitation tradeoff). LinUniform tries pick randomly a good actions, on the other hand LinEpsilonGreedy tries to take good actions with maximum reward, most of the time it get stuck at sub-optimal actions which gives good results comparing to LinUniform but it prevents from exploring other actions which could lead to better rewards on the long-run.

* 2 - Implement LinUCB as studied in class: as for LinUCB, you need to compute UCBs for each arm of the current action set received from the environment, but this time the exploration bonus depends on the history of taken actions and received rewards.

* One key step is to invert the covariance matrix in order to compute the elliptical norm of each available action. Remark however that at round $t+1$, the new covariance matrix is very similar to the previous one at round $t$...
> - **Can you think of a way to optimize this step by simply updating the old one ?**
> - Hint : You can search for a way to compute the inverse of the sum of an invertible matrix A and the outer product, $ uv^\top$, of vectors u and v
>> - **Answer:** By using  _Sherman–Morrison formula_
>>> - _Lemma:_ If $A$ and $A+B$ are _invertible_, and $B$ has rank 1, then let $g=trace(B.A−1)$.
Then $g≠−1$ and $(A+B)^{−1} = A^{−1}− \frac{1}{1+g} * A^{−1}.B.A^{−1}$.

* 3 - It is common practice to modify LinUCB by multiplying the confidence bonus of each arm by some hyperparameter $ 0<\alpha <1 $.
> - **What would be the effect of doing so ? Do you think it is a good idea ? What are the risks ?**
>> - **Answer:** $\alpha$ values has an impact on the exploration-exploitation of the model, it should be set diligently by tuning (i.e: it increases incentives for exploration or exploitation). If it $\alpha = 0 $, the algorithm becomes greedier it privelages exploitation rather than searching for other possibly optimal action, which may lead the agent into being stuck at sub-optimal action.

> - Implement this little modification and compare the results for several values of $\alpha$ (see further down for experiments scripts).

```python
class LinUniform(Agent):
    def __init__(self):
        pass

    def get_action(self, arms):
        K, _ = arms.shape
        return arms[np.random.choice(K)]

    def receive_reward(self, chosen_arm, reward):
        pass

    def reset(self):
        pass

    #@staticmethod
    def name(self):
        return 'Unif'
```

```python
class LinEpsilonGreedy(Agent):
    def __init__(self, d, lambda_reg, epsilon, label="LinEpsilonGreedy"):
        self.epsilon = epsilon
        self.d = d
        self.lambda_reg = lambda_reg
        self.label = label
        self.reset()

    def get_action(self, arms):
        if np.random.random() < self.epsilon :
            theta = np.dot(self.V_inv, self.b)
            y_hats = [np.dot(theta, a) for a in arms]
            return arms[np.argmax(y_hats)]
        else :
            return arms[np.random.randint(arms.shape[0])]

    def receive_reward(self, chosen_arm, reward):
        outer_product = np.outer(chosen_arm,chosen_arm)
        factor = 1 + np.dot(chosen_arm, np.dot(self.V_inv, chosen_arm))
        self.V_inv -= np.dot(self.V_inv, np.dot(outer_product, self.V_inv))/factor #Sherman-Morrison formula
        self.b += reward*chosen_arm

    def reset(self):
        self.V_inv = np.diag(np.ones(self.d)/self.lambda_reg)
        self.b = np.zeros(self.d)

    def name(self):
        return self.label
```

```python
from numpy.linalg import pinv

class LinUCB(Agent):
    def __init__(self, T, d, delta, lambda_reg, alpha = 1, label="LinUCB"):
        """
        TODO: describe inputs
        TODO: add an input alpha to optionally scale the exploration bonus in the get_action function
        """
        self.T = T
        self.d = d
        self.delta = delta
        self.lambda_reg = lambda_reg

        ### TODO
        self.alpha = alpha
        self.label = label
        self.cov = self.lambda_reg * np.identity(d)
        lmbd = np.sqrt(self.lambda_reg)
        cst = 2 * (np.log(1 / self.delta))
        self.beta = [(lmbd) + np.sqrt(cst + self.d * np.log((self.lambda_reg * self.d + t) / self.lambda_reg * self.d)) for t in range(1, self.T)]

        #Re-setting the parameters
        self.reset()

    def reset(self):
        # reset all local variables that should not be kept when the experiment is restarted
        self.t = 0
        ### TODO
        self.tild_theta = np.zeros(self.d)
        self.cov = self.lambda_reg * np.identity(self.d)
        self.invcov = np.identity(self.d)
        self.previous = np.zeros(self.d)

    def get_action(self, arms):
        """
            This function implements LinUCB
            Input:
            -------
            arms : list of arms (d-dimensional vectors)

            Output:
            -------
            chosen_arm : index of the pulled arm
            """
        # compute the UCB of each of the arm in arms, here arms are vectors
        # ucbs = np.array(K)

        # for i in range(K):
        # ucbs[i] = 1# use hat_theta and beta and the covariance matrix.
        #  beta_n is the upper bound given at the very end of the class

        K, _ = arms.shape
        self.UCBs = np.zeros(K)

        ### TODO
        for index_arm in range(K):
            a = arms[index_arm, :]
            covxa = np.inner(self.invcov, a.T)
            # Multiply the confidence bonus by alpha
            self.UCBs[index_arm] = np.dot(self.tild_theta, a) + self.alpha * self.beta[self.t - 1 ] * np.sqrt((np.dot(a, covxa)))

        index = np.argmax(self.UCBs)
        chosen_arm = arms[index]
        AAt = np.outer(chosen_arm, chosen_arm.T)
        self.cov += AAt
        self.invcov = pinv(self.cov)

        return chosen_arm
        #     return np.random.choice(K)

    def receive_reward(self, chosen_arm, reward):
        ### TODO
        self.previous += reward * chosen_arm
        #Inner product
        self.tild_theta = np.inner(self.invcov, self.previous)
        self.t += 1
        pass

    def name(self):
        return self.label
```

--------------------------------------------------------------------------------------

## LinTS : Taking the Bayesian way

Thompson Sampling is a popular bayesian alternative to the standard optimistic bandit algorithms (see Chapter 36 of Bandit Algorithms). The key idea is to rely on Bayesian *samples* to get a proxy for the hidden parameter $\theta$ of the problem instead of building high-probability confidence regions.

* 1-  Let us place a Gaussian prior with mean $\mathbf{0}$ and covariance $\sigma_0 I$ on $\theta$. Given actions $A_1,\ldots,A_t$ and rewards $Y_1,\ldots,Y_t$,
> - **Can you compute the expression of the posterior at the beginning of round $t+1$ ?**
>> - **Answer:**

Let $f$ be the PDF of $\theta$ prior distribution, $\pi$, such that $\pi \sim \mathcal{N}(\mu_0, \Sigma_0)$. After observation $A, Y$, the $log$ of $\theta$ posterior distribution's PDF is given by :
$$
\begin{align}
\log (f(Y|\theta, A)f(\theta)) + cste &=  -\frac{1}{2}(\theta - \mu_0)^T\Sigma_0^{-1}(\theta - \mu_0) - \frac{1}{2\sigma^2}(Y - A^T\theta)^T(Y - A^T\theta) + cste\\
&= -\frac{1}{2}[\theta^T(\Sigma_0^{-1}+\frac{1}{\sigma^2}AA^T)\theta - 2\theta^T(\Sigma_0^{-1}\mu_0 + \frac{Y}{\sigma^2}A)] + cste
\end{align}
$$
Now, let us denote
$$
\begin{align}
\Sigma^{-1} &= &\Sigma_0^{-1}+\frac{1}{\sigma^2}AA^T \\
\mu &= &\Sigma(\Sigma_0^{-1}\mu_0 + \frac{Y}{\sigma^2}A)
\end{align}
$$
The $log$ of $\theta$ posterior distribution's PDF is now given by :
$$
\begin{align}
-\frac{1}{2}[\theta^T\Sigma^{-1}\theta - 2\theta^T\Sigma^{-1}\mu] + cste &= -\frac{1}{2}[\theta^T\Sigma^{-1}\theta - 2\theta^T\Sigma^{-1}\mu + \mu^T\mu] + cste \\
& = -\frac{1}{2}(\theta - \mu)^T\Sigma^{-1}(\theta - \mu) + cste
\end{align}
$$
which proves that $\theta$ posterior distribution follows $\mathcal{N}(\mu, \Sigma)$.

Now, let us denote by $\pi_t$ $\theta$'s distribution at the begining of round $t$. We proved that for all $t\geq0$, $\pi_{t+1} \sim \mathcal{N}(\mu_{t+1}, \Sigma_{t+1})$ such that :
$$\begin{align}
\Sigma_{t+1}^{-1} &= \Sigma_{t}^{-1}+\frac{1}{\sigma^2}A_tA_t^T = \frac{1}{\sigma_0^2}I + \frac{1}{\sigma^2}\sum\limits_{i=1}^t A_iA_i^T \\
\Sigma_{t+1}^{-1}\mu_{t+1} &= \Sigma_{t}^{-1}\mu_{t} + \frac{Y_t}{\sigma^2}A_t =\frac{1}{\sigma^2}\sum\limits_{i=1}^t Y_iA_i
\end{align}$$

>> - Thus, for all $t \geq 1$, samplig for $\theta$ is done according to the distribution $\pi_t \sim N(\mu, \Sigma_{t})$, where:
>>> - $$\Sigma_{t} = (\frac{1}{\delta_{0}^2} I_{d} + \frac{1}{\delta^2} \Sigma_{i=1}^{t-1} A_{i} A_{i}^T)^{-1}$$
>>> - $$\mu_{t} = \frac{1}{\delta^2} \Sigma_{t} (\Sigma_{i=1}^{t-1} Y_{i} A_{i}) $$

* 2- Implement a LinTS (Linear Thompson Sampling) agent and run it against LinUCB and LinUCB with some $alpha$ scaling parameter (see further down).

**Strategy:**
> - 1. Sample a random variable $X_b$ from the prior of bandit $b$, for all b.
> - 2. Select the bandit with largest sample, i.e. select bandit $B$ = $argmax$($X_b$).
> - 3. Observe the result of pulling bandit  $B$, and update your prior on bandit $B$ using the conjugate model update rule.
> - 4. Repeat.

```python
from numpy.linalg import pinv

class LinTS(Agent):
    def __init__(self, T, d, sigma, sigma_init, label="LinTS"):
        self.T = T
        self.d = d
        self.sigma = sigma

        ### TODO
        self.sigma_init = sigma_init
        self.label = label

        #Re-setting the parameters
        self.reset()

    def reset(self):
        # reset all local variables that should not be kept when the experiment is restarted
        self.t = 0
        ### TODO
        self.tild_theta = np.zeros(self.d)
        self.cov = (1/self.sigma_init**2) * np.identity(self.d)
        self.mu = np.zeros(self.d)
        self.previous = np.zeros(self.d)

    def get_action(self, arms):
        """
            This function implements LinTS
            Input:
            -------
            arms : list of arms (d-dimensional vectors)

            Output:
            -------
            chosen_arm : index of the pulled arm
            """
        # compute the UCB of each of the arm in arms, here arms are vectors
        # ucbs = np.array(K)

        # for i in range(K):
        # ucbs[i] = 1# use hat_theta and beta and the covariance matrix.
        #  beta_n is the upper bound given at the very end of the class

        K, _ = arms.shape
        self.UCBs = np.zeros(K)

        ### TODO
        for index_arm in range(K):
            a = arms[index_arm, :]
            self.UCBs[index_arm] = np.dot(self.tild_theta, a)

        index = np.argmax(self.UCBs)
        chosen_arm = arms[index]

        outer_product = (1/(self.sigma**2)) * (np.outer(chosen_arm, chosen_arm.T))
        fraction = 1 + np.dot(chosen_arm, np.dot(self.cov, chosen_arm))
        self.cov -= (1 / fraction) * np.dot(self.cov, np.dot(outer_product, self.cov))

        return chosen_arm

    def receive_reward(self, chosen_arm, reward):
        ### TODO
        self.previous += reward * chosen_arm

        #Pick tild_theta
        self.mu = (1/(self.sigma**2)) * self.previous
        mean_t = np.dot(self.cov, self.mu)

        self.tild_theta = np.random.multivariate_normal(mean_t, self.cov)
        self.t += 1
        pass

    def name(self):
        return self.label
```

# Play !
The function play runs one path of regret for one agent. The function experiment runs all agents several (Nmc) times and returns all the logged data. Feel free to check the inputs and outputs required when you decide on the implementation of your own agents.

```python
def play(environment, agent, Nmc, T):
    data = np.zeros((Nmc, T))

    for n in range(Nmc):
        agent.reset()
        for t in range(T):
            action_set = environment.get_action_set()
            action = agent.get_action(action_set)
            reward = environment.get_reward(action)
            agent.receive_reward(action,reward)

            # compute instant regret
            means = environment.get_means()
            best_reward = np.max(means)
            data[n,t]= best_reward - reward

    return agent.name(), data

def experiment(environment, agents, Nmc, T):

    all_data = {}

    for agent in agents:
        agent_id, regrets = play(environment, agent,Nmc, T)

        all_data[agent_id] = regrets

    return all_data
```

--------------------------------------------------------------------------------------

# 1 -  Example: LinUCB versus LinUniform

- Before delving into the analysis of experiments we need to fix the seed for generating distributions.

```python
seed = 42
np.random.seed(42)
```

```python
# import time

d = 2  # dimension
K = 8  # number of arms

# parametor vector \theta, normalized :
theta = np.random.randint(2, size=d)
theta = theta / np.linalg.norm(theta)

#theta = randomActionsGenerator(1, d)

T = 100  # Finite Horizon
N = 10  # Monte Carlo simulations

delta = 0.1

# save subsampled points for Figures
Nsub = 100
tsav = range(2, T, Nsub)

#choice of quantile display
q = 10
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(T, d, delta, lambda_reg=1.)
agent2 = LinUniform()
agent3 = LinEpsilonGreedy(d, lambda_reg = 1., epsilon = 0.5)

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3], Nmc=N, T=T)

plot_regret(all_data)
```

#### _You should get a straight diagonal line for Unif and a nice, almost flat curve for LinUCB._
**Condition: SATISFIED**

**Comment:**
> - As seen in the lecture, LinUCB outperforms both LinUniform, and LinEpsilonGreedy. Since the former, picks actions randomly, not a good stategy to maximize the rewards, while the later is greedy by taking only action which maximized the reward which could be sub-optimal preventing the agent from exploring other actions which could be more optimal.

### 1.1 - Discussion:  Experimenting with Parameter $\lambda_{reg}$

- LinUCB takes as input a parameter $\lambda_{reg}$ that controls the regularisation in the least squares estimation. Choosing it is not easy and to the best of my knowledge there is no clear rule... but maybe we could see experimentally if it impacts the results.

- Run a few LinUCB with different $\lambda_{reg}$ values and plot the results. Beware ! You will have to modify the function "name()" of LinUCB to include the chosen value, otherwise the plotting function will overwrite each curve (this holds for the next experiments as well).

```python
basicGaussianBandit = LinearBandit(theta, K)
#....TODO
agent1 = LinUCB(T, d, delta, lambda_reg=0.1, label="lmbd_0.1")
agent2 = LinUCB(T,d, delta, lambda_reg=0.5, label="lmbd_0.5")
agent3 = LinUCB(T, d, delta, lambda_reg=0.8, label="lmbd_0.8")

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3], Nmc=10, T=100)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
#....TODO
agent1 = LinUCB(T,d, delta, lambda_reg=1, label="lmbd_1")
agent2 = LinUCB(T,d, delta, lambda_reg=1.5, label="lmbd_1.5")
agent3 = LinUCB(T,d, delta, lambda_reg=2, label="lmbd_2")

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3], Nmc=10, T=100)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
#....TODO
agent1 = LinUCB(T,d, delta, lambda_reg=2.2, label="lmbd_2.2")
agent2 = LinUCB(T,d, delta, lambda_reg=2.5, label="lmbd_2.5")
agent3 = LinUCB(T,d, delta, lambda_reg=3, label="lmbd_3")

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3], Nmc=10, T=100)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
#....TODO
agent1 = LinUCB(T,d, delta, lambda_reg=0.5, label="lmbd_0.5")
agent2 = LinUCB(T,d, delta, lambda_reg=1.5, label="lmbd_1.5")
agent3 = LinUCB(T,d, delta, lambda_reg=2.5, label="lmbd_2.5")

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3], Nmc=10, T=100)

plot_regret(all_data)
```

**Comment:**
> - $\lambda_{reg}$ is a regularization hyperparameter for LSE. Thus, it prevents the model from overfitting, to all fit the data samples. It should be tunned correctly in order to provide an optimal balance between variance-bias.
> - The model is very stochastic which makes very difficult to pick the right value for $\lambda_{reg}$, and the confidence intervals are very large. But roughly, mostly in the literature it has 0.1 value.

## 1.2- LinUCB with $\alpha$ scaling

```python
basicGaussianBandit = LinearBandit(theta, K)

agent1 = LinUCB(T, d, delta, lambda_reg=0.1, alpha = 0, label="alpha_0")
agent2 = LinUCB(T, d, delta, lambda_reg=0.1, alpha = 0.1, label="alpha_0.1")
agent3 = LinUCB(T,d, delta, lambda_reg=0.1, alpha=0.5, label="alpha_0.5")
agent4 = LinUCB(T,d, delta, lambda_reg=0.1, alpha=0.8, label="alpha_0.8")

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3, agent4], Nmc=10, T=100)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(T, d, delta, lambda_reg=0.1, alpha = 1, label="alpha_1")
agent2 = LinUCB(T,d, delta, lambda_reg=0.1, alpha=1.5, label="alpha_1.5")
agent3 = LinUCB(T,d, delta, lambda_reg=0.1, alpha=2, label="alpha_2")

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3], Nmc=10, T=100)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(T, d, delta, lambda_reg=0.1, alpha = 0, label="alpha_0")
agent2 = LinUCB(T, d, delta, lambda_reg=0.1, alpha = 0.1, label="alpha_0.5")
agent3 = LinUCB(T,d, delta, lambda_reg=0.1, alpha=1.0, label="alpha_1.0")
agent4 = LinUCB(T,d, delta, lambda_reg=0.1, alpha=1.5, label="alpha_1.5")

all_data = experiment(basicGaussianBandit, [agent1, agent2, agent3, agent4], Nmc=50, T=100)

plot_regret(all_data)
```

### Discussion :

**_Please comment on your results briefly. Do they validate your hypotheses or not ? Why ?_**

**Comment:**
> - Yes, as stated above, indeed $\alpha $ it effects the behavior of LinUCB, where for small values the agent becomes more inclined towards exploitation becoming more greedy.

------------------------------------------------------------------------------------------------

# 2-  LinTS versus LinUCB

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(T, d, delta, lambda_reg=1.)
agent2 = LinTS(T, d, sigma = 1, sigma_init = 1, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], N, T)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(1000, d, delta, lambda_reg=1.)
agent2 = LinTS(1000, d, sigma = 1, sigma_init = 1, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], N, 1000)

plot_regret(all_data)
```

### 2.1 Experimenting with Parameters $\sigma_{0}$ \& $\sigma$ for LinTS

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(1000, d, delta, lambda_reg=1.)
agent2 = LinTS(1000, d, sigma = 1, sigma_init = 0.01, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], N, 1000)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(1000, d, delta, lambda_reg=1.)
agent2 = LinTS(1000, d, sigma = 1, sigma_init = 5, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], N, 1000)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(1000, d, delta, lambda_reg=1.)
agent2 = LinTS(1000, d, sigma = 2, sigma_init = 1, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], N, 1000)

plot_regret(all_data)
```

**Comment:**
> - Initial values $\sigma_{0}$ and $\sigma$ for LinTS Algorithm has a major impact on its performance.
> - Thompson Sampling like LinUCB is also asymptotically optimal for Gaussian with unknown mean and variance [Honda and Takemura, 14]

### Long-Term Horizon (LinUCB versus LinTS):

#### $\sigma_{0}$ = [0.1, 0.05, 0.01]

Long horizon, T = 10000 (average over N = 100 runs)

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(10000, d, delta, lambda_reg=1.)
agent2 = LinTS(10000, d, sigma = 1, sigma_init = 0.1, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], 50, 10000)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(10000, d, delta, lambda_reg=1.)
agent2 = LinTS(10000, d, sigma = 1, sigma_init = 0.05, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], 50, 10000)

plot_regret(all_data)
```

```python
basicGaussianBandit = LinearBandit(theta, K)
agent1 = LinUCB(10000, d, delta, lambda_reg=1.)
agent2 = LinTS(10000, d, sigma = 1, sigma_init = 0.01, label="LinTS")
#....TODO

all_data = experiment(basicGaussianBandit, [agent1, agent2], 50, 10000)

plot_regret(all_data)
```

**Comment:**
> - Both LinUCB, LinTS it seems do good when the assumption that the reward distribution belong to Gaussian, but in the literature it was proven that LinTS outperforms LinUCB when the distribution is Bernolli.[Honda and Takemura, 14]

### Discussion :

**_Please comment on your results briefly. Do they validate your hypotheses or not ? Why ?_**

**Comment:**
> - Thompson sampling is really interesting algorithm, where it gives an optimal balance between exploitation-exploration trade-off, which makes it not greedy. It outperforms LinUCB in most cases.

> - An interesting phenomena, we notice is that mostly LinUCB outperforms LinTS at the beginning of iterations, due to the that LinUCB exploits bandits which gave best mean rewards, on the otherhand LinTS gives an optimal balance between exploitation-exploration, it exploits some good actions, and check other actions which could be optimal. If they are not their frequency of using used will be reduced in the following iterations. Which makes a powerful algorithm for recommendation systems (we would like to give users best recommendations to maximize rewards, at the same time, we want to explore other user's preferences).

> - Although Thompson Sampling is very powerful, it has some pitfalls like:
>> - It depends on some parameters (like noise, sigma) which are hypothesis in our case -Difficult to know their exact values-. As illustrated in the last experiments depending on those hyperparameters, somestimes LinUCB outperforms LinTS.
>> - It depends on the a critical hypothesis we have to make regarding the type of distribution of $\theta$. For instance, in our case the chosen was _Gaussian_ although it can be any other type of distribution ( _Bernolli distribtuion_ ).

```python

```


```python
from __future__ import division
from IPython.display import Latex
```

# On Galerkin approximations for the QG equations
## Supplementary material

### Cesar B. Rocha*, William R. Young, and Ian Grooms

### Winter 2015

*Scripps Institution of Oceanography, University of California, San Diego, 9500 Gilman Dr. MC 0213, La Jolla, CA/USA, <crocha@ucsd.edu>

# Can we get a sine out of cosines?
##An elementary example of Galerkin approximation

Consider the linear boundary value problem

\begin{equation}
\phi'' + \phi = 0\, ,
\end{equation}
with boundary conditions

\begin{equation}
@z = 0 \qquad \phi' = 1  \, ,
\end{equation}
and
\begin{equation}
@z = \pi \qquad \phi' = -1  \, .
\end{equation}

The most general exact solution is
\begin{equation}
\phi(z) = \sin  z + C\cos  z\, ,
\end{equation}

where $C$ is an undetermined constant. A solution can be found as a linear combination of the eigenfunctions of the linear operator $D_2 \equiv d\,^2/d\, z^2$:

\begin{equation}
\phi = \sum_{n=0}^{\infty} \phi_n\,\mathrm{e}_n\,.
\end{equation}

If we define the $\mathrm{e}_n$ as the eigenfunctions of $D_2$  with inhomogeneous boundary conditions, the series above is truncated and gives the exact solution. With homogeneous boundary conditions the series is infinite.  We define

\begin{equation}
D_2 \, \mathrm{e}_n = -\lambda_n^2\, \mathrm{e}_n\,,
\end{equation}
and

\begin{equation}
@z = 0\,,\pi:\qquad \mathrm{e}_n' = 0\,.
\end{equation}

 The Galerkin approximation represents the solution as a truncated series in this orthogonal basis:
\begin{equation}
\phi^{\mathrm{N}}_G = \sum_{n=0}^{\mathrm{N}} \phi_n\,\mathrm{e}_n\,,
\end{equation}

The eigenproblem above is a simplified Sturm-Liouville problem (e.g. Strang CSE book). The eigenfunctions are orthogonal because the operator $D_2$ is self-ajoint. It is convenient to normalize the eigenfunctions $\mathrm{e}_n$ to have unit $L_2$-norm

\begin{equation}
\frac{1}{\pi}\int_{0}^{\pi} \mathrm{e}_n\,\mathrm{e}_m \,d z= \delta_{mn} \,,
\end{equation}

Thus we have

\begin{equation}
\mathrm{e}_0 = 1 \qquad \text{and} \qquad \mathrm{e}_n = \sqrt{2} \cos n \,z\,.
\end{equation}

The coefficients $\phi_n$ are determined to minimize the $L_2$-norm of the residual

\begin{equation}
E(\phi_0, \phi_1, \cdots , \phi_N)  \equiv \frac{1}{\pi} \int_{0}^{\pi} \left( \phi - \sum_{n=0}^{\mathrm{N}} \phi_n \mathrm{e}_n\right)^2 \, d\, z \, .
\end{equation}
Thus

\begin{equation}
\phi_n = \frac {1}{\pi} \int_{0}^{\pi} \mathrm{e}_n \phi  \, d z\, .
\end{equation}

That is, the n'th coefficient is the projection of the true solution onto the n'th eigenfunction. **The Galerkin approximation is therefore the best least-squares approximation using the truncated series in the basis spanned by $\mathrm{e}_n$, $n=0,\,1,\,2,\ldots,\,\mathrm{N}$**.

At this point, one might be tempted to introduce the Galerkin series $\phi^{\mathrm{N}}_G$ into the governing equation to obtain $\phi_n$. This **naïve approach** leads to

\begin{equation}
\sum_{n=0}^{\mathrm{N}} (1-n^2)\phi_n\,\mathrm{e}_n = 0\,,
\end{equation}

from which one might wrongly conclude that $\phi_n = 0,\,n\neq 1$; $\phi_1$ is undetermined (this is correct). This approach only capturues part of the exact solution $C\,\cos z$; it does not approximates the sine term by a seires of cosines. This **naïve approach** gives the wrong answer.

Instead, to determine $\phi_n$ we project the exact equation onto the  n'th eigenfunction $\mathrm{e}_n$. That is, we multiply the exact equation  by $\tfrac{1}{\pi}\mathrm{e}_n$ and integrate over the domain:

\begin{equation}
\frac{1}{\pi}\int_0^{\pi}\mathrm{e}_n\,y'' \,dz + \frac{1}{\pi}\int_0^{\pi}\mathrm{e}_n\,y \,dz = 0\,.
\end{equation}

We have to move the derivatives off $y$. As most of exciting things in applied mathematics, this is possible using integration by parts

\begin{equation}
\frac{1}{\pi}\int_0^{\pi}\!\!\!\!\mathrm{e}_n\,y'' \,dz = \frac{1}{\pi}\int_0^{\pi}\!\!\!\!\mathrm{e}_n''\,y \,\,dz + \frac{1}{\pi}\Big[\mathrm{e}_n\,y' - \mathrm{e}_n'\,y  \Big]_0^{\pi}\,.
\end{equation}

Using the definition of $\mathrm{e}_n$ and the boundary conditions on $y$ and $\mathrm{e}_n$, we finally obtain

\begin{equation}
\phi_0 = \frac{2}{\pi}\qquad \text{and}\qquad\phi_n = \frac{2\,\sqrt{2}}{\pi\left(1-n^2\right)}\,,\qquad n \text{ even}\,\,.
\end{equation}

Notice that $\phi_1$ (the coefficient of $\cos z$) is undetermined as in the exact solution. Thus, the Galerkin approximate solution is

\begin{equation}
\phi^{\mathrm{N}}_G = \frac{2}{\pi} + \phi_1 \cos z+ \frac{4}{\pi} \sum_{n=2,\text{ even}}^{\mathrm{N}} \frac{\cos n z}{1-n^2}\,.
\end{equation}

With $\phi_1=0$, **the Galerkin series $\phi^{\mathrm{N}}_G$ is the best least-squares approximation for $\sin z$, $z \in [0,\pi]$**.

## Comparison with exact solution
We now comparare the Galerkin approximation againt the exact solution. **For simplicity we take $C = \phi_1 = 0$.**

```python
import numpy as np
from numpy import sqrt,pi,sin,cos
```

Define the domain and compute the exact solution

```python
z = np.linspace(0,pi,100)
phi_exact = sin(z)
```

A simple function to evaluate the Galerkin series

```python
def Galerkin_solution(N,z):
    """Computes the Galerkin solution with phi_1 = 0
        -------------------------------
        Inputs: N: truncation index
                z: array of depths
        -------------------------------  """

    n = np.arange(2,N+1,2)
    phi = np.zeros(z.size)
    for i in range(z.size):
        phi[i] = 2/pi + (4/pi)*( ( cos(n*z[i])/(1-n**2) ).sum() )

    return phi
```

We now compute the galerkin solutin for various $\mathrm{N}$

```python
N = np.arange(0,20,2)

phi_galerkin = np.zeros((N.size,z.size))

for i in range(N.size):
    phi_galerkin[i,:] = Galerkin_solution(N[i],z)
```

```python
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams.update({'font.size': 25, 'legend.handlelength'  : 1.25})

import seaborn as sns
sns.set(style="darkgrid")
sns.set_context("paper", font_scale=3., rc={"lines.linewidth": 1.5})
sns.set_style("darkgrid",{'grid_color':.95})
```

```python
plt.figure(figsize=(10,10))
plt.plot(phi_exact,z,'k',linewidth=3.,alpha=.8,label='exact')
for i in range(N.size):
    plt.plot(phi_galerkin[i,:],z,linewidth=2,alpha=.5,label=str(N[i]))
plt.xlabel(r'$\phi,\,\phi^{\mathrm{N}}_G$')
plt.ylabel(r'z')
plt.yticks([0,pi/4,pi/2,3*pi/4,pi],[r'$0$',r'$\pi/4$',r'$\pi/2$',r'$3\pi/4$',r'$\pi$'])
plt.ylim(0,pi)
plt.legend(loc=(.04,.15), title = r'$\mathrm{N} $')
plt.savefig('galerk_sin_cos',format='eps')
```

While the galerkin approximation $\phi^{\mathrm{N}}_G$ does not satisfy the inhomogeneous boundary conditions the solution with moderate $\mathrm{N}$ provides an acceptable approximation to the exact solution $\phi$.  However, the convergence near the boundaries is slow. To see this, note that the error at the boundaries is

\begin{equation}
E_b = \frac{4}{\pi}\sum_{n=N+1, \text{even}}^{\infty}\frac{1}{1-n^{2}}
\end{equation}

Hence

\begin{equation}
E_b \sim\! -\frac{4}{\pi}\sum_{n=N+1, \text{even}}^{\infty}\frac{1}{n^{2}}\approx -\frac{4}{\pi}\int  \frac{1}{\mathrm{N}^2} \, dz= \frac{4}{\pi \mathrm{N}}
\end{equation}

Thus the Galerkin series converges at rate $\sim\!\mathrm{N}^{-1}$ at the boundaries. Let's test this against our computational estimates.

```python
N = np.arange(0,1000,2)
phi_galerkin2 = np.zeros((N.size,z.size))

for i in range(N.size):
    phi_galerkin2[i,:] = Galerkin_solution(N[i],z)
```

Notice that the absolute error at $z=0$ (or at $z=\phi$) is simply $\phi^{\mathrm{N}}_G (0)$. We plot this value against $\mathrm{N}$ in a log$_{10}\times\log_{10}$ space. For reference we also plot the line $\mathrm{N}^{-1}$.

```python
n1 = np.array([5,350])

plt.figure(figsize=(12,8))
plt.loglog(N,phi_galerkin2[:,0],linewidth=3)
plt.loglog(n1,1/n1,'k--',alpha=.75,linewidth=2)
plt.text(10,.125,r'$N^{-1}$')
plt.xlabel(r'$\mathrm{N}$')
plt.ylabel(r'Absolute error')
plt.savefig('galerk_error',format='eps')
```

The convergence rate is $\mathrm{N^{-1}}$, consistent with our asymptotic estimate.

```python

```

```python

```


```python
from sympy import *
from sympy.abc import *
import numpy as np
init_printing()

import warnings
warnings.filterwarnings("ignore")

creq, ka, kc, ln, rpm, Lreq = symbols('C_{req} K_a Kc L_N rpm L_{req}')
```

#### Equacionamento necessario

```python
# Se L for dado em milhares de horas transfom to -> revoluções
#L = rpm*Lreq*60
```

```python
# METODO 1
ex = Eq(creq, P*ka*(L/(kc*ln))**p) #L precisa ser em revoluções
ex
```

#### Problema - Atividade 7

```python
#Cargas dinâmicas
PA = 2.5
PB = 8

#Dados gerais
KA = 1.1        #Maquina operatriz
KC = 1          #90% confiabilidade
LN =90E6        #Vida nominal rev
p_value = 0.33  #p/ esferas como elemento rolante
L_value = 30e3*350*60 #horas*rpm*60
```

```python
#Mancal A - Esquerdo - rolamento 6813
ex.subs({P:PA, ka:KA, kc:KC, L:L_value, ln:LN, p:p_value})
```

```python
#Mancal B - Direito - rolamento 6913
ex.subs({P:PB, ka:KA, kc:KC, L:L_value, ln:LN, p:p_value})
```

#### Aula 8 - Resolução do exercicio de exemplo

```python
P_list = [3e3, 5e3, 7e3]
Rpm_list = [1000, 1000, 1000]
tempo_list = [0.5, 0.3, 0.2]

lista = []
for p_ in P_list:#Tabela do rolamento, carga aplicada
    a = ex.subs({creq:8.8e3, P:p_, ka:KA, kc:KC, L:x, ln:LN, p:p_value})
    lista.append(solve(a)[0])

lista
```

```python
minner = Eq((tempo_list[0]*x*Rpm_list[0]*60)/lista[0] + (tempo_list[1]*x*Rpm_list[1]*60)/lista[1] + (tempo_list[2]*x*Rpm_list[2]*60)/lista[2],1)
minner
```

```python
print(f'O rolamento dura {solve(minner)[0]:.3f} Horas ou {solve(minner)[0]/24:.3f} dias')
```

    O rolamento dura 6486.633 Horas ou 270.276 dias

#### Problema - Atividade 8
#A

```python
P_list = [7e3, 14e3, 18e3]
Rpm_list = [1800, 1200, 800]
tempo_list = [0.55, 0.25, 0.2]

lista = []
for p_ in P_list:#Tabela do rolamento, carga aplicada
    a = ex.subs({creq:21.8e3, P:p_, ka:KA, kc:KC, L:x, ln:LN, p:p_value})
    lista.append(solve(a)[0])

minner = Eq((tempo_list[0]*x*Rpm_list[0]*60)/lista[0] + (tempo_list[1]*x*Rpm_list[1]*60)/lista[1] + (tempo_list[2]*x*Rpm_list[2]*60)/lista[2],1)
print(f'O rolamento dura {solve(minner)[0]:.3f} Horas ou {solve(minner)[0]/24:.3f} dias')
```

    O rolamento dura 5629.534 Horas ou 234.564 dias

#B

```python
P_list = [3e3]
Rpm_list = [1250]
tempo_list = [1]

lista = []
for p_ in P_list:#Tabela do rolamento, carga aplicada
    a = ex.subs({creq:6.4e3, P:p_, ka:2.25, kc:KC, L:x, ln:LN, p:p_value})
    lista.append(solve(a)[0])

minner = Eq((tempo_list[0]*x*Rpm_list[0]*60)/lista[0], 1)
print(f'O rolamento dura {solve(minner)[0]:.3f} Horas ou {solve(minner)[0]/6:.3f} dias')
```

    O rolamento dura 1021.196 Horas ou 170.199 dias

## Cargas radiais e axiais juntas

```python
#from sympy import *
#from sympy.abc import *
```

```python
cr_exp = Eq(creq, P*ka*(L/(kc*ln))**p) #L precisa ser em revoluções
cr_exp
```

```python
Fr, Fa = symbols('F_r F_a')
P_exp = Eq(P, X*V*Fr + Y*Fa)
P_exp
```

```python
cr_exp = Eq(creq, (X*V*Fr + Y*Fa)*ka*(L/(kc*ln))**p) #L precisa ser em revoluções
cr_exp
```

```python

```

```python

```


## Using SafeDrone for reliability and safety modelling

SafeDrone is a framework for Reliability/Safety Modelling and Evaluation of Multicopters (Multi-rotor Drones) and Electric powered Vertical TakeOff and Landing (eVTOL) Aircrafts. SafeDrone implement a reliability evaluation approach based on a Markov process.For more information please refer to [this](https://link.springer.com/chapter/10.1007/978-3-030-17771-3_8) work.

The drone consists of three main electrical components that are essential for its operation.
1. Motor (Rotor)
2. Battery
3. Onboard chip

In the following, we show an example usage of evaluation of reliability for a multi rotor drone system. This is done by predicting the failure probability of the three components.

```python
import sys
import os

import inspect

from IPython.display import Markdown as md

from sys import platform

if platform == "linux" or platform == "linux2":
    currentdir = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))
    parentdir = os.path.dirname(currentdir)
    sys.path.insert(0, parentdir)
elif platform == "win32":
    # in Windows, the first path entry contains the directory where the notebook is
    sys.path.insert(0, os.path.dirname(sys.path[0]))

from SafeDrones.core.SafeDrones import SafeDrones
```

```python
eval = SafeDrones()
```

### 1. Motor Failure risk prediction

To start, let us evaluate the risk of motor failure. For this, we need to provide motor status as input. This can be either "0" for out of order, or "1" for operational. This needs to be provided for all the motors present, as an array. For example, for Quadcopters, the status can be given as any array [1, 0, 0, 1]. Which shows that the 2nd and 3rd motor failure. For our example, let us consider a hexacopter with all operational motors, i.e [1,1,1,1,1,1].

The next required input is motor configuration. This should be given as a string of length equal to the number of motors, made up of 'P' and 'N'. Where 'P' stands for positive rotion, and 'N' for negative rotation. To start with, let us consider a configuration of hexacopter as 'PNPNPN'.

The last two parameters needed are failure rate, and time. Let us consider 0.001 as failure rate and 100 as time.

```python
P_Fail, MTTF = eval.Motor_Failure_Risk_Calc([1,1,1,1,1,1], 'PNPNPN', 0.001, 100)

print("The probability of failure obtained is: ", P_Fail)

print("The mean time between failure obtained is: ", MTTF)
```

    The probability of failure obtained is:  0.0489641066173940
    The mean time between failure obtained is:  483.333333333333

 Now, let us experiments with different configurations and failure statuses, while keeping the failure rate and the time constant.

```python
P_Fail, MTTF = eval.Motor_Failure_Risk_Calc([0,1,1,1,1,1], 'PNPNPN', 0.001, 100)

print("The probability of failure obtained is: ", P_Fail)

print("The mean time between failure obtained is: ", MTTF)
```

    The probability of failure obtained is:  0.195392392995276
    The mean time between failure obtained is:  450.000000000000

```python
P_Fail, MTTF = eval.Motor_Failure_Risk_Calc([0,1,1,0,1,1], 'PNPNPN', 0.001, 100)

print("The probability of failure obtained is: ", P_Fail)

print("The mean time between failure obtained is: ", MTTF)
```

    The probability of failure obtained is:  0.329679953964361
    The mean time between failure obtained is:  350.000000000000

```python
P_Fail, MTTF = eval.Motor_Failure_Risk_Calc([1,1,1,1,1,1], 'PPNNPN', 0.001, 100)

print("The probability of failure obtained is: ", P_Fail)

print("The mean time between failure obtained is: ", MTTF)
```

    The probability of failure obtained is:  0.196030818613950
    The mean time between failure obtained is:  416.666666666667

We can see that the probability of failure is not only dependent on the failure statuses, but also the configuration. Let us extend our experiments to an Octacopter.

```python
P_Fail, MTTF = eval.Motor_Failure_Risk_Calc([1,1,1,1,1,1], 'PPNNPPNN', 0.001, 100)

print("The probability of failure obtained is: ", P_Fail)

print("The mean time between failure obtained is: ", MTTF)
```

    The probability of failure obtained is:  0.196030818613950
    The mean time between failure obtained is:  452.380952380952

Now let us check how the reliability of a hexa copter changes over time.

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

P_Fail2 = np.zeros(1000)
P_Fail3 = np.zeros(1000)
MTTF2 = np.zeros(1000)
MTTF3 = np.zeros(1000)

for ii in tqdm(range(1000)):
    P_Fail2[ii], MTTF2[ii] = eval.Motor_Failure_Risk_Calc([1,1,1,1,1,1], 'PNPNPN', 0.001, ii)
    P_Fail3[ii], MTTF3[ii] = eval.Motor_Failure_Risk_Calc([0,1,1,1,1,1], 'PNPNPN', 0.001, ii)

time = list(range(1000))
plt.plot(time,P_Fail2, label='No Failure',marker='o')
plt.plot(time,P_Fail3, label='One Rotor Failure',marker='o')

plt.xlabel('Time (hours)')
plt.ylabel('Probability')
plt.legend()
plt.title('Unreliability of PNPNPN Hexacopter vs. Time')
plt.grid()
plt.show()
```

From the graph, it can be seen that the probability of failure with time is always higher for drone operating with one failed rotor.

### 2. Battery Failure risk prediction

Now, let us use SafeDrone to predict the failure risk of a battery. The failure risk of battery is modelled using morkov mdoel from [this](https://link.springer.com/chapter/10.1007/978-3-030-32872-6_8) work. The input required are battery level, failure rate, etc. Battery level is an integer between 100 and 0, which indicates charge level. The failure rate, battery charge rate alpha, and discharge rate beta, and the battery degradation rate can be used as default.

```python
import sympy
from sympy.utilities.lambdify import lambdify
import numpy as np
import time
from multiprocessing import Pool
from functools import partial

t = sympy.Symbol('t')

def estate_np(x, y, z):
    L = 0.001
    a = 0.008
    b = 0.007
    d = 0.0064
    time = 100
    M_Battery = np.matrix([[-L-a-d,  b,       0,  0,       0,  0, 0, 0],
                                    [     a, -b,       0,  0,       0,  0, 0, 0],
                                    [     d,  0,  -L-a-d,  b,       0,  0, 0, 0],
                                    [     0,  0,       a, -b,       0,  0, 0, 0],
                                    [     0,  0,       d,  0,  -L-a-d,  b, 0, 0],
                                    [     0,  0,       0,  0,       a, -b, 0, 0],
                                    [     0,  0,       0,  0,       d,  0, 0, 0],
                                    [     L,  0,       L,  0,       L,  0, 0, 0]], dtype=np.float128)
    #print(M_Battery)
    M_Battery = M_Battery.astype(np.float64)
    P0_Battery = np.matrix([[1],[0],[0],[0],[0],[0],[0],[0]])
    P0_Battery = P0_Battery.astype(np.float64)
    #print(P0_Battery[0][0])

    print(M_Battery)

    P_Battery1 = np.exp(M_Battery*np.float64(time))#*P0_Battery
    P_Battery1 = P_Battery1*P0_Battery

    #print(P_Battery1)

    P_Battery_Fail = P_Battery1[-1] + P_Battery1[-2]

    #print(P_Battery_Fail)

    #print(P_Battery[-1],'\n\n\n',P_Battery[-2])
    #print("In new function. . . . . ")

    return P_Battery_Fail

def estate(x, y, z):
    L = 0.001
    a = 0.008
    b = 0.007
    d = 0.0064
    time = 100
    M_Battery_sym = sympy.Symbol('M_Battery')
    M_Battery = sympy.Matrix([[-L-a-d,  b,       0,  0,       0,  0, 0, 0],
                                    [     a, -b,       0,  0,       0,  0, 0, 0],
                                    [     d,  0,  -L-a-d,  b,       0,  0, 0, 0],
                                    [     0,  0,       a, -b,       0,  0, 0, 0],
                                    [     0,  0,       d,  0,  -L-a-d,  b, 0, 0],
                                    [     0,  0,       0,  0,       a, -b, 0, 0],
                                    [     0,  0,       0,  0,       d,  0, 0, 0],
                                    [     L,  0,       L,  0,       L,  0, 0, 0]])
    P0_Battery = sympy.Matrix([[1],[0],[0],[0],[0],[0],[0],[0]])

    P0_Battery_sym = sympy.Symbol('P0_Battery')

    print(M_Battery)

    #P_Battery = lambdify(f, (M_Battery*t)*P0_Battery)

    P_Battery = sympy.exp(M_Battery*t)*P0_Battery
    #P_Battery = P0_Battery_sym*sympy.exp(M_Battery_sym*t)
    #print(P_Battery)

    #U = P_Battery.evalf(subs={M_Battery_sym:M_Battery, P0_Battery_sym:P0_Battery})
    U = P_Battery.subs({M_Battery_sym:M_Battery, t:time, P0_Battery_sym:P0_Battery})

    #sympy.pprint(P_Battery[-1])
    #sympy.pprint(P_Battery[-2])

    UPOP = P_Battery.evalf(subs={t: time})
    #print(UPOP[-1] + UPOP[-2])

    P_Battery_Fail = P_Battery[-1] + P_Battery[-2]

    #print(P_Battery[-1],'\n\n\n',P_Battery[-2])

    return P_Battery_Fail.evalf(subs={t: time}), P_Battery
    #return UPOP, P_Battery_Fail
    #return UPOP[-1] + UPOP[-2], P_Battery

start = time.time()
d, s = estate(1,2,3)
end = time.time()
print("The function old takes: ", end - start, " Seconds.", d)

start = time.time()
d1 = estate_np(1,2,3)
end = time.time()
print("The function new takes: ", end - start, " Seconds.", d1)

#print(d, d1)
#s
```

```python
import time

start = time.time()
P_Fail, MTTF = eval.Battery_Failure_Risk_Calc(80, 100)
end = time.time()
print("The function takes: ", end - start, " Seconds.")
print("The probability of battery failure obtained is: ", P_Fail)

print("The mean time between failure for battery obtained is: ", MTTF)
```

    Matrix([[3.90787197022022e-15*t**2*exp(-0.01978137518117*t) + 1.03448492182524e-20*t**2*exp(-0.00261862481883003*t) + 4.55388302823019e-13*t*exp(-0.01978137518117*t) - 8.82760466624206e-19*t*exp(-0.00261862481883003*t) + 0.744716022650919*exp(-0.01978137518117*t) + 0.255283977248708*exp(-0.00261862481883003*t) + 9.79262531842392*(1.75921126624456e-13*t**2*exp(-0.01978137518117*t) + 1.02501340748518e-11*t*exp(-0.01978137518117*t))/t + 678.78539915058*(4.06575814682064e-20*t**2*exp(-0.00261862481883003*t) - 1.73472347597681e-18*t*exp(-0.00261862481883003*t))/t], [7.13543213498172e-15*t**2*exp(-0.01978137518117*t) - 3.44828307275081e-21*t**2*exp(-0.00261862481883003*t) + 8.31503527326019e-13*t*exp(-0.01978137518117*t) + 8.82760466624206e-19*t*exp(-0.00261862481883003*t) - 0.466125756034235*exp(-0.01978137518117*t) + 0.466125755850956*exp(-0.00261862481883003*t) + 9.79262531842392*(3.2121657764228e-13*t**2*exp(-0.01978137518117*t) + 1.87159454601016e-11*t*exp(-0.01978137518117*t))/t + 678.78539915058*(-1.35525271560688e-20*t**2*exp(-0.00261862481883003*t) + 1.73472347597681e-18*t*exp(-0.00261862481883003*t))/t], [3.01426789578181e-15*t**2*exp(-0.01978137518117*t) + 6.89656614550161e-21*t**2*exp(-0.00261862481883003*t) + 0.0035494525051735*t*exp(-0.01978137518117*t) + 0.000417087418630826*t*exp(-0.00261862481883003*t) - 0.924150084182407*exp(-0.01978137518117*t) - 0.414561249667248*exp(-0.00261862481883003*t) + 9.79262531842392*(1.35693648158075e-13*t**2*exp(-0.01978137518117*t) + 0.0798930579569286*t*exp(-0.01978137518117*t))/t + 678.78539915058*(2.71050543121376e-20*t**2*exp(-0.00261862481883003*t) + 0.000819623628366979*t*exp(-0.00261862481883003*t))/t], [2.84299055021481e-15*t**2*exp(-0.01978137518117*t) - 6.03449537731391e-21*t**2*exp(-0.00261862481883003*t) - 0.00222164044425639*t*exp(-0.01978137518117*t) + 0.000761564397266618*t*exp(-0.00261862481883003*t) + 0.404616886808654*exp(-0.01978137518117*t) - 0.930770414761829*exp(-0.00261862481883003*t) + 9.79262531842392*(1.27983235988227e-13*t**2*exp(-0.01978137518117*t) - 0.0500059230300241*t*exp(-0.01978137518117*t))/t + 678.78539915058*(-2.37169225231204e-20*t**2*exp(-0.00261862481883003*t) + 0.00149655958593004*t*exp(-0.00261862481883003*t))/t], [8.45866928352531e-6*t**2*exp(-0.01978137518117*t) + 3.40722352916533e-7*t**2*exp(-0.00261862481883003*t) - 0.00474256008206368*t*exp(-0.01978137518117*t) - 0.000561490122124597*t*exp(-0.00261862481883003*t) + 1.00652926680544*exp(-0.01978137518117*t) + 0.787782046071514*exp(-0.00261862481883003*t) + 9.79262531842392*(0.000380784898134112*t**2*exp(-0.01978137518117*t) - 0.106748189177984*t*exp(-0.01978137518117*t))/t + 678.78539915058*(1.33911539254732e-6*t**2*exp(-0.00261862481883003*t) - 0.00110339116125515*t*exp(-0.00261862481883003*t))/t], [-5.2943719505995e-6*t**2*exp(-0.01978137518117*t) + 6.22128603605317e-7*t**2*exp(-0.00261862481883003*t) + 0.00213996822467164*t*exp(-0.01978137518117*t) - 0.00130921866925709*t*exp(-0.00261862481883003*t) - 0.462568841314472*exp(-0.01978137518117*t) + 1.73723425251081*exp(-0.00261862481883003*t) + 9.79262531842392*(-0.000238337357368926*t**2*exp(-0.01978137518117*t) + 0.0481675991298607*t*exp(-0.01978137518117*t))/t + 678.78539915058*(2.44510517757529e-6*t**2*exp(-0.00261862481883003*t) - 0.0025727617475129*t*exp(-0.00261862481883003*t))/t], [-2.73668958313883e-6*t**2*exp(-0.01978137518117*t) - 8.32735962397277e-7*t**2*exp(-0.00261862481883003*t) + 0.00125769847298647*t*exp(-0.01978137518117*t) + 0.000736289079267305*t*exp(-0.00261862481883003*t) + 0.646911274240916 - 0.262069183034453*exp(-0.01978137518117*t) - 1.64418972302953*exp(-0.00261862481883003*t) + 9.79262531842392*(-0.000123197872999935*t**2*exp(-0.01978137518117*t) + 0.0283089791589526*t*exp(-0.01978137518117*t))/t + 678.78539915058*(-3.27283941199794e-6*t**2*exp(-0.00261862481883003*t) + 0.00144689074692566*t*exp(-0.00261862481883003*t))/t], [-4.27607745837985e-7*t**2*exp(-0.01978137518117*t) - 1.30114994124575e-7*t**2*exp(-0.00261862481883003*t) + 1.70813243095691e-5*t*exp(-0.01978137518117*t) - 4.4232103783057e-5*t*exp(-0.00261862481883003*t) + 0.353088638943962 - 0.040948309702862*exp(-0.01978137518117*t) - 0.256904644223366*exp(-0.00261862481883003*t) + 9.79262531842392*(-1.92496675874782e-5*t**2*exp(-0.01978137518117*t) + 0.000384475980748132*t*exp(-0.01978137518117*t))/t + 678.78539915058*(-5.11381158124681e-7*t**2*exp(-0.00261862481883003*t) - 8.69210524546246e-5*t*exp(-0.00261862481883003*t))/t]])

    -3.16429732897681e-6*t**2*exp(-0.01978137518117*t) - 9.62850956521852e-7*t**2*exp(-0.00261862481883003*t) + 0.00127477979729604*t*exp(-0.01978137518117*t) + 0.000692056975484248*t*exp(-0.00261862481883003*t) + 0.999999913184877 - 0.303017492737315*exp(-0.01978137518117*t) - 1.9010943672529*exp(-0.00261862481883003*t) + 9.79262531842392*(-0.000123197872999935*t**2*exp(-0.01978137518117*t) + 0.0283089791589526*t*exp(-0.01978137518117*t))/t + 9.79262531842392*(-1.92496675874782e-5*t**2*exp(-0.01978137518117*t) + 0.000384475980748132*t*exp(-0.01978137518117*t))/t + 678.78539915058*(-3.27283941199794e-6*t**2*exp(-0.00261862481883003*t) + 0.00144689074692566*t*exp(-0.00261862481883003*t))/t + 678.78539915058*(-5.11381158124681e-7*t**2*exp(-0.00261862481883003*t) - 8.69210524546246e-5*t*exp(-0.00261862481883003*t))/t

    The function takes:  15.92792010307312  Seconds.
    The probability of battery failure obtained is:  0.0864154183556407
    The mean time between failure for battery obtained is:  949.916941881880

### 3. Chip model Failure risk prediction

Finally, let use predict the failure of chip. Primary source of chip failure is its failure due to temperature. We estimate chip MTTF and failure probability based on dynamic temperature and the time. We use [arrhenius equation](https://en.wikipedia.org/wiki/Arrhenius_equation) to take into account the temperature effect.

For our, example, let us consider a battery with rference MTTF as 400, reference temperature as 30, actual temperature as 50, drone utilisation u as 1 (full utilisation), beta constant describing weibull failure distribution as 1 (constant random failure with time), and time as 100.

```python
P_Fail, MTTF= eval.Chip_MTTF_Model(400, 30, 50, 1, 1,100)

print("The probability of chip failure obtained is: ", P_Fail)

print("The mean time between failure for chip obtained is: ", MTTF)
```

    The probability of chip failure obtained is:  0.234962513468844
    The mean time between failure for chip obtained is:  373.370548822240

Now, let us obtain the drone failure probability by combining all three failure modes. (Assuming default values)

```python
P_Fail, MTTF = eval.Drone_Risk_Calc()

print("The probability of overall drone failure obtained is: ", P_Fail)

print("The mean time between failure for complete drone is: ", MTTF)
```

    The probability of overall drone failure obtained is:  0.568003741219376
    The mean time between failure for complete drone is:  373.370548822240

Hence, by obtaining the probability of failure for three main components of a drone, we conclude the prediction of proability of failure for a drone.


# Classical Walk

Classical walk refers to a random process in which one is randomly moved (with equal or different probabilities) to the left or right. This is an example of a one-dimensional random walk which moving in a straight line. This movement to the right or left can be simulated by throwing a coin. In such a way that each step is a throw. In this case, the person moves in discrete points and we do not have a continuous movement. Now we want to find the position of the person after n steps. To do this, we set the starting point to zero and call the particle position after n steps m. Suppose we have  steps to the right and   steps to the left. So, we will have:

\begin{equation}
    n_{R} +n_{l} =n \\ n_{R} -n_{l} =m. \\
    \stackrel{}{\longrightarrow}{\rm \; \; \; }n_{R} =\frac{n+m}{2} ,{\rm \; \; }n_{l} =\frac{n+m}{2}
\end{equation}

Suppose, the probability of moving to the right is p and the probability of moving to the left is q. The probability of the person being at m point after n step is as follows:

\begin{equation}
P_{n} (m)=\left(\begin{array}{l} {n} \\ {n_{R} } \end{array}\right)p^{n_{R} } q^{n-n_{R} }
\end{equation}

A specific example is that the probability of p and q are equal, in this case we have:

\begin{equation}
P_{n} (m)=\frac{n!}{n_{R} !(n-n_{R} )!} \left(\frac{1}{2} \right)^{n}
\end{equation}

Now the goal is calculate of X${}_{rms}$ $x_{r.m.s} =\sqrt{\overline{m^{2} }-\overline{m}^{2} } $in wich $\overline{m}=2\overline{n_{R} }-n$, so:

\begin{equation}
\overline{n_{R} }=\sum \frac{n!}{n_{R} !(n-n_{R} )!} n_{R} p^{n_{R} } q^{n-n_{R} }   \\ {{\rm \; \; \; \; }=p\frac{\partial }{\partial p} \left(\sum _{n_{R} =0}^{\infty }\left(\begin{array}{l} {n} \\ {n_{R} } \end{array}\right)p^{n_{R} } q^{n-n_{R} }  \right)} \\ {{\rm \; \; \; \; }=p\frac{\partial }{\partial p} (p+q)^{N} =np}
\end{equation}

It explicitly becomes $\overline{m}=0$. We obtain $\overline{m^{2} }$ with a similar approach, the first obtain $\overline{n_{R} {}^{2} }$:

\begin{equation}
\overline{n_{R} {}^{2} }=p\frac{\partial }{\partial p} \left(p\frac{\partial }{\partial p} (p+q)^{n} \right)=np+np^{2} (n-1)
\end{equation}

With straightforward calculations the relation  $\overline{m^{2} }=n$ would be obtained. Then we will see that

\begin{equation}
x_{r.m.s.} =n^{1/2},
\end{equation}

Table.1) The probability of position of the particle after t times.

Fig.1) The probability distribution of one-dimensional Classical walk for t = 72, 180, 450.

Fig.2) The probability distribution of a classical walk

```python

```


```python
from scipy import stats
from statistics import mean, stdev
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Times New Roman"
import sys
import os
if "../" not in sys.path:
  sys.path.append("../")
import os
os.chdir("..")
from envs.data_handler import DataHandler
import envs.data_utils as du
```

# Data Shift

* Step 1: Calculate the Standard Deviation
* Step 2: Create the ordering by the mean value for each <component,failure> group and sort them ascending by mean value, component name, failure name
* Step 3: Shift data

```python
dh = DataHandler(data_generation='Linear', take_component_id=True, transformation='raw')
data_new = du.shift_data(dh.data)
data_new.head()
```

      Optimal_Affected_Component_Uid
      Optimal_Failure
      raw

      0
      _SEwwu-cdEeet0YmmfbMwkw
      CF1
      138.2518

      1
      _SExXgOcdEeet0YmmfbMwkw
      CF2
      25.9559

      2
      _SEx_HucdEeet0YmmfbMwkw
      CF3
      61.6029

      3
      _SEymDucdEeet0YmmfbMwkw
      CF3
      50.0732

      4
      _SExYKucdEeet0YmmfbMwkw
      CF3
      25.9599

Perform T-Test:

```python
ttest = du.execute_ttest(data_new)
num_distinguishable_pairs = len(ttest[ttest['pvalue']<0.05])
total = len(ttest.index) - 2
print('{0} of the {1} <component, failure> combination pairs are statistical significant'.format(num_distinguishable_pairs, total))
```

    636 of the 912 <component, failure> combination pairs are statistical significant

# Parameterization

Evaluate different spread multiplication factors for an optimal shifting for each of the transformed datasets (e.g. cube/square root transfromation). We add to the series point of a <component,failure> group a shift value $s$, which is calculated as:

\begin{align}
s = f * (\sigma(x_{-1}) + \sigma(x)) + tieCount_{t}
\end{align}

Here, we evaluate different values for $f \in \mathbb{N}^{+}$ as the spread mulitplication factor which is mulitplied with the standard deviation of the previous <component,failure> combination $\sigma(x_{-1})$ and of the current one $\sigma(x)$. Since we shift the groups in the order of their ascending mean value, we add the $tieCount_{t}$ at for the comparison at time point $t$ which is the sum of the standard deviations of all <component,failure> pairs which have the same mean value.

```python
def parameterize(start, end, step):

    result = {}

    trans = ['raw', 'cube', 'sqt', 'log10', 'ln', 'log2']
    factor = range(start, end, step)

    for t in trans:
        X = []
        Y = []

        # initialize the result dictionary for each transformation
        result[t] = (0,0) # (spread_multiplication_factor, number of statistical significant pairs)

        # evaluate the different spread mulitplication factors
        for f in factor:

            # load the data
            dh = DataHandler(data_generation='Linear', take_component_id=True, transformation=t)
            shifted_data = du.shift_data(dh.data, spread_multiplication_factor=f)
            ttest = du.execute_ttest(shifted_data)
            num_significant_pairs = len(ttest[ttest['pvalue']<0.05])

            # save the results for plotting
            X.append(f) # factor
            Y.append(num_significant_pairs) # number of statistical significant pairs

            if num_significant_pairs > result[t][1]:
                # we were able to generate at least one more statistical significant pair
                result[t] = (f, num_significant_pairs)

            print("{0}: With the spread multiplication factor of {1} we have {2} statistical significant pairs.    ".format(t, f, num_significant_pairs), end="\r")

        plt.plot(X, Y, label=t)

    print(result)
    plt.xlabel("Spread Multiplication Factor")
    plt.ylabel("No. of statistical significant pairs")
    plt.legend()
    plt.savefig('data_analysis/03_plots/shifting_parameter_evaluation_' + str(step) + '.pdf')
    plt.show()
```

```python
parameterize(1, 300, 10)
```

```python
parameterize(1, 50, 1)
```

And then saving optimal shifted data to csv

```python
result = {'raw': (151, 667), 'cube': (241, 672), 'sqt': (231, 667), 'log10': (231, 672), 'ln': (231, 671), 'log2': (211, 666)}
dh = DataHandler(data_generation='Linear', take_component_id=True, transformation='raw')
optimal_shifted_data = dh.data[[dh.data.columns[0], dh.data.columns[1]]]

for t, value in result.items():
    dh = DataHandler(data_generation='Linear', take_component_id=True, transformation=t)
    data = du.shift_data(dh.data, spread_multiplication_factor=value[0])
    ttest = du.execute_ttest(data)
    print("Shifting {0} data with a factor of {1} results in {2} distinguishable pairs.".format(t, value[0], len(ttest[ttest['pvalue']<0.05])))
    optimal_shifted_data = optimal_shifted_data.merge(data[[data.columns[2]]], how='outer', left_index=True, right_index=True)

optimal_shifted_data.to_csv('data/prepared_data/LinearShifted_Id.csv')
print("Data saved.")
```

    Shifting raw data with a factor of 151 results in 667 distinguishable pairs.
    Shifting cube data with a factor of 241 results in 672 distinguishable pairs.
    Shifting sqt data with a factor of 231 results in 667 distinguishable pairs.
    Shifting log10 data with a factor of 231 results in 672 distinguishable pairs.
    Shifting ln data with a factor of 231 results in 671 distinguishable pairs.
    Shifting log2 data with a factor of 211 results in 666 distinguishable pairs.
    Data saved.

# Create datasets with only distinguishable <component,failue>

```python
trans = ['raw', 'cube', 'sqt', 'log10', 'ln', 'log2']

for t in trans:
    dh = DataHandler(data_generation='LinearShifted', take_component_id=True, transformation=t)
    ttest = du.execute_ttest(dh.data)
    component_failure_list = du.get_distinguishable_groups(ttest)
    filtered_data = du.filter_dataset(dh.data, component_failure_list)
    ttest_2 = du.execute_ttest(filtered_data)
    num_significant_pairs = len(ttest_2[ttest_2['pvalue']<0.05])
    print("{0}: {1}/{2} statistical significant pairs   ".format(t, num_significant_pairs, len(ttest_2)))
    filtered_data.to_csv('data/prepared_data/LinearShifted_Id_' + t + '_dist.csv')

print("Data saved.")
```

    raw: 728/730 statistical significant pairs
    cube: 727/737 statistical significant pairs
    sqt: 726/734 statistical significant pairs
    log10: 727/736 statistical significant pairs
    ln: 727/735 statistical significant pairs
    log2: 727/736 statistical significant pairs
    Data saved.

```python

```


# Wahlergebnisse
Wienwahl-Ergebnisse 2020 (Wahlbeteiligung 65.27%): SPÖ 41.62%, ÖVP 20.43%, Grüne 14.8%, NEOS 7.47%, FPÖ 7.11%, HC 3.27%, Sonstige 5.29%.

a) Erstellen Sie einen Plot mit 2 Subplots: einem Balkendiagramm und einem Kuchendiagramm zu den Wienwahl-Daten. Die Gruppen sowie Achsen/Titel sollen dabei sinnvoll beschriftet werden und die Farben akkurat gewählt werden (diese können in einem Vektor übergeben werden, `color = [...]` für das Balkendiagramm, `colors = [...]` für das Kuchendiagramm). Beim Kuchendiagramm soll 'Sonstige' abgespaltet dargestellt werden.

```python
import matplotlib.pyplot as plt

parties = ['SPOE', 'OEVP', 'Gruene', 'NEOS', 'FPOE', 'HC', 'Sonstige']
data = {
    'SPOE': 41.62,
    'OEVP': 20.43,
    'Gruene': 14.80,
    'NEOS': 7.47,
    'FPOE': 7.11,
    'HC': 3.27,
    'Sonstige': 5.29
}
colors = {
    'SPOE': '#E52320',
    'OEVP': '#63C3D0',
    'Gruene': '#92C254',
    'NEOS': '#E74087',
    'FPOE': '#0C65AF',
    'HC': 'white',
    'Sonstige': 'grey'
}

fig, (barax, pieax) = plt.subplots(1, 2)
barax.bar(
    x = range(len(data)),
    height = [data[party] for party in parties],
    align = 'center',
    color = [colors[party] for party in parties],
    edgecolor = 'k',
    tick_label = parties
)
barax.set_ylabel('Prozent Wählerstimmen')

for pos in ['top', 'right']:
    barax.spines[pos].set_visible(False)

pieax.pie(
    x = [data[party] for party in parties],
    explode = [0 for party in parties[:-1]] + [0.2],
    colors = [colors[party] for party in parties],
    labels = parties,
    wedgeprops = {'edgecolor': 'k'}
)

fig.set_figwidth(20)
fig.set_figheight(5)
fig.tight_layout()
```

b) Berechnen Sie wie das Wahlergebnis aussehen würde, wenn ein Zehntel der Nichtwähler*innen eine Kleinpartei aus 'Sonstige' gewählt hätte. Stellen Sie dies nun wieder mit einem Kuchendiagramm dar. Diesmal sollen auch die Prozentzahlen der Teile angegeben sein. Geben Sie nochmals das Kuchendiagramm aus a) aus und vergleichen/interpretieren Sie die Ergebnisse der anderen Parteien.

```python
population = 1e6
participation = 0.6527
voters = population * participation
nonvoters = population - voters
absolute_data = {k: v / 100 * voters for k, v in data.items()}
absolute_data['Sonstige'] += nonvoters * 0.1
new_data = {k: v/sum(absolute_data.values()) * 100 for k, v in absolute_data.items()}

fig, axs = plt.subplots(1, 2)
for ax, dat in zip(axs, [data, new_data]):
    ax.pie(
        x = [dat[party] for party in parties],
        explode = [0 for party in parties[:-1]] + [0.2],
        colors = [colors[party] for party in parties],
        labels = parties,
        autopct = '%.2f%%',
        wedgeprops = {'edgecolor': 'k'}
    )

fig.set_figwidth(20)
fig.set_figheight(10)
fig.tight_layout()
```

# Funktionswerte
Berechnen Sie die Funktionswerte von

$$
f(x) = x^2*\sin(x)*\cos(x)^3
$$

im Intervall [−2,2) für jeweils 5,10,15 und 100 äquidistante x-Werte. Erstellen Sie für jede Anzahl der x-Werte einen Subplot, wobei die Funktionswerte jeweils durch Punkte, die linear verbunden sind, dargestellt werden sollen. Jeder Subplot soll sich weiters durch unterschiedliche Symbole der Punkte und Farben unterscheiden. Geben Sie den gesamten Plot (mit den 4 Subplots) aus.

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

f = lambda x: x*x*np.sin(x)*np.cos(x)**3
fig, axs = plt.subplots(2, 2, sharex = 'col', sharey = 'row')
for n, marker, ax in zip(
    [5, 10, 15, 100],
    ['o', '^', 's', 'D'],
    axs.reshape(4)
):
    x = np.arange(-2, 2, step = 4 / n)
    ax.plot(
        x, f(x),
        marker = marker,
        markerfacecolor = 'white'
    )
    ax.set_xlim(-2.05, 2)

axs[0, 0].set_ylabel('f(x)')
axs[1, 0].set_ylabel('f(x)')
axs[1, 0].set_xlabel('x')
axs[1, 1].set_xlabel('x')
fig.set_figwidth(20)
fig.set_figheight(10)
fig.tight_layout()
```

# Histogramme
Plotten Sie Histogramme für $N=10^3,\ldots,10^6$ zufällige Punkte in einem Plot mit je 2 Subplots: Diese sollen 1) standard normalverteilt und 2) gleichverteilt sein. Die Anzahl der bins soll dabei sinnvoll in Abhängigkeit von $N$ gewählt werden.

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

fig, axs = plt.subplots(4, 2)
normal = lambda n: np.random.normal(size = n)
uniform = lambda n: np.random.uniform(low = -3, high = 3, size = n)
for n, rowaxs in zip(np.logspace(3, 6, 4), axs):
    for ax, randomfunc, title in zip(
        rowaxs,
        [normal, uniform],
        ['normal', 'uniform']
    ):
        bins = max(100, int(0.001 * n))
        ax.hist(
            randomfunc(int(n)),
            bins = bins
        )
        ax.set_title(title + f'; N = {int(n)}; bins = {bins}')

fig.set_figheight(20)
fig.set_figwidth(20)
fig.tight_layout()
```

# Eulerfunktion
Stellen Sie $e^{c*x}$ für $c=1,2,3,4$ und $x \in [0.1,10)$ in einem Plot mit logarithmischer y-Achsen Skalierung dar. Die Graphen sollen für jedes $c$ eine Legende haben und der Plot einen Titel. Implementieren Sie dann analog die Darstellung von $c*x$ ohne Achsenskalierung. Erklären Sie warum die Plots gleich aussehen.

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

def plot_function(func, c, title, logy = False):
    x = np.arange(0.1, 10, step = 0.1)
    fig, ax = plt.subplots()
    for c_i in c:
        ax.plot(
            func(x * c_i),
            label = f'c = {c_i}'
        )
        if logy:
            ax.set_yscale('log')

    ax.legend()
    ax.set_title(title)

    fig.set_figheight(5)
    fig.set_figwidth(10)
    fig.tight_layout()
    return fig, ax
```

```python
fig, ax = plot_function(
    np.exp,
    np.arange(1, 5, step = 1),
    'exp(c*x)',
    logy = True
)
```

```python
fig, ax = plot_function(
    lambda x: x,
    np.arange(1, 5, step = 1),
    'c*x',
    logy = False
)
```

# Funktionswerte mit `sympy` und `numpy`
Plotten Sie die Funktion

$$
f(x)=\cos(x)*\sin(x)
$$

für $x \in [−5,5]$ einmal mittels der Routinen im Modul `sympy` und einmal mittels `numpy` und `matplotlib`.

```python
import numpy as np
import matplotlib.pyplot as plt
from sympy.functions import cos, sin
from sympy.abc import x
from sympy import plot

p = plot(
    cos(x)*sin(x),
    xlim = (-5, 5),
)
```

```python
fig, ax = plt.subplots()
x = np.arange(-5, 5, step = 0.01)
ax.plot(
    x,
    np.cos(x)*np.sin(x)
)
ax.set_xlim(-5, 5)
```


# Boundary Value Problems

\begin{equation}
    \nabla^2 y = 0
\end{equation}

```python
import numpy as np
from scipy.integrate import solve_bvp as bvp
```

```python
def fun(X, Y):
    '''The Electrostatic Equation'''
    y, dy = Y

    return [
        dy,
        0
    ]
```

```python
def bc(Ya, Yb):
    '''Boundary conditions for the equation'''

    ya, dya = Ya
    yb, dyb = Yb

    return [
        ya,
        yb
    ]
```

```python
# Define the Mesh
x = np.linspace(0, 1, 51)
```

```python
# Define the Starting Points
Yg = np.zeros()
```


# _*Qiskit Aqua: Generating Random Variates*_

The latest version of this notebook is available on https://github.com/Qiskit/qiskit-iqx-tutorials.

***
### Contributors
Albert Akhriev[1], Jakub Marecek[1]

### Affiliation
- [1]IBMQ

## Introduction

While classical computers use only pseudo-random routines, quantum computers
can generate true random variates.
For example, the measurement of a quantum superposition is intrinsically random,
as suggested by Born's rule.
Consequently, some of the
best random-number generators are based on such quantum-mechanical effects.
Further, with a logarithmic amount of random bits, quantum computers can produce
linearly many more bits, which is known as
randomness expansion protocols.

In practical applications, one wishes to use random variates of well-known
distributions, rather than random bits.
In this notebook, we illustrate ways of generating random variates of several popular
distributions on IBM Q.

## Random Bits and the Bernoulli distribution

It is clear that there are many options for generating random bits (i.e., Bernoulli-distributed scalars, taking values either 0 or 1). Starting from a simple circuit such as a Hadamard gate followed by measurement, one can progress to vectors of Bernoulli-distributed elements. By addition of such random variates, we could get binomial distributions. By multiplication we could get geometric distributions, although perhaps leading to a circuit depth that may be impractical at the moment, though.

Let us start by importing the basic modules and creating a quantum circuit for generating random bits:

```python
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import sys, math, time
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

from qiskit import Aer
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, execute

# In this example we use 'qasm_simulator' backend.
glo_backend = Aer.get_backend("qasm_simulator")
```

In the next step we create a quantum circuit, which will be used for generation:

```python
# Number of qubits utilised simultaneously.
glo_num_qubits = 5

def create_circuit(num_target_qubits: int) -> QuantumCircuit:
    """
    Creates and returns quantum circuit for random variate generation.
    :param num_target_qubits: number of qubits to be used.
    :return: quantum curcuit.
    """
    assert isinstance(num_target_qubits, int) and num_target_qubits > 0
    q = QuantumRegister(num_target_qubits)
    c = ClassicalRegister(num_target_qubits)
    circuit = QuantumCircuit(q, c)
    circuit.h(q)
    circuit.barrier()
    circuit.measure(q, c)
    return circuit

# Create and plot generating quantum circuit.
circuit = create_circuit(glo_num_qubits)
#print(circuit)
circuit.draw(output='mpl')
```

## Uniformly-distributed scalars and vectors

It is clear that there are many options for approximating uniformly-distributed scalars by the choice of an integer from a finite range uniformly at random, e.g., by a binary-code construction from the Bernoulli-distributed vectors. In the following snippet, we generate random bits, which we then convert using the binary-code construction, up to the machine precision of a classical computer.

```python
def uniform_rand_float64(circuit: QuantumCircuit, num_target_qubits: int,
                         size: int, vmin: float, vmax: float) -> np.ndarray:
    """
    Generates a vector of random float64 values in the range [vmin, vmax].
    :param circuit: quantum circuit for random variate generation.
    :param num_target_qubits: number of qubits to be used.
    :param size: length of the vector.
    :param vmin: lower bound.
    :param vmax: upper bound.
    :return: vector of random values.
    """
    assert sys.maxsize == np.iinfo(np.int64).max    # sizeof(int) == 64 bits
    assert isinstance(size, int) and size > 0
    assert isinstance(vmin, float) and isinstance(vmax, float) and vmin <= vmax
    nbits = 7 * 8                                   # nbits > mantissa of float64
    bit_str_len = (nbits * size + num_target_qubits - 1) // num_target_qubits
    job = execute(circuit, glo_backend, shots=bit_str_len, memory=True)
    bit_str = ''.join(job.result().get_memory())
    scale = float(vmax - vmin) / float(2**nbits - 1)
    return np.array([vmin + scale * float(int(bit_str[i:i+nbits], 2))
                     for i in range(0, nbits * size, nbits)], dtype=np.float64)

def uniform_rand_int64(circuit: QuantumCircuit, num_target_qubits: int,
                       size: int, vmin: int, vmax: int) -> np.ndarray:
    """
    Generates a vector of random int64 values in the range [vmin, vmax].
    :param circuit: quantum circuit for random variate generation.
    :param num_target_qubits: number of qubits to be used.
    :param size: length of the vector.
    :param vmin: lower bound.
    :param vmax: upper bound.
    :return: vector of random values.
    """
    assert sys.maxsize == np.iinfo(np.int64).max        # sizeof(int) == 64 bits
    assert isinstance(size, int) and size > 0
    assert isinstance(vmin, int) and isinstance(vmax, int) and vmin <= vmax
    assert abs(vmin) <= 2**52 and abs(vmax) <= 2**52    # 52 == mantissa of float64
    return np.rint(uniform_rand_float64(circuit, num_target_qubits,
                                        size, float(vmin), float(vmax))).astype(np.int64)
```

### Uniform distribution over floating point numbers.
In this example we draw a random vector of floating-point values uniformly distributed within some arbitrary selected interval:

```python
# Draw a sample from uniform distribution.
start_time = time.time()
sample = uniform_rand_float64(circuit, glo_num_qubits, size=54321, vmin=-7.67, vmax=19.52)
sampling_time = time.time() - start_time

# Print out some details.
print("Uniform distribution over floating point numbers:")
print("  sample type:", type(sample), ", element type:", sample.dtype, ", shape:", sample.shape)
print("  sample min: {:.4f}, max: {:.4f}".format(np.amin(sample), np.amax(sample)))
print("  sampling time: {:.2f} secs".format(sampling_time))

# Plotting the distribution.
plt.hist(sample.ravel(),
         bins=min(int(np.ceil(np.sqrt(sample.size))), 100),
         density=True, facecolor='b', alpha=0.75)
plt.xlabel("value", size=12)
plt.ylabel("probability", size=12)
plt.title("Uniform distribution over float64 numbers in [{:.2f} ... {:.2f}]".format(
            np.amin(sample), np.amax(sample)), size=12)
plt.grid(True)
# plt.savefig("uniform_distrib_float.png", bbox_inches="tight")
plt.show()
```

### Uniform distribution over integers.
Our next example is similar to the previous one, but here we generate a random vector of integers:

```python
# Draw a sample from uniform distribution.
start_time = time.time()
sample = uniform_rand_int64(circuit, glo_num_qubits, size=54321, vmin=37, vmax=841)
sampling_time = time.time() - start_time

# Print out some details.
print("Uniform distribution over bounded integer numbers:")
print("  sample type:", type(sample), ", element type:", sample.dtype, ", shape:", sample.shape)
print("  sample min: {:d}, max: {:d}".format(np.amin(sample), np.amax(sample)))
print("  sampling time: {:.2f} secs".format(sampling_time))

# Plotting the distribution.
plt.hist(sample.ravel(),
         bins=min(int(np.ceil(np.sqrt(sample.size))), 100),
         density=True, facecolor='g', alpha=0.75)
plt.xlabel("value", size=12)
plt.ylabel("probability", size=12)
plt.title("Uniform distribution over int64 numbers in [{:d} ... {:d}]".format(
            np.amin(sample), np.amax(sample)), size=12)
plt.grid(True)
# plt.savefig("uniform_distrib_int.png", bbox_inches="tight")
plt.show()
```

## Normal distribution

To generate random variates with a standard normal distribution using two independent
samples $u_1, u_2$ of the uniform distribution on the unit interval [0, 1], one can
consider the Box-Muller transform to obtain a 2-vector:

\begin{align}
\begin{bmatrix}
%R\cos(\Theta )=
{\sqrt {-2\ln u_{1}}}\cos(2\pi u_{2}) \\
% R\sin(\Theta )=
{\sqrt {-2\ln u_{1}}}\sin(2\pi u_{2})
\end{bmatrix},
\end{align}

wherein we have two independent samples of the standard normal distribution.
In IBM Q, this is implemented as follows:

```python
def normal_rand_float64(circuit: QuantumCircuit, num_target_qubits: int,
                        size: int, mu: float, sigma: float) -> np.ndarray:
    """
    Draws a sample vector from the normal distribution given the mean and standard
    deviation, using the Box-Muller method.
    """
    TINY = np.sqrt(np.finfo(np.float64).tiny)
    assert isinstance(size, int) and size > 0
    rand_vec = np.zeros((size,), dtype=np.float64)

    # Generate array of uniformly distributed samples, factor 1.5 longer that
    # actually needed.
    n = (3 * size) // 2
    x = np.reshape(uniform_rand_float64(circuit, num_target_qubits,
                                        2*n, 0.0, 1.0), (-1, 2))

    x1 = 0.0                # first sample in a pair
    c = 0                   # counter
    for d in range(size):
        r2 = 2.0
        while r2 >= 1.0 or r2 < TINY:
            # Regenerate array of uniformly distributed samples upon shortage.
            if c >= n:
                c = 0
                n = max(size // 10, 1)
                x = np.reshape(uniform_rand_float64(circuit, num_target_qubits,
                                                    2*n, 0.0, 1.0), (-1, 2))

            x1 = 2.0 * x[c, 0] - 1.0        # first sample in a pair
            x2 = 2.0 * x[c, 1] - 1.0        # second sample in a pair
            r2 = x1 * x1 + x2 * x2
            c += 1

        f = np.sqrt(np.abs(-2.0 * np.log(r2) / r2))
        rand_vec[d] = f * x1

    return (rand_vec * sigma + mu)
```

The following example demonstrates how to draw a random vector of normally distributed variates:

```python
# Mean and standard deviation.
mu = 2.4
sigma = 5.1

# Draw a sample from the normal distribution.
start_time = time.time()
sample = normal_rand_float64(circuit, glo_num_qubits, size=4321, mu=mu, sigma=sigma)
sampling_time = time.time() - start_time

# Print out some details.
print("Normal distribution (mu={:.3f}, sigma={:.3f}):".format(mu, sigma))
print("  sample type:", type(sample), ", element type:", sample.dtype, ", shape:", sample.shape)
print("  sample min: {:.4f}, max: {:.4f}".format(np.amin(sample), np.amax(sample)))
print("  sampling time: {:.2f} secs".format(sampling_time))

# Plotting the distribution.
x = np.linspace(mu - 4.0 * sigma, mu + 4.0 * sigma, 1000)
analyt = np.exp(-0.5 * ((x - mu) / sigma)**2) / (sigma * math.sqrt(2.0 * math.pi))
plt.hist(sample.ravel(),
         bins=min(int(np.ceil(np.sqrt(sample.size))), 100),
         density=True, facecolor='r', alpha=0.75)
plt.plot(x, analyt, '-b', lw=1)
plt.xlabel("value", size=12)
plt.ylabel("probability", size=12)
plt.title("Normal distribution: empirical vs analytic", size=12)
plt.grid(True)
# plt.savefig("normal_distrib.png", bbox_inches="tight")
plt.show()
```

There is a substantial amount of further work needed to either certify the quality of the source of random numbers (cf. NIST SP 800-90B, Recommendation for the Entropy Sources Used for Random Bit Generation) or to use random variates within quantum algorithms (cf. uncertainty_models within Qiskit Aqua).

```python
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright
```

Version Information

Qiskit Software	Version

Qiskit	None

Terra	0.14.1

Aer	0.5.1

Ignis	0.3.0

Aqua	0.7.0

IBM Q Provider	0.7.0

System information

Python	3.7.4 (default, Aug 13 2019, 15:17:50)
[Clang 4.0.1 (tags/RELEASE_401/final)]

OS	Darwin

CPUs	2

Memory (Gb)	8.0

Mon May 04 09:22:20 2020 EDT

This code is a part of Qiskit

© Copyright IBM 2017, 2020.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
 of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.

```python

```