#  Diagrammatic Differentiation in Practice

Slides from the Oxford quantum group lunch talk on February 18th 2021.

## Implementing automatic differentiation in discopy

```python
from discopy import *
from discopy.quantum import *
from discopy.quantum.zx import Functor, Diagram

from sympy.abc import theta, phi, symbols
from matplotlib import pyplot as plt
```

**Derivatives are compositional**: if you have the derivative of each box, then you have the derivative of the diagrams made from those boxes.

```python
x = symbols('x')

f_array = [[1, 0], [0, x]]
g_array = [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,x*x]]
h_array = [[1, 1], [1,-1]]

f = QuantumGate('f(x)', n_qubits=1, data=x, array=f_array)
g = QuantumGate('g(x)', n_qubits=2, data=x, array=g_array)
h = QuantumGate('h', n_qubits=1, data=None, array=h_array)

d = {f: [[0, 0], [0, 1]], g: [[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,2*x]]}

circuit = (f @ h >> g)
circuit.draw(figsize=(3,3))
```

Given a commutative rig $\mathbb{S}$, a derivation $\partial: \mathbb{S} \rightarrow \mathbb{S}$ is defined as any operation that satisfies the linearity and product rules:

$$ \partial(f + g) = \partial f + \partial g \quad \text{and} \quad \partial(f \times g) = \partial f \times g + f \times \partial g $$

A related notation is dual numbers $D[\mathbb{S}]$, numbers of the form $a + b \epsilon$ for $a, b \in \mathbb{S}$ such that $\epsilon^2 = 0$. Sum and product are given by:

\begin{aligned}
\left(a+a^{\prime} \epsilon\right)+\left(b+b^{\prime} \epsilon\right) &=(a+b)+\left(a+b^{\prime}\right) \epsilon \\
\left(a+a^{\prime} \epsilon\right) \times\left(b+b^{\prime} \epsilon\right) &=(a \times b)+\left(a \times b^{\prime}+a^{\prime} \times b\right) \epsilon
\end{aligned}

Write $\pi_0, \pi_1 : D[\mathbb{S}] \to \mathbb{S}$ for the projections along the real and epsilon component resp.

**Lemma:** Every derivation defines a rig homomorphism $\mathbb{S} \to D[\mathbb{S}]$ with $f \mapsto f + \partial f$. The other way around, every a rig homomorphism $\partial : \mathbb{S} \to D[\mathbb{S}]$ with $\pi_0 \circ \partial = \text{id}_\mathbb{S}$ defines a derivation $\pi_1 \circ \partial : \mathbb{S} \to \mathbb{S}$.

For example, in the rig of smooth functions we can lift any
smooth function $f : \mathbb{R} \rightarrow \mathbb{R}$ to a function $f: D[\mathbb{R}] \rightarrow D[\mathbb{R}]$ over the dual numbers defined by:

$$f\left(a+a^{\prime} \epsilon\right)=f(a)+a^{\prime} \times(\partial f)(a) \epsilon$$

Then we can derive the following linearity, product and chain rules:

\begin{aligned}
(f+g)\left(a+a^{\prime} \epsilon\right) &=(f+g)(a)+a^{\prime} \times(\partial f+\partial g)(a) \epsilon \\
(f \times g)\left(a+a^{\prime} \epsilon\right) &=(f \times g)(a)+a^{\prime} \times(f \times \partial g+\partial f \times g)(a) \epsilon \\
(f \circ g)\left(a+a^{\prime} \epsilon\right) &=(f \circ g)(a)+a^{\prime} \times(\partial g \times \partial f \circ g)(a) \epsilon
\end{aligned}

```python
eps = QuantumGate('eps', n_qubits=0, array=[1e-10])

def DualFunctorAr(box):
    if x in box.free_symbols:
        d_box = QuantumGate(f'd{box.name}', n_qubits=len(box.cod), data=x, array=d[box])
        return box + d_box @ eps
    else:
        return box

dual_functor = CircuitFunctor(ob=lambda x: x,  ar=DualFunctorAr)
test = dual_functor(circuit)
test.draw(figsize=(15, 5))
```

```python
def project_in_eps(diagram):
    eps_terms = []
    for term in diagram.terms:
        if [box.name for box in term.boxes].count('eps') == 1:
            # remove epsilon
            remove_eps_functor = CircuitFunctor(ob=lambda x: x,
                                                ar=lambda x: Id(0) if x.name == 'eps' else x)
            eps_term = remove_eps_functor(term)
            eps_terms.append(eps_term)
    return Sum(eps_terms, cod=diagram.cod, dom=diagram.dom)

drawing.equation(circuit,
                  project_in_eps(test), figsize=(18, 3), symbol="--dual--> --project-->")
```

$$\tiny
(f(x) \otimes h) \circ g =
\left(\begin{pmatrix}1 & 0 \\ 0 & x\end{pmatrix} \otimes
\begin{pmatrix}1 & 1 \\ 1 & -1\end{pmatrix}\right) \circ
\begin{pmatrix}1&&&\\&1&&\\&&1&\\&&&x^2\end{pmatrix} =
\begin{pmatrix}1&1&&\\1&-1&&\\&&x&x^3\\&&x&-x^3\end{pmatrix}
$$

```python
project_in_eps(test).eval().array.reshape(4, 4)
```

    array([[0, 0, 0, 0],
           [0, 0, 0, 0],
           [0, 0, 1.00000000000000, 3.0*x**2],
           [0, 0, 1.00000000000000, -3.0*x**2]], dtype=object)

## Rules for diffentiating diagrams

```python
circuit = (Rx(x) @ Id(1)) >> CX >> (Rx(2*x) @ Id(1))
drawing.equation(circuit, circuit.grad(x, mixed=False), symbol='|--->', figsize=(15, 4))
```

## Rules for differentiating ZX diagrams

```python
XC = QuantumGate('XC', n_qubits=2, array=[[0,1,0,0], [1,0,0,0],[0,0,1,0], [0,0,0,1]])

def gate2zx_new(box):
    from discopy.quantum.zx import gate2zx, PRO, Z, X, Id
    if box == XC:
        return Id(1) @ Z(1, 2) >> X(2, 1) @ Id(1)
    else:
        return gate2zx(box)
circuit2zx = Functor(ob={qubit: PRO(1)}, ar=gate2zx_new, ob_factory=PRO, ar_factory=Diagram)
```

```python
circuit = (Rx(x) @ Id(1)) >> CX
drawing.equation(circuit, circuit2zx(circuit), circuit2zx(circuit).grad(x), figsize=(9, 2), symbol="---->")
```

## Doubling via the CPM construction

```python
swaps = Id(2) @ SWAP >> Id(1) @ SWAP @ Id(1)
doubled_circuit = swaps[::-1] >> Id(1) @ Rx(-x) @ Rx(x) @ Id(1) >> XC @ CX >> swaps
drawing.equation(doubled_circuit, circuit2zx(doubled_circuit), symbol="--- ZX -->", figsize=(15, 4))
```

Both `Circuit`s and `zx.Diagram`s can be differentiated.

```python
doubled_circuit.grad(x, mixed=False).draw(figsize=(12, 4))
```

```python
circuit2zx(doubled_circuit).grad(x).draw(figsize=(12, 3))
```

Differentiating a circuit as doubled diagram can give you an asymmetric, undoubled diagram due to the product rule.
These diagrams cannot be executed on quantum hardware.

## Differntiating Circuits
Similar to how we defined the derivatives of the ZX `Spider`s in terms of `Spiders`, the generators of ZX, we need to define the derivatives of the parameterised `QuantumGate`s in terms of `QuantumGate`s

From Schuld et al. the parameter-shift rule for `Rz` is given by
$\partial R_z(\theta) = \frac{1}{2} [R_z(\theta + \frac{\pi}{2}) - R_z(\theta - \frac{\pi}{2})]$

```python
drawing.equation(Rz(x).bubble(drawing_name="circ ∂"),
                 Rz(x).grad(x), figsize=(12, 4))
```

```python
drawing.equation(Rz(x).bubble(drawing_name="double").bubble(drawing_name="diag ∂"),
                 (Rz(-x) @ Rz(x)).grad(x, mixed=False), figsize=(12, 4))
```

Bear in mind that the previous equation is an equation on circuits, and the this one is an equation on linear maps.

## Checks for Diagrams
we checked that the diagrammatic derivatives equals the derivatives of sympy.

```python
import numpy as np
def _to_square_mat(m):
    m = np.asarray(m).flatten()
    return m.reshape(2 * (int(np.sqrt(len(m))), ))

def test_rot_grad():
    from sympy.abc import phi
    import sympy as sy
    for gate in (Rx, Ry, Rz, CU1, CRx, CRz):
        # Compare the grad discopy vs sympy
        op = gate(phi)
        d_op_sym = sy.Matrix(_to_square_mat(op.eval().array)).diff(phi)
        d_op_disco = sy.Matrix(
            _to_square_mat(op.grad(phi, mixed=False).eval().array))
        diff = sy.simplify(d_op_disco - d_op_sym).evalf()
        assert np.isclose(float(diff.norm()), 0.)

test_rot_grad()
```

## Checks for Circuits
we checked that the circuit derivative on the circuit is equal to the diagrammatic derivative of the doubled diagram.

```python
def test_rot_grad_mixed():
    from sympy.abc import symbols
    from sympy import Matrix

    z = symbols('z', real=True)
    random_values = [0., 1., 0.123, 0.321, 1.234]

    for gate in (Rx, Ry, Rz):
        cq_shape = (4, 4)
        v1 = Matrix((gate(z).eval().conjugate() @ gate(z).eval())
                    .array.reshape(*cq_shape)).diff(z)
        v2 = Matrix(gate(z).grad(z).eval(mixed=True).array.reshape(*cq_shape))

        for random_value in random_values:
            v1_sub = v1.subs(z, random_value).evalf()
            v2_sub = v2.subs(z, random_value).evalf()

            difference = (v1_sub - v2_sub).norm()
            assert np.isclose(float(difference), 0.)

test_rot_grad_mixed()
```

```python
circuit = Ket(0, 0) >> H @ Rx(phi) >> CX >> Bra(0, 1)
gradient = (circuit >> circuit[::-1]).grad(phi, mixed=False)
drawing.equation(circuit, gradient, symbol="|-->", figsize=(15, 4))
```

```python
x = np.arange(0, 1, 0.05)
y = np.array([circuit.subs(phi, i).eval(mixed=True).array.real for i in x])
dy = np.array([gradient.subs(phi, i).eval(mixed=False).array.real for i in x])
plt.subplot(2, 1, 1)
plt.plot(x, y)
plt.ylabel("Amplitude")
plt.subplot(2, 1, 2)
plt.plot(x, dy)
plt.ylabel("Gradient")
```

## Bonus: Finding the exponent of a gate using Stone's theorem

A one-parameter unitary group is a unitary matrix $U: n \rightarrow n$ in $\operatorname{Mat}_{\mathrm{R} \rightarrow \mathrm{C}}$ with $U(0)=\mathrm{id}_{n}$ and $U(t) U(s)=U(s+t)$ for all $s, t \in \mathbb{R}$. It is strongly continuous when $\lim _{t \rightarrow t_{0}} U(t)=U\left(t_{0}\right)$ for all $t_{0} \in \mathbb{R}$ A one-parameter diagram $d: x^{\otimes n} \rightarrow x^{\otimes n}$ is said to be a unitary group when its interpretation $[[d]]$ is.

**Stone's Theorem**: There is a one-to-one correspondance between strongly continuous one-parameter unitary groups and self-adjoint matrices. The bijection is given explicitly by
$$H \mapsto \exp (i t H)\quad \text{ and } \quad \mapsto-i(\partial U)(0)$$

## Future Work

* completing discopy codebase for QML
* solving differential equations
* Keeping derivatives of ZX in ZX, rather than sum of ZX
* Formulate diag diff for Boolean circuits


# Inferential Statistics Ia - Frequentism

## Learning objectives

Welcome to the first Frequentist inference mini-project! Over the course of working on this mini-project and the next frequentist mini-project, you'll learn the fundamental concepts associated with frequentist inference. The following list includes the topics you will become familiar with as you work through these two mini-projects:
* the _z_-statistic
* the _t_-statistic
* the difference and relationship between the two
* the Central Limit Theorem, including its assumptions and consequences
* how to estimate the population mean and standard deviation from a sample
* the concept of a sampling distribution of a test statistic, particularly for the mean
* how to combine these concepts to calculate a confidence interval

## Prerequisites

For working through this notebook, you are expected to have a very basic understanding of:
* what a random variable is
* what a probability density function (pdf) is
* what the cumulative density function is
* a high-level sense of what the Normal distribution

If these concepts are new to you, please take a few moments to Google these topics in order to get a sense of what they are and how you might use them.

While it's great if you have previous knowledge about sampling distributions, this assignment will introduce the concept and set you up to practice working using sampling distributions. This notebook was designed to bridge the gap between having a basic understanding of probability and random variables and being able to apply these concepts in Python. The second frequentist inference mini-project focuses on a real-world application of this type of inference to give you further practice using these concepts.

For this notebook, we will use data sampled from a known normal distribution. This allows us to compare our results with theoretical expectations.

## I An introduction to sampling from the Normal distribution

First, let's explore the ways we can generate the Normal distribution. While there's a fair amount of interest in [sklearn](https://scikit-learn.org/stable/) within the machine learning community, you're likely to have heard of [scipy](https://docs.scipy.org/doc/scipy-0.15.1/reference/index.html) if you're coming from the sciences. For this assignment, you'll use [scipy.stats](https://docs.scipy.org/doc/scipy-0.15.1/reference/tutorial/stats.html) to complete your work.

```python
from scipy.stats import norm
from scipy.stats import t
import numpy as np
import pandas as pd
from numpy.random import seed
import matplotlib.pyplot as plt
```

__Q:__ Call up the documentation for the `norm` function imported above. What is the second listed method?

```python
#help (norm)
#help (t)
```

__A:__ 1. help (norm)
2. help (t) - A Student's t continuous random variable.

__Q:__ Use the method that generates random variates to draw five samples from the standard normal distribution.

```python
#np.random.normal(size=5)
norm.rvs(size =5)
```

    array([ 2.44308467, -0.03199535, -0.6755559 , -0.86191072,  0.2104086 ])

__A:__ 1. np.random.normal(size=5)

```python
seed(47)
# draw five samples here
first_sample = norm.rvs(size =5) #np.random.normal(size=5)
first_sample
```

    array([-0.84800948,  1.30590636,  0.92420797,  0.6404118 , -1.05473698])

__Q:__ What is the mean of this sample? Is it exactly equal to the value you expected? Hint: the sample was drawn from the standard normal distribution.

```python
np.mean(first_sample)
```

    0.19355593334131074

__A:__ The mean of this sample is (0.19355593334131074). There are so many numbers with decimal points so it's not easy to calculate the mean without using tools.

```python
# Calculate and print the mean here, hint: use np.mean()
print(np.mean(first_sample))
```

    0.19355593334131074

__Q:__ What is the standard deviation of these numbers? Calculate this manually here as $\sqrt{\frac{\sum_i(x_i - \bar{x})^2}{n}}$. Hint: np.sqrt() and np.sum() will be useful here and remember that numpy supports [broadcasting](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html).

__A:__

```python
# Manually calculating the Standard Deviation
diff = first_sample - np.mean(first_sample)
diff_squared = diff **2
v = (np.sum(diff_squared))/len(first_sample)

np.sqrt(v)
```

    0.9606195639478641

```python
# Using the square root (np.sqrt()) of the variance (np.var()) to calculate the Standard Deviation
np.sqrt(np.var(first_sample))
```

    0.9606195639478641

Here we have calculated the actual standard deviation of a small (size 5) data set. But in this case, this small data set is actually a sample from our larger (infinite) population. In this case, the population is infinite because we could keep drawing our normal random variates until our computers die. In general, the sample mean we calculate will not be equal to the population mean (as we saw above). A consequence of this is that the sum of squares of the deviations from the _population_ mean will be bigger than the sum of squares of the deviations from the _sample_ mean. In other words, the sum of squares of the deviations from the _sample_ mean is too small to give an unbiased estimate of the _population_ variance. An example of this effect is given [here](https://en.wikipedia.org/wiki/Bessel%27s_correction#Source_of_bias). Scaling our estimate of the variance by the factor $n/(n-1)$ gives an unbiased estimator of the population variance. This factor is known as [Bessel's correction](https://en.wikipedia.org/wiki/Bessel%27s_correction). The consequence of this is that the $n$ in the denominator is replaced by $n-1$.

__Q:__ If all we had to go on was our five samples, what would be our best estimate of the population standard deviation? Use Bessel's correction ($n-1$ in the denominator), thus $\sqrt{\frac{\sum_i(x_i - \bar{x})^2}{n-1}}$.

__A:__

```python
var_b = (np.sum(diff_squared))/(len(first_sample)-1)

np.sqrt(var_b)
```

    1.0740053227518152

__Q:__ Now use numpy's std function to calculate the standard deviation of our random samples. Which of the above standard deviations did it return?

__A:__ The np.std() returns 0.9606195639478641

```python
# Calculating the Standard Deviation using the np.std()
np.std(first_sample)
```

    0.9606195639478641

__Q:__ Consult the documentation for np.std() to see how to apply the correction for estimating the population parameter and verify this produces the expected result.

__A:__

```python
help (np.std)
```

    Help on function std in module numpy:

    std(a, axis=None, dtype=None, out=None, ddof=0, keepdims=<no value>)
        Compute the standard deviation along the specified axis.

        Returns the standard deviation, a measure of the spread of a distribution,
        of the array elements. The standard deviation is computed for the
        flattened array by default, otherwise over the specified axis.

        Parameters
        ----------
        a : array_like
            Calculate the standard deviation of these values.
        axis : None or int or tuple of ints, optional
            Axis or axes along which the standard deviation is computed. The
            default is to compute the standard deviation of the flattened array.

            .. versionadded:: 1.7.0

            If this is a tuple of ints, a standard deviation is performed over
            multiple axes, instead of a single axis or all the axes as before.
        dtype : dtype, optional
            Type to use in computing the standard deviation. For arrays of
            integer type the default is float64, for arrays of float types it is
            the same as the array type.
        out : ndarray, optional
            Alternative output array in which to place the result. It must have
            the same shape as the expected output but the type (of the calculated
            values) will be cast if necessary.
        ddof : int, optional
            Means Delta Degrees of Freedom.  The divisor used in calculations
            is ``N - ddof``, where ``N`` represents the number of elements.
            By default `ddof` is zero.
        keepdims : bool, optional
            If this is set to True, the axes which are reduced are left
            in the result as dimensions with size one. With this option,
            the result will broadcast correctly against the input array.

            If the default value is passed, then `keepdims` will not be
            passed through to the `std` method of sub-classes of
            `ndarray`, however any non-default value will be.  If the
            sub-class' method does not implement `keepdims` any
            exceptions will be raised.

        Returns
        -------
        standard_deviation : ndarray, see dtype parameter above.
            If `out` is None, return a new array containing the standard deviation,
            otherwise return a reference to the output array.

        See Also
        --------
        var, mean, nanmean, nanstd, nanvar
        numpy.doc.ufuncs : Section "Output arguments"

        Notes
        -----
        The standard deviation is the square root of the average of the squared
        deviations from the mean, i.e., ``std = sqrt(mean(abs(x - x.mean())**2))``.

        The average squared deviation is normally calculated as
        ``x.sum() / N``, where ``N = len(x)``.  If, however, `ddof` is specified,
        the divisor ``N - ddof`` is used instead. In standard statistical
        practice, ``ddof=1`` provides an unbiased estimator of the variance
        of the infinite population. ``ddof=0`` provides a maximum likelihood
        estimate of the variance for normally distributed variables. The
        standard deviation computed in this function is the square root of
        the estimated variance, so even with ``ddof=1``, it will not be an
        unbiased estimate of the standard deviation per se.

        Note that, for complex numbers, `std` takes the absolute
        value before squaring, so that the result is always real and nonnegative.

        For floating-point input, the *std* is computed using the same
        precision the input has. Depending on the input data, this can cause
        the results to be inaccurate, especially for float32 (see example below).
        Specifying a higher-accuracy accumulator using the `dtype` keyword can
        alleviate this issue.

        Examples
        --------
        >>> a = np.array([[1, 2], [3, 4]])
        >>> np.std(a)
        1.1180339887498949
        >>> np.std(a, axis=0)
        array([ 1.,  1.])
        >>> np.std(a, axis=1)
        array([ 0.5,  0.5])

        In single precision, std() can be inaccurate:

        >>> a = np.zeros((2, 512*512), dtype=np.float32)
        >>> a[0, :] = 1.0
        >>> a[1, :] = 0.1
        >>> np.std(a)
        0.45000005

        Computing the standard deviation in float64 is more accurate:

        >>> np.std(a, dtype=np.float64)
        0.44999999925494177

```python
# ddof: Means Delta Degrees of Freedom. The divisor used in calculations is ``N - ddof``, where ``N`` represents the number of elements.
np.std(first_sample, ddof=1)
```

    1.0740053227518152

### Summary of section

In this section, you've been introduced to the scipy.stats package and used it to draw a small sample from the standard normal distribution. You've calculated the average (the mean) of this sample and seen that this is not exactly equal to the expected population parameter (which we know because we're generating the random variates from a specific, known distribution). You've been introduced to two ways of calculating the standard deviation; one uses $n$ in the denominator and the other uses $n-1$ (Bessel's correction). You've also seen which of these calculations np.std() performs by default and how to get it to generate the other.

You use $n$ as the denominator if you want to calculate the standard deviation of a sequence of numbers. You use $n-1$ if you are using this sequence of numbers to estimate the population parameter. This brings us to some terminology that can be a little confusing.

The population parameter is traditionally written as $\sigma$ and the sample statistic as $s$. Rather unhelpfully, $s$ is also called the sample standard deviation (using $n-1$) whereas the standard deviation of the sample uses $n$. That's right, we have the sample standard deviation and the standard deviation of the sample and they're not the same thing!

The sample standard deviation
\begin{equation}
s = \sqrt{\frac{\sum_i(x_i - \bar{x})^2}{n-1}} \approx \sigma,
\end{equation}
is our best (unbiased) estimate of the population parameter ($\sigma$).

If your data set _is_ your entire population, you simply want to calculate the population parameter, $\sigma$, via
\begin{equation}
\sigma = \sqrt{\frac{\sum_i(x_i - \bar{x})^2}{n}}
\end{equation}
as you have complete, full knowledge of your population. In other words, your sample _is_ your population. It's worth noting at this point if your sample is your population then you know absolutely everything about your population, there are no probabilities really to calculate and no inference to be done.

If, however, you have sampled _from_ your population, you only have partial knowledge of the state of your population and the standard deviation of your sample is not an unbiased estimate of the standard deviation of the population, in which case you seek to estimate that population parameter via the sample standard deviation, which uses the $n-1$ denominator.

You're now firmly in frequentist theory territory. Great work so far! Now let's dive deeper.

## II Sampling distributions

So far we've been dealing with the concept of taking a sample from a population to infer the population parameters. One statistic we calculated for a sample was the mean. As our samples will be expected to vary from one draw to another, so will our sample statistics. If we were to perform repeat draws of size $n$ and calculate the mean of each, we would expect to obtain a distribution of values. This is the sampling distribution of the mean. The Central Limit Theorem (CLT) tells us that such a distribution will approach a normal distribution as $n$ increases. For the sampling distribution of the mean, the standard deviation of this distribution is given by

\begin{equation}
\sigma_{mean} = \frac{\sigma}{\sqrt n}
\end{equation}

where $\sigma_{mean}$ is the standard deviation of the sampling distribution of the mean and $\sigma$ is the standard deviation of the population (the population parameter).

This is important because typically we are dealing with samples from populations and all we know about the population is what we see in the sample. From this sample, we want to make inferences about the population. We may do this, for example, by looking at the histogram of the values and by calculating the mean and standard deviation (as estimates of the population parameters), and so we are intrinsically interested in how these quantities vary across samples. In other words, now that we've taken one sample of size $n$ and made some claims about the general population, what if we were to take another sample of size $n$? Would we get the same result? Would we make the same claims about the general population? This brings us to a fundamental question: _when we make some inference about a population based on our sample, how confident can we be that we've got it 'right'?_

Let's give our normal distribution a little flavor. Also, for didactic purposes, the standard normal distribution, with its variance equal to its standard deviation of one, would not be a great illustration of a key point. Let us imagine we live in a town of 50000 people and we know the height of everyone in this town. We will have 50000 numbers that tell us everything about our population. We'll simulate these numbers now and put ourselves in one particular town, called 'town 47', where the population mean height is 172 cm and population standard deviation is 5 cm.

```python
seed(47)
pop_heights = norm.rvs(172, 5, size=50000)
```

```python
_ = plt.hist(pop_heights, bins=30)
_ = plt.xlabel('height (cm)')
_ = plt.ylabel('number of people')
_ = plt.title('Distribution of heights in entire town population')
_ = plt.axvline(172, color='r')
_ = plt.axvline(172+5, color='r', linestyle='--')
_ = plt.axvline(172-5, color='r', linestyle='--')
_ = plt.axvline(172+10, color='r', linestyle='-.')
_ = plt.axvline(172-10, color='r', linestyle='-.')
```

Now, 50000 people is rather a lot to chase after with a tape measure. If all you want to know is the average height of the townsfolk, then can you just go out and measure a sample to get a pretty good estimate of the average height?

```python
def townsfolk_sampler(n):
    return np.random.choice(pop_heights, n)
```

Let's say you go out one day and randomly sample 10 people to measure.

```python
seed(47)
daily_sample1 = townsfolk_sampler(10)
```

```python
_ = plt.hist(daily_sample1, bins=10)
_ = plt.xlabel('height (cm)')
_ = plt.ylabel('number of people')
_ = plt.title('Distribution of heights in sample size 10')
```

The sample distribution doesn't look much like what we know (but wouldn't know in real-life) the population distribution looks like. What do we get for the mean?

```python
np.mean(daily_sample1)
```

    173.47911444163503

And if we went out and repeated this experiment?

```python
daily_sample2 = townsfolk_sampler(10)
```

```python
np.mean(daily_sample2)
```

    173.7317666636263

__Q:__ Simulate performing this random trial every day for a year, calculating the mean of each daily sample of 10, and plot the resultant sampling distribution of the mean.

__A:__

```python
trial_10 = [np.mean(townsfolk_sampler(10)) for x in range(365)]
```

```python
seed(47)
# take your samples here
_ = plt.hist(trial_10, bins = 10)
_ = plt.xlabel('height (cm)')
_ = plt.ylabel('number of people')
_ = plt.title('Distribution of heights of daily sample of 10 for a year')
_ = plt.axvline(172, color='r')
_ = plt.axvline(172+5, color='r', linestyle='--')
_ = plt.axvline(172-5, color='r', linestyle='--')
_ = plt.axvline(172+10, color='r', linestyle='-.')
_ = plt.axvline(172-10, color='r', linestyle='-.')

print(len(trial_10))
print(np.mean(trial_10))
print(np.median(trial_10))
```

The above is the distribution of the means of samples of size 10 taken from our population. The Central Limit Theorem tells us the expected mean of this distribution will be equal to the population mean, and standard deviation will be $\sigma / \sqrt n$, which, in this case, should be approximately 1.58.

__Q:__ Verify the above results from the CLT.

__A:__ We get (1.5778333114768472) which is approximately 1.58

```python
np.std(trial_10,ddof = 1)
```

    1.578160835870796

```python

```

Remember, in this instance, we knew our population parameters, that the average height really is 172 cm and the standard deviation is 5 cm, and we see some of our daily estimates of the population mean were as low as around 168 and some as high as 176.

__Q:__ Repeat the above year's worth of samples but for a sample size of 50 (perhaps you had a bigger budget for conducting surveys that year!) Would you expect your distribution of sample means to be wider (more variable) or narrower (more consistent)? Compare your resultant summary statistics to those predicted by the CLT.

__A:__

```python
seed(47)
# calculate daily means from the larger sample size here

trial_50 = [np.mean(townsfolk_sampler(50)) for x in range(365)]
```

```python
_ = plt.hist(trial_50, bins = 50, histtype='stepfilled', alpha=0.8)
_ = plt.xlabel('height (cm)')
_ = plt.ylabel('number of people')
_ = plt.title('Distribution of heights of daily sample of 50 for a year')
_ = plt.axvline(172, color='r')
_ = plt.axvline(172+5, color='r', linestyle='--')
_ = plt.axvline(172-5, color='r', linestyle='--')
_ = plt.axvline(172+10, color='r', linestyle='-.')
_ = plt.axvline(172-10, color='r', linestyle='-.')

print(len(trial_50))
print(np.mean(trial_50))
print(np.median(trial_50))
print(np.std(trial_50,ddof = 1))
```

What we've seen so far, then, is that we can estimate population parameters from a sample from the population, and that samples have their own distributions. Furthermore, the larger the sample size, the narrower are those sampling distributions.

### III Normally testing times!

All of the above is well and good. We've been sampling from a population we know is normally distributed, we've come to understand when to use $n$ and when to use $n-1$ in the denominator to calculate the spread of a distribution, and we've  seen the Central Limit Theorem in action for a sampling distribution. All seems very well behaved in Frequentist land. But, well, why should we really care?

Remember, we rarely (if ever) actually know our population parameters but you still have to estimate them somehow. If we want to make inferences such as "is this observation unusual?" or "has my population mean changed?" then you need to have some idea of what the underlying distribution is so you can calculate relevant probabilities. In frequentist inference, you use the formulas above to deduce these population parameters. Take a moment in the next part of this assignment to refresh your understanding of how these probabilities work.

Recall some basic properties of the standard Normal distribution, such as about 68% of observations being within plus or minus 1 standard deviation of the mean.

__Q:__ Using this fact, calculate the probability of observing the value 1 or less in a single observation from the standard normal distribution. Hint: you may find it helpful to sketch the standard normal distribution (the familiar bell shape) and mark the number of standard deviations from the mean on the x-axis and shade the regions of the curve that contain certain percentages of the population.

__A:__

Calculating this probability involved calculating the area under the pdf from the value of 1 and below. To put it another way, we need to integrate the pdf. We could just add together the known areas of chunks (from -Inf to 0 and then 0 to $+\sigma$ in the example above. One way to do this is using look up tables (literally). Fortunately, scipy has this functionality built in with the cdf() function.

__Q:__ Use the cdf() function to answer the question above again and verify you get the same answer.

__A:__

```python
help(norm.cdf)
```

    Help on method cdf in module scipy.stats._distn_infrastructure:

    cdf(x, *args, **kwds) method of scipy.stats._continuous_distns.norm_gen instance
        Cumulative distribution function of the given RV.

        Parameters
        ----------
        x : array_like
            quantiles
        arg1, arg2, arg3,... : array_like
            The shape parameter(s) for the distribution (see docstring of the
            instance object for more information)
        loc : array_like, optional
            location parameter (default=0)
        scale : array_like, optional
            scale parameter (default=1)

        Returns
        -------
        cdf : ndarray
            Cumulative distribution function evaluated at `x`

```python
norm.cdf(1)
```

    0.8413447460685429

__Q:__ Using our knowledge of the population parameters for our townsfolk's heights, what is the probability of selecting one person at random and their height being 177 cm or less? Calculate this using both of the approaches given above.

__A:__

```python
norm.cdf(x=177, loc=172, scale=5)
```

    0.8413447460685429

__Q:__ Turning this question around. Let's say we randomly pick one person and measure their height and find they are 2.00 m tall? How surprised should we be at this result, given what we know about the population distribution? In other words, how likely would it be to obtain a value at least as extreme as this? Express this as a probability.

__A:__

```python
tall = norm.cdf(x=200, loc=172, scale=5) * 100
prob = 100 - tall
print(prob) # this produces a negative e number
print(prob * 100)
```

    1.0717590299691437e-06
    0.00010717590299691437

We could calculate this probability by virtue of knowing the population parameters. We were then able to use the known properties of the relevant normal distribution to calculate the probability of observing a value at least as extreme as our test value. We have essentially just performed a z-test (albeit without having prespecified a threshold for our "level of surprise")!

We're about to come to a pinch, though here. We've said a couple of times that we rarely, if ever, know the true population parameters; we have to estimate them from our sample and we cannot even begin to estimate the standard deviation from a single observation. This is very true and usually we have sample sizes larger than one. This means we can calculate the mean of the sample as our best estimate of the population mean and the standard deviation as our best estimate of the population standard deviation. In other words, we are now coming to deal with the sampling distributions we mentioned above as we are generally concerned with the properties of the sample means we obtain.

Above, we highlighted one result from the CLT, whereby the sampling distribution (of the mean) becomes narrower and narrower with the square root of the sample size. We remind ourselves that another result from the CLT is that _even if the underlying population distribution is not normal, the sampling distribution will tend to become normal with sufficiently large sample size_. This is the key driver for us 'requiring' a certain sample size, for example you may frequently see a minimum sample size of 30 stated in many places. In reality this is simply a rule of thumb; if the underlying distribution is approximately normal then your sampling distribution will already be pretty normal, but if the underlying distribution is heavily skewed then you'd want to increase your sample size.

__Q:__ Let's now start from the position of knowing nothing about the heights of people in our town.
* Use our favorite random seed of 47, to randomly sample the heights of 50 townsfolk
* Estimate the population mean using np.mean
* Estimate the population standard deviation using np.std (remember which denominator to use!)
* Calculate the (95%) [margin of error](https://www.statisticshowto.datasciencecentral.com/probability-and-statistics/hypothesis-testing/margin-of-error/#WhatMofE) (use the exact critial z value to 2 decimal places - [look this up](https://www.statisticshowto.datasciencecentral.com/probability-and-statistics/find-critical-values/) or use norm.ppf())
* Calculate the 95% Confidence Interval of the mean
* Does this interval include the true population mean?

__A:__

```python
seed(47)
# take your sample now
second_trial_50 = townsfolk_sampler(50)
```

```python
np.mean(second_trial_50)
```

    172.7815108576788

```python
# Using ddof = 1 because this is a sample not population
np.std(second_trial_50, ddof=1)
```

    4.195424364433547

```python
critical_val = (norm.ppf(0.95))
# Standard Error of the Mean (a.k.a. the standard deviation of the sampling distribution of the sample mean!
se = (np.std(second_trial_50, ddof=1)) / (np.sqrt(50))
moe = critical_val * se # Margin of Error
print(moe)
```

    0.9759288364989565

```python
lower = np.mean(second_trial_50) - moe
upper = np.mean(second_trial_50) + moe
print('lower:',lower,'\n','Upper: ',upper)
```

    lower: 171.80558202117984
     Upper:  173.75743969417775

__Q:__ Above we calculated the confidence interval using the critical z value. What is the problem with this? What requirement, or requirements, are we (strictly) failing?

__A:__ The dataset has an unknown population standard deviation. I think using a t-score instead of z-score would be better.

__Q:__ Calculate the 95% confidence interval for the mean using the _t_ distribution. Is this wider or narrower than that based on the normal distribution above? If you're unsure, you may find this [resource](https://www.statisticshowto.datasciencecentral.com/probability-and-statistics/confidence-interval/) useful. For calculating the critical value, remember how you could calculate this for the normal distribution using norm.ppf().

__A:__ Wider than the normal distribution since t-distribution curves are wider and bigger at the tails compared to normal distribution curves

```python
t = t.ppf(0.95, (50 -1))
t
```

    1.6765508919142629

```python
moe_t = t * se # Margin of error for t-score
```

```python
lower = np.mean(second_trial_50) - moe_t
upper = np.mean(second_trial_50) + moe_t
print('lower:',lower,'\n','Upper: ',upper)
```

    lower: 171.78677531740482
     Upper:  173.77624639795278

This is slightly wider than the previous confidence interval. This reflects the greater uncertainty given that we are estimating population parameters from a sample.

## Learning outcomes

Having completed this project notebook, you now have hands-on experience:
* sampling and calculating probabilities from a normal distribution
* the correct way to estimate the standard deviation of a population (the population parameter) from a sample
* what a sampling distribution is and how the Central Limit Theorem applies
* how to calculate critical values and confidence intervals


# Reproducing and extending diffeq_pendulum.cpp - Pass 3

Pass 3: Fix up the 'interactive_output' version and add some more features.

* Created 26-Dec-2018 by Dick Furnstahl (furnstahl.1@osu.edu)
* Last revised 26-Dec-2018 by Dick Furnstahl (furnstahl.1@osu.edu).

```python
%matplotlib inline
```

```python
import numpy as np
from scipy.integrate import ode, odeint

import matplotlib.pyplot as plt
```

## Pendulum code

```python
class Pendulum():
    """
    Pendulum class implements the parameters and differential equation from
     the diffeq_pendulum.cpp code.

    Parameters
    ----------
    omega0 : float
        natural frequency of the pendulum (\sqrt{g/l} where l is the pendulum length)
    alpha : float
        coefficient of friction
    f_ext : float
        amplitude of external force
    omega_ext : float
        frequency of external force
    phi_ext : float
        phase angle for external force

    Methods
    -------
    dy_dt(y, t)
        Returns the right side of the differential equation in vector y, given time t
        and the corresponding value of y.
    """
    def __init__(self,
                 omega0=1.,
                 alpha=0.2,
                 f_ext=0.2,
                 omega_ext=0.689,
                 phi_ext=0.
                ):
        self.omega0 = omega0
        self.alpha = alpha
        self.f_ext = f_ext
        self.omega_ext = omega_ext
        self.phi_ext = phi_ext

    def dy_dt(self, y, t):
        """
        This function returns the right-hand side of the diffeq: [dtheta/dt d^2theta/dt^2]

        Parameters
        ----------
        y : float
            A 2-component vector with y[0] = theta(t) and y[1] = dtheta/dt
        t : float
            time

        Returns
        -------

        """
        F_ext = self.f_ext * np.cos(self.omega_ext*t + self.phi_ext)
        return [y[1], -self.omega0**2*np.sin(y[0]) - self.alpha*y[1] + F_ext]
```

```python
def plot_y_vs_x(x, y, axis_labels=None, label=None, title=None, ax=None):
    """
    Return a figure axis with a plot of y vs. x.
    """
    if ax is None:
        ax = plt.gca()

    ax.plot(x, y, label=label)
    if label is not None:
        ax.legend()
    if title is not None:
        ax.set_title(title)
    if axis_labels is not None:
        ax.set_xlabel(axis_labels[0])
        ax.set_ylabel(axis_labels[1])

    return ax

```

## Interface using ipywidgets with interactive_output

```python
# Import the widgets we will use (add more as needed!)
import ipywidgets as widgets
from ipywidgets import HBox, VBox, Layout, Tab, Label, Checkbox
from ipywidgets import FloatSlider, IntSlider, Play, Dropdown, HTMLMath

from IPython.display import display
from time import sleep
```

```python
# theta = []
# theta_dot = []
# t_pts = []
# p1 = []

# This function generates the main output, which is a grid of plots
def pendulum_plots(theta_vs_time_plot=True, theta_dot_vs_time_plot=True,
                   phase_space_plot=True, omega0=1.,
                   alpha=0.2, f_ext=0.2, omega_ext=0.689, phi_ext=0.,
                   theta0=0.8, theta_dot0=0.0,
                   t_start=0, t_end=100, delta_t=0.1, plot_start=0,
                   animate_flag=False, t_index=0,
                   font_size=18):
    """
    Create plots for interactive_output according to the inputs.

    Based on generating a Pendulum instance and the requested graphs.

    Notes
    -----
        1. We generate a new Pendulum instance every time *and* solved
            the ODE every time, even if the only change is to parameters
            like t_start and t_end.  Should we care or is this just so
            cheap to recalculate that it doesn't matter?
            How could we structure this differently?
    """
    # Test of not recalculating
    #global theta, theta_dot, t_pts, p1

    # add delta_t o it goes at least to t_end (probably should use linspace)
    #if not animate_flag:
    t_pts = np.arange(t_start, t_end+delta_t, delta_t)

    # Instantiate a pendulum with the passed (or default) values of the
    #  natural frequency omega0, damping alpha, driving amplitude, frequency,
    #  and phase (f_ext, omega_ext, phi_ext).
    #  Should we delete p1 at some point?  Is there a memory issue?
    #if not animate_flag:
    p1 = Pendulum(omega0=omega0, alpha=alpha, f_ext=f_ext, omega_ext=omega_ext,
                  phi_ext=phi_ext)
    y0 = [theta0, theta_dot0]  # initial conditions for the pendulum ODE

    # ODE solver parameters
    abserr = 1.0e-8
    relerr = 1.0e-6

    # For now we solve with odeint; give more options in the future.
    #if not animate_flag:
    theta, theta_dot = odeint(p1.dy_dt, y0, t_pts,
                              atol=abserr, rtol=relerr).T

    # Update the common font size
    plt.rcParams.update({'font.size': font_size})

    # Labels for individual plot axes
    theta_vs_time_labels = (r'$t$', r'$\theta$')
    theta_dot_vs_time_labels = (r'$t$', r'$d\theta/dt$')
    phase_space_labels = (r'$\theta$', r'$d\theta/dt$')

    # Figure out how many rows and columns [one row for now]
    plot_flags = [theta_vs_time_plot, theta_dot_vs_time_plot, phase_space_plot]
    plot_num = plot_flags.count(True)
    plot_rows = 1
    figsize_rows = plot_rows*6
    plot_cols = plot_num
    figsize_cols = min(plot_cols*8, 16)  # at most 16

    # Make the plot!
    fig, axes = plt.subplots(plot_rows, plot_cols,
                             figsize=(figsize_cols,figsize_rows))
    axes = np.atleast_1d(axes)  # make it always a 1d array, even if only 1

    start_index = (np.fabs(t_pts-plot_start)).argmin() # finds nearest index

    next_axes = 0
    if theta_vs_time_plot:
        plot_y_vs_x(t_pts, theta, axis_labels=theta_vs_time_labels,
                    label='pendulum', title=r'$\theta$ vs. time',
                    ax=axes[next_axes])
        # add a line where the phase space plot starts
        axes[next_axes].axvline(t_pts[start_index], lw=3, color='red')
        if animate_flag:
            axes[next_axes].plot(t_pts[t_index], theta[t_index], 'ro')
        next_axes += 1

    if theta_dot_vs_time_plot:
        plot_y_vs_x(t_pts, theta_dot, axis_labels=theta_dot_vs_time_labels,
                    label='pendulum', title=r'$d\theta/dt$ vs. time',
                    ax=axes[next_axes])
        # add a line where the phase space plot starts
        axes[next_axes].axvline(t_pts[start_index], lw=3, color='red')
        if animate_flag:
            axes[next_axes].plot(t_pts[t_index], theta_dot[t_index], 'ro')
        next_axes += 1

    if phase_space_plot:
        plot_y_vs_x(theta[start_index:-1], theta_dot[start_index:-1],
                    axis_labels=phase_space_labels, title='Phase space',
                    ax=axes[next_axes])
        if animate_flag:
            axes[next_axes].plot(theta[t_index], theta_dot[t_index], 'ro')
        next_axes += 1

    fig.tight_layout()

    return fig, axes

```

```python
# Widgets for the various inputs.
#   For any widget, we can set continuous_update=False if we don't want the
#    plots to shift until the selection is finished (particularly relevant for
#    sliders).

# Widgets for the plot choice (plus a label out front)
plot_choice_w = Label(value='Which plots: ',layout=Layout(width='100px'))
def plot_choice_widget(on=True, plot_description=None):
    return Checkbox(value=on, description=plot_description,
                  disabled=False, indent=False, layout=Layout(width='150px'))
theta_vs_time_plot_w = plot_choice_widget(True, r'$\theta$ vs. time')
theta_dot_vs_time_plot_w = plot_choice_widget(False, r'$d\theta/dt$ vs. time')
phase_space_plot_w = plot_choice_widget(True, 'phase space')

# Widgets for the pendulum parameters
def float_widget(value, min, max, step, description, format):
    slider_border = Layout(border='solid 1.0px')
    return FloatSlider(value=value,min=min,max=max,step=step,disabled=False,
                       description=description,continuous_update=False,
                       orientation='horizontal',layout=slider_border,
                       readout=True,readout_format=format)
omega0_w = float_widget(value=1.0, min=0.0, max=10., step=0.1,
                        description=r'natural $\omega_0$:', format='.1f')
alpha_w = float_widget(value=0.1, min=0.0, max=2., step=0.1,
                       description=r'damping $\alpha$:', format='.1f')
f_ext_w = float_widget(value=0.2, min=0.0, max=2., step=0.05,
                       description=r'strength $f_{\rm ext}$:', format='.2f')
omega_ext_w = float_widget(value=0.689,min=0.0,max=3.,step=0.1,
                       description=r'freq. $\omega_{\rm ext}$:', format='.2f')
phi_ext_w = float_widget(value=0.0, min=0, max=2.*np.pi, step=0.1,
                         description=r'phase $\phi_{\rm ext}$:', format='.1f')

# Widgets for the initial conditions
theta0_w = float_widget(value=0.8, min=0., max=2.*np.pi, step=0.1,
                        description=r'$\theta_0$:', format='.1f')
theta_dot0_w = float_widget(value=0.0, min=-10., max=10., step=0.1,
                            description=r'$(d\theta/dt)_0$:', format='.1f')

# Widgets for the plotting parameters
t_start_w = float_widget(value=0., min=0., max=100., step=10.,
                         description='t start:', format='.1f')
t_end_w = float_widget(value=100., min=0., max=500., step=10.,
                       description='t end:', format='.1f')
delta_t_w = float_widget(value=0.1, min=0.01, max=0.2, step=0.01,
                         description='delta t:', format='.2f')
plot_start_w = float_widget(value=0., min=0., max=300., step=5.,
                            description='start plotting:', format='.1f')

# Widgets for the animating
animate_flag_w = Checkbox(value=False, description='Animate',
                  disabled=False, indent=False, layout=Layout(width='100px'))
t_index_w = Play(interval=100, value=0, min=0, max=1000, step=1,
                      disabled=True, continuous_update=True,
                      description='press play',
                      orientation='horizontal')
# t_index_w = IntSlider(value=0, min=0, max=1000, step=10, disabled=False,
#                       description='time:', continuous_update=True,
#                       orientation='horizontal', readout=True)

# Widgets for the styling parameters
font_size_w = Dropdown(options=['12', '16', '18', '20', '24'], value='18',
                       description='Font size:',disabled=False,
                       continuous_update=False,layout=Layout(width='140px'))

# Text for the help section
overview_text = \
   r"""<p>Here we explore the dynamics of a damped, driven pendulum. There is help
          available under the other tabs.</p>
          <ul>
            <li>Physics tab: find out about the equations being solved.
            <li>Plotting tab: adjust what is plotted and over what intervals.
                Also about the algorithm used.
            <li>Styling tab: change how the plots look.
            <li>Animate tab: look at animated plots of the time dependence.
          </ul>
    """
physics_text = \
   r"""<p>We have in mind a physical pendulum with moment of inertia $I$ for which
       the dependent variable is the angle $\theta$.  It is subject to gravity,
       a damping force proportional to $d\theta/dt \equiv \dot\theta$, and an external
       periodic driving torque.  Newton's second law for the torque can be rearranged to
       give the differential equation:
       \begin{align}
         \qquad
         \frac{d^2\theta}{dt^2} + \alpha \frac{d\theta}{dt}
           + \omega_0^2 \sin\theta = f_{\rm ext}\cos(\omega_{\rm ext}t)
           \; .
       \end{align}
       The presence of $\sin\theta$ rather than $\theta$ makes this problem
       inherently nonlinear.</p>

       <p>To study this system, we'll look at plots such as $\theta$ versus $t$
       and the phase-space plot $\dot\theta$ vs. $\theta$, given initial
       conditions $\theta_0 \equiv \theta(t=0)$ and
       $\dot\theta_0 \equiv \dot\theta(t=0)$.
       Under what conditions will these look like the results for a
       simple harmonic oscillator?
       Try to decide what range of $\theta_0$ gives harmonic behavior,
       given $\dot\theta_0 = 0$.</p>

       <p>Let's think about <em>chaos</em>.  Consider the following brief
       discussion as a teaser.  Here are some characteristics of chaos:
       <ul>
         <li> The system does not repeat past behavior (cf.\ periodic
         behavior).</li>
         <li> An uncertainty (or variation) in initial conditions grows
         <em>exponentially</em> (rather than linearly) in time.  The consequence
         is that the system is deterministic (as opposed to having a random
         component) but not predictable, since there is a finite precision in
         specifying the initial conditions (e.g., think about round-off
         error).</li>
         <li> The system has a distributed power spectrum.</li>
       </ul>
       The following are necessary conditions for chaotic behavior:
       <ol type="a">
         <li> The system has at least \emph{three} independent dynamical
         variables.  That is, the system can be written as
         \begin{align}
            \qquad\frac{dy_0}{dt} &= F_0(y_0,\cdots,y_n) \ , \\
            \qquad\frac{dy_1}{dt} &= F_0(y_0,\cdots,y_n) \ , \\
                 &  \quad \vdots \\
            \qquad\frac{dy_n}{dt} &= F_0(y_0,\cdots,y_n) \ , \\
         \end{align}
         with $n \geq 3$.</li>
         <li>
           The equations of motion contain nonlinear term(s) that couple
           several of the variables.</li>
       </ol>
       You might not think that our pendulum example qualifies, since there
       only seem to be two independent dynamical variables, $\theta$ and
       $\omega \equiv \dot\theta$.  We find the third by introducing $\phi$
       as
       \begin{align}
         \qquad\phi &= \omega_{\rm ext} t \quad \Longrightarrow \quad
           \frac{d\phi}{dt} = \omega_{\rm ext} \ .
       \end{align}
       Thus, the three necessary equations are
       \begin{align}
         \qquad\frac{d\theta}{dt} &= \omega \ ,\\
         \qquad\frac{d\omega}{dt} &= -\alpha\omega - \omega_0^2 \sin\theta
            - f_{\rm ext}\cos\phi \ ,\\
         \qquad\frac{d\phi}{dt} &= \omega_{\rm ext} \ .
       \end{align}
       Now we satisfy a) with $\theta$, $\omega$, and $\phi$, and we satisfy
       b) since the $\sin\theta$ and $\cos\phi$ terms couple the equations
       nonlinearly.  So we should be able to find chaos!
    """

plotting_text = \
    """
    <p>Notes on plotting:</p>
    <ul>
      <li>The <tt>plot_start</tt> variable sets when the phase space plot
       starts plotting.  This enables you to remove the transient behavior
       at early ties.
    </ul>
    """

styling_text = \
    """
    <p>For now you can only change the font size.</p>
    """

# Widgets for the help section
help_max_height = '500px'
help_overview_w = HTMLMath(value=overview_text)
help_physics_w = HTMLMath(value=physics_text)
help_plotting_w = HTMLMath(value=plotting_text)
help_styling_w = HTMLMath(value=styling_text)
help_w = Tab(children=[help_overview_w, help_physics_w,
                       help_plotting_w, help_styling_w],
             layout=Layout(width='95%', max_height=help_max_height))
help_w.set_title(0, 'Overview')
help_w.set_title(1, 'Physics')
help_w.set_title(2, 'Plotting')
help_w.set_title(3, 'Styling')
help_w.set_title(4, 'Animate')

############## Begin: Explicit callback functions #######################

# Make sure that t_end is at least t_start + 50
def update_t_end(*args):
    if t_end_w.value < t_start_w.value:
        t_end_w.value = t_start_w.value + 50
t_end_w.observe(update_t_end, 'value')
t_start_w.observe(update_t_end, 'value')

# Make sure that plot_start is at least t_start and less than t_end
def update_plot_start(*args):
    if plot_start_w.value < t_start_w.value:
        plot_start_w.value = t_start_w.value
    if plot_start_w.value > t_end_w.value:
        plot_start_w.value = t_end_w.value
plot_start_w.observe(update_plot_start, 'value')
t_start_w.observe(update_plot_start, 'value')
t_end_w.observe(update_plot_start, 'value')

# Only turn on play widget when animate is selected
def turn_on_play_widget(*args):
    if animate_flag_w.value is True:
        t_index_w.disabled = False
        t_pts = np.arange(t_start_w.value, t_end_w.value+delta_t_w.value,
                          delta_t_w.value)
        t_index_w.min = (np.fabs(t_pts-plot_start_w.value)).argmin()
        t_index_w.max = len(t_pts) - 1
    else:
        t_index_w.disabled = True
animate_flag_w.observe(turn_on_play_widget, 'value')

############## End: Explicit callback functions #######################

# Set up the interactive_output widget
plot_out = widgets.interactive_output(pendulum_plots,
                          dict(
                          theta_vs_time_plot=theta_vs_time_plot_w,
                          theta_dot_vs_time_plot=theta_dot_vs_time_plot_w,
                          phase_space_plot=phase_space_plot_w,
                          omega0=omega0_w,
                          alpha=alpha_w,
                          f_ext=f_ext_w,
                          omega_ext=omega_ext_w,
                          phi_ext=phi_ext_w,
                          theta0=theta0_w,
                          theta_dot0=theta_dot0_w,
                          t_start=t_start_w,
                          t_end=t_end_w,
                          delta_t=delta_t_w,
                          plot_start=plot_start_w,
                          animate_flag=animate_flag_w,
                          t_index=t_index_w,
                          font_size=font_size_w)
                       )

# Now do some manual layout, where we can put the plot anywhere using plot_out
hbox1 = HBox([plot_choice_w, theta_vs_time_plot_w, theta_dot_vs_time_plot_w,
              phase_space_plot_w]) #  choice of what plots to show
hbox2 = HBox([omega0_w, f_ext_w, omega_ext_w, phi_ext_w])  # external driving parameters
hbox3 = HBox([theta0_w, theta_dot0_w, alpha_w]) # initial conditions and damping
hbox4 = HBox([t_start_w, t_end_w, delta_t_w, plot_start_w]) # time and plot ranges
hbox5 = HBox([font_size_w]) # font size
hbox6 = HBox([animate_flag_w, t_index_w])  # animate
hbox7 = HBox([help_w])  # help tabs

# We'll set up Tabs to organize the controls.  The Tab contents are declared
#  as tab0, tab1, ... (probably should make this a list?) and the overall Tab
#  is called tab (so its children are tab0, tab1, ...).
tab_height = '70px'  # Fixed minimum height for all tabs. Specify another way?
tab0 = VBox([hbox2, hbox3], layout=Layout(min_height=tab_height))
tab1 = VBox([hbox1, hbox4], layout=Layout(min_height=tab_height))
tab2 = VBox([hbox5], layout=Layout(min_height=tab_height))
tab3 = VBox([hbox6], layout=Layout(min_height=tab_height))
tab4 = VBox([hbox7], layout=Layout(min_height=tab_height))

tab = Tab(children=[tab0, tab1, tab2, tab3, tab4])
tab.set_title(0, 'Physics')
tab.set_title(1, 'Plotting')
tab.set_title(2, 'Styling')
tab.set_title(3, 'Animate')
tab.set_title(4, 'Help')

# Release the Kraken!
vbox2 = VBox([tab, plot_out])
display(vbox2)
```

    VBox(children=(Tab(children=(VBox(children=(HBox(children=(FloatSlider(value=1.0, continuous_update=False, des…

```python
# with plot_out:
#     print('Which cell?')
#     plt.plot([0],[0],'ro')
```

```python
# plot_out.clear_output()  # this will eliminate the plots; they reappear with
#                          #  changes in the widgets
```

```python
# plot_out.outputs;   # tuple of outputs (only seems to have one element)
```

```python
# plot_out.keys
```

```python
# plot_out.__dir__()
```

```python

```


## IBM Quantum Challenge Fall 2021
# Challenge 1: Optimizing your portfolio with quantum computers

We recommend that you switch to **light** workspace theme under the Account menu in the upper right corner for optimal experience.

## Introduction: What is portfolio optimization?
Portfolio optimization is a crucial process for anyone who wants to maximize returns from their investments.
Investments are usually a collection of so-called assets (stock, credits, bonds, derivatives, calls, puts, etc..) and this collection of assets is called a **portfolio**.

The goal of portfolio optimization is to minimize risks (financial loss) and maximize returns (financial gain). But this process is not as simple as it may seem. Gaining high returns with little risk is indeed too good to be true. Risks and returns usually have a trade-off relationship which makes optmizing your portfolio a little more complicated. As Dr. Harry Markowitz states in his Moderbn Portfolio Theory he created in 1952,  "risk is an inherrent part of higher reward."

**Modern Portfolio Theory (MPT)**

An investment theory based on the idea that investors are risk-averse, meaning that when given two portfolios that offer the same expected return they will prefer the less risky one. Investors can construct portfolios to maximize expected return based on a given level of market risk, emphasizing that risk is an inherent part of higher reward. It is one of the most important and influential economic theories dealing with finance and investment. Dr. Harry Markowitz created the modern portfolio theory (MPT) in 1952 and won the Nobel Prize in Economic Sciences in 1990 for it.

**Reference:** [Modern Portfolio Theory](https://en.wikipedia.org/wiki/Modern_portfolio_theory)

## Challenge

**Goal**

Portfolio optimization is a crucial process for anyone who wants to maximize returns from their investments. In this first challenge, you will learn some of the basic theory behind portfolio optimization and how to formulate the problem so it can be solved by quantum computers. During the process, you will learn about Qiskit's Finance application class and methods to solve the problem efficiently.

1. **Challenge 1a**: Learn how to use the PortfolioOptimization() method in Qiskit's Finance module to convert the portfolio optimization into a quadratic program.

2. **Challenge 1b**: Implement VQE to solve a four-stock portfolio optimization problem based on the instance created in challenge 1a.

3. **Challenge 1c**: Solve the same problem using QAOA with three budgets and double weights for any of the assets in your portfolio.

Before you begin, we recommend watching the [**Qiskit Finance Demo Session with Julien Gacon**](https://youtu.be/UtMVoGXlz04?t=2022) and check out the corresponding [**demo notebook**](https://github.com/qiskit-community/qiskit-application-modules-demo-sessions/tree/main/qiskit-finance) to learn about Qiskit's Finance module and its appications in portfolio optimization.

## 1. Finding the efficient frontier
The Modern portfolio theory (MPT) serves as a general framework to determine an ideal portfolio for investors.  The MPT is also referred to as mean-variance portfolio theory because it assumes that any investor will choose the optimal portfolio from the set of portfolios that
- Maximizes expected return for a given level of risk; and
- Minimizes risks for a given level of expected returns.

The figure below shows the minimum variance frontier of modern portfolio theory where the horizontal axis shows the risk and the vertical axis shows expected return.

Consider a situation where you have two stocks to choose from: A and B. You can invest your entire wealth in one of these two stocks. Or you can invest 10% in A and 90% in B, or 20% in A and 80% in B, or 70% in A and 30% in B, etc ... There is a huge number of possible combinations and this is a simple case when considering two stocks. Imagine the different combinations you have to consider when you have thousands of stocks.

The minimum variance frontier shows the minimum variance that can be achieved for a given level of expected return. To construct a minimum-variance frontier of a portfolio:

- Use historical data to estimate the mean, variance of each individual stock in the portfolio, and the correlation of each pair of stocks.
- Use a computer program to find out the weights of all stocks that minimize the portfolio variance for each pre-specified expected return.
- Calculate the expected returns and variances for all the minimum variance portfolios determined in step 2 and then graph the two variables.

Investors will never want to hold a portfolio below the minimum variance point. They will always get higher returns along the positively sloped part of the minimum-variance frontier. And the positively sloped part of the minimum-variance frontier is called the **efficient frontier**.

The **efficient frontier** is where the optimal portfolios are. And it helps narrow down the different portfolios from which the investor may choose.

## 2. Goal Of Our Exercise
The goal of this exercise is to find the efficent frontier for an inherent risk using a quantum approach. We will use Qiskit's Finance application modules to convert our portfolio optimization problem into a quadratic program so we can then use variational quantum algorithms such as VQE and QAOA to solve our optimization problem. Let's first start by looking at the actual problem we have at hand.

## 3. Four-Stock Portfolio Optimization Problem

Let us consider a portfolio optimization problem where you have a total of four assets (e.g. STOCK0, STOCK1, STOCK2, STOCK3) to choose from. Your goal is to find out a combination of two assets that will minimize the tradeoff between risk and return which is the same as finding the efficient frontier for the given risk.

## 4. Formulation

How can we formulate this problem?

The function which describes the efficient frontier can be formulated into a quadratic program with linear constraints as shown below.

The terms that are marked in red are associated with risks and the terms in blue are associated with returns.
You can see that our goal is to minimize the tradeoff between risk and return. In general, the function we want to optimize is called an objective function.

 $\min_{x \in \{0, 1\}^n}: $ $q x^n\Sigma x$ - $\mu^n x$

 $subject$ $to: 1^n x = B$

- $x$ indicates asset allocation.
- $Σ$ (sigma) is a covariance matrix.
A covariance matrix is a useful math concept that is widely applied in financial engineering. It is a statistical measure of how two asset prices are varying with respect to each other. When the covariance between two stocks is high, it means that one stock experiences heavy price movements and is volatile if the price of the other stock changes.
- $q$ is called a risk factor (risk tolerance), which is an evaluation of an individual's willingness or ability to take risks. For example, when you use the automated financial advising services, the so-called robo-advising, you will usually see different risk tolerance levels. This q value is the same as such and takes a value between 0 and 1.
- $𝝁$ (mu) is the expected return and is something we obviously want to maximize.
- $n$ is the number of different assets we can choose from
- $B$ stands for Budget.
And budget in this context means the number of assets we can allocate in our portfolio.

#### Goal:
Our goal is to find the **x** value. The x value here indicates which asset to pick (𝑥[𝑖]=1) and which not to pick (𝑥[𝑖]=0).

#### Assumptions:
We assume the following simplifications:
- all assets have the same price (normalized to 1),
- the full budget $B$ has to be spent, i.e. one has to select exactly $B$ assets.
- the equality constraint $1^n x = B$ is mapped to a penalty term $(1^n x - B)^2$ which is scaled by a parameter and subtracted from the objective function.

## Step 1. Import necessary libraries

```python
#Let us begin by importing necessary libraries.
from qiskit import Aer
from qiskit.algorithms import VQE, QAOA, NumPyMinimumEigensolver
from qiskit.algorithms.optimizers import *
from qiskit.circuit.library import TwoLocal
from qiskit.utils import QuantumInstance
from qiskit.utils import algorithm_globals
from qiskit_finance import QiskitFinanceError
from qiskit_finance.applications.optimization import PortfolioOptimization
from qiskit_finance.data_providers import *
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from qiskit_optimization.applications import OptimizationApplication
from qiskit_optimization.converters import QuadraticProgramToQubo
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import datetime
import warnings
from sympy.utilities.exceptions import SymPyDeprecationWarning
warnings.simplefilter("ignore", SymPyDeprecationWarning)
```

## Step 2. Generate time series data (Financial Data)
Let's first generate a random time series financial data for a total number of stocks n=4. We use RandomDataProvider for this. We are going back in time and retrieve financial data from November 5, 1955 to October 26, 1985.

```python
# Set parameters for assets and risk factor
num_assets = 4     # set number of assets to 4
q = 0.5                   # set risk factor to 0.5
budget = 2           # set budget as defined in the problem
seed = 132     #set random seed

# Generate time series data
stocks = [("STOCK%s" % i) for i in range(num_assets)]
data = RandomDataProvider(tickers=stocks,
                 start=datetime.datetime(1955,11,5),
                 end=datetime.datetime(1985,10,26),
                 seed=seed)
data.run()
```

```python
# Let's plot our finanical data
for (cnt, s) in enumerate(data._tickers):
    plt.plot(data._data[cnt], label=s)
plt.legend()
plt.xticks(rotation=90)
plt.xlabel('days')
plt.ylabel('stock value')
plt.show()
```

**WARNING** Please do not change the start/end dates that are given to the RandomDataProvider in this challenge. Otherwise, your answers will not be graded properly.

## Step 3. Quadratic Program Formulation

Let's generate the expected return first and then the covariance matrix which are both needed to create our portfolio.

### Expected Return μ
Expected return of a portfolio is the anticipated amount of returns that a portfolio may generate, making it the mean (average) of the portfolio's possible return distribution.
For example, let's say stock A, B and C each weighted 50%, 20% and 30% respectively in the portfolio.  If the expected return for each stock was 15%, 6% and 9% respectively, the expected return of the portfolio would be:

 μ = (50% x 15%) + (20% x 6%) + (30% x 9%) = 11.4%

For the problem data we generated earlier, we can calculate the expected return over the 30 years period from 1955 to 1985 by using the following `get_period_return_mean_vector()` method which is provided by Qiskit's RandomDataProvider.

```python
#Let's calculate the expected return for our problem data

mu = data.get_period_return_mean_vector()   # Returns a vector containing the mean value of each asset's expected return.

print(mu)
```

### Covariance Matrix Σ
Covariance Σ is a statistical measure of how two asset's mean returns vary with respect to each other and helps us understand the amount of risk involved from an investment portfolio's perspective to make an informed decision about buying or selling stocks.

If you have 'n' stocks in your porfolio, the size of the covariance matrix will be n x n.
Let us plot the covariance marix for our 4 stock portfolio which will be a 4 x 4 matrix.

```python
# Let's plot our covariance matrix Σ（sigma）
sigma = data.get_period_return_covariance_matrix() #Returns the covariance matrix of the four assets
print(sigma)
fig, ax = plt.subplots(1,1)
im = plt.imshow(sigma, extent=[-1,1,-1,1])
x_label_list = ['stock3', 'stock2', 'stock1', 'stock0']
y_label_list = ['stock3', 'stock2', 'stock1', 'stock0']
ax.set_xticks([-0.75,-0.25,0.25,0.75])
ax.set_yticks([0.75,0.25,-0.25,-0.75])
ax.set_xticklabels(x_label_list)
ax.set_yticklabels(y_label_list)
plt.colorbar()
plt.clim(-0.000002, 0.00001)
plt.show()
```

The left-to-right diagnoal values (yellow boxes in the figure below) show the relation of a stock with 'itself'. And the off-diagonal values show the deviation of each stock's mean expected return with respect to each other.  A simple way to look at a covariance matrix is:

 - If two stocks increase and decrease simultaneously then the covariance value will be positive.
 - If one increases while the other decreases then the covariance will be negative.

You may have heard the phrase "Don't Put All Your Eggs in One Basket." If you invest in things that always move in the same direction, there will be a risk of losing all your money at the same time. Covariance matrix is a nice measure to help investors diversify their assets to reduce such risk.

Now that we have all the values we need to build our portfolio for optimization, we will look into Qiskit's Finance application class that will help us contruct the quadratic program for our problem.

## Qiskit Finance application class

In Qiskit, there is a dedicated [`PortfolioOptimization`](https://qiskit.org/documentation/finance/stubs/qiskit_finance.applications.PortfolioOptimization.html#qiskit_finance.applications.PortfolioOptimization) application to construct the quadratic program for portfolio optimizations.

PortfolioOptimization class creates a porfolio instance by taking the following **five arguments** then converts the instance into a quadratic program.

Arguments of the PortfolioOptimization class:
- expected_returns
- covariances
- risk_factor
- budget
- bounds

Once our portfolio instance is converted into a quadratic program, then we can use quantum variational algorithms suchs as Variational Quantum Eigensolver (VQE) or the Quantum Approximate Optimization Algorithm (QAOA) to find the optimal solution to our problem.

We already obtained expected_return and covariances from Step 3 and have risk factor and budget pre-defined. So, let's build our portfolio using the [`PortfolioOptimization`](https://qiskit.org/documentation/finance/stubs/qiskit_finance.applications.PortfolioOptimization.html#qiskit_finance.applications.PortfolioOptimization) class.

## Challenge 1a: Create the portfolio instance using PortfolioOptimization class

**Challenge 1a**

Complete the code to generate the portfolio instance using the [**PortfolioOptimization**](https://qiskit.org/documentation/finance/stubs/qiskit_finance.applications.PortfolioOptimization.html#qiskit_finance.applications.PortfolioOptimization) class. Make sure you use the **five arguments** and their values which were obtained in the previos steps and convert the instance into a quadratic program **qp**.

**Note:** A binary list [1. 1. 0. 0.] indicates a portfolio consisting STOCK2 and STOCK3.

```python
##############################
# Provide your code here

portfolio =
qp =

##############################
print(qp)
```

If you were able to successfully generate the code, you should see a standard representation of the formulation of our qudratic program.

```python
# Check your answer and submit using the following code
from qc_grader import grade_ex1a
grade_ex1a(qp)
```

## Minimum Eigen Optimizer

Interestingly, our portfolio optimization problem can be solved as a ground state search of a Hamiltonian. You can think of a Hamiltonian as an energy function representing the total energy of a physical system we want to simulate such as a molecule or a magnet. The physical system can be further represented by a mathemetical model called an [**Ising model**](https://en.wikipedia.org/wiki/Ising_model) which gives us a framework to convert our binary variables into a so called spin up (+1) or spin down (-1) state.

When it comes to applyting the optimization algorithms, the algorithms usually require problems to satisfy certain criteria to be applicable. For example, variational algorithms such as VQE and QAOA can only be applied to [**Quadratic Unconstrained Binary Optimization (QUBO)**](https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization) problems, thus Qiskit provides converters to automatically map optimization problems to these different formats whenever possible.

Solving a QUBO is equivalent to finding a ground state of a Hamiltonian. And the Minimum Eigen Optimizer translates the Quadratic Program to a Hamiltonian, then calls a given Mimimum Eigensolver such as VQE or QAOA to compute the ground states and returns the optimization results for us.

This approach allows us to utilize computing ground states in the context of solving optimization problems as we will demonstrate in the next step in our challenge exercise.

## Step 5. Solve with classical optimizer as a reference
Lets solve the problem. First classically...

We can now use the Operator we built above without regard to the specifics of how it was created. We set the algorithm for the NumPyMinimumEigensolver so we can have a classical reference. Backend is not required since this is computed classically not using quantum computation. The result is returned as a dictionary.

```python
exact_mes = NumPyMinimumEigensolver()
exact_eigensolver = MinimumEigenOptimizer(exact_mes)
result = exact_eigensolver.solve(qp)

print(result)
```

The optimal value indicates your asset allocation.

## Challenge1b: Solution using VQE

**Variational Quantum Eigensolver (VQE)** is a classical-quantum hybrid algorithm which outsources some of the processing workload to a classical computer to efficiently calculate the ground state energy (lowest energy) of a [**Hamiltonian**](https://en.wikipedia.org/wiki/Hamiltonian_(quantum_mechanics)). As we discussed earlier, we can reformulate the quadratic program as a ground state energy search to be solved by [**VQE**](https://qiskit.org/documentation/stubs/qiskit.algorithms.VQE.html) where the ground state corresponds to the optimal solution we are looking for. In this challenge exercise, you will be asked to find the optimal solution using VQE.

**Challenge 1b**

Find the same solution by using Variational Quantum Eigensolver (VQE) to solve the problem.　We will specify the optimizer and variational form to be used.

**HINT:** If you are stuck, check out [**this qiskit tutorial**](https://qiskit.org/documentation/finance/tutorials/01_portfolio_optimization.html) and adapt it to our problem:

Below is some code to get you started.

```python
optimizer = SLSQP(maxiter=1000)
algorithm_globals.random_seed = 1234
backend = Aer.get_backend('statevector_simulator')

##############################
# Provide your code here

vqe =

##############################

vqe_meo = MinimumEigenOptimizer(vqe)  #please do not change this code

result = vqe_meo.solve(qp)  #please do not change this code

print(result)    #please do not change this code
```

```python
# Check your answer and submit using the following code
from qc_grader import grade_ex1b
grade_ex1b(vqe, qp)
```

VQE should give you the same optimal results as the reference solution.

## Challenge 1c: Portfolio optimization for B=3, n=4 stocks

In this exercise, solve the same problem where one can allocate double weights (can allocate twice the amount) for a single asset.  (For example, if you allocate twice for STOCK3 one for STOCK2, then your portfolio can be represented as [2, 1, 0, 0]. If you allocate a single weight for STOCK0, STOCK1, STOCK2 then your portfolio will look like [0, 1, 1, 1])

Furthermore, change the constraint to B=3.  With this new constraint, find the optimal portfolio that minimizes the tradeoff between risk and return.

**Challenge 1c**

Complete the code to generate the portfolio instance using the PortfolioOptimization class.

Find the optimal portfolio for budget=3 where one can allocate double weights for a single asset.

Use QAOA to find your optimal solution and submit your answer.

**HINT:**  Remember that any one of STOCK0, STOCK1, STOCK2, STOCK3 can have double weights in our portfolio. How can we change our code to accommodate integer variables?

## Step 1: Import necessary libraries

```python
#Step 1: Let us begin by importing necessary libraries
import qiskit
from qiskit import Aer
from qiskit.algorithms import VQE, QAOA, NumPyMinimumEigensolver
from qiskit.algorithms.optimizers import *
from qiskit.circuit.library import TwoLocal
from qiskit.utils import QuantumInstance
from qiskit.utils import algorithm_globals
from qiskit_finance import QiskitFinanceError
from qiskit_finance.applications.optimization import *
from qiskit_finance.data_providers import *
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from qiskit_optimization.applications import OptimizationApplication
from qiskit_optimization.converters import QuadraticProgramToQubo
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import datetime
import warnings
from sympy.utilities.exceptions import SymPyDeprecationWarning
warnings.simplefilter("ignore",SymPyDeprecationWarning)
```

## Step 2: Generate Time Series Data (Financial Data)

```python
# Step 2. Generate time series data for four assets.
# Do not change start/end dates specified to generate problem data.
seed = 132
num_assets = 4
stocks = [("STOCK%s" % i) for i in range(num_assets)]
data = RandomDataProvider(tickers=stocks,
                 start=datetime.datetime(1955,11,5),
                 end=datetime.datetime(1985,10,26),
                 seed=seed)
data.run()
```

```python
# Let's plot our finanical data (We are generating the same time series data as in the previous example.)
for (cnt, s) in enumerate(data._tickers):
    plt.plot(data._data[cnt], label=s)
plt.legend()
plt.xticks(rotation=90)
plt.xlabel('days')
plt.ylabel('stock value')
plt.show()
```

## Step 3: Calculate expected return mu and covariance sigma

```python
# Step 3. Calculate mu and sigma for this problem

mu2 = data.get_period_return_mean_vector()   #Returns a vector containing the mean value of each asset.
sigma2 = data.get_period_return_covariance_matrix() #Returns the covariance matrix associated with the assets.
print(mu2, sigma2)
```

## Step 4: Set parameters and constraints based on this challenge 1c.

```python
# Step 4. Set parameters and constraints based on this challenge 1c

##############################
# Provide your code here

q2 =      #Set risk factor to 0.5
budget2 =      #Set budget to 3

##############################
```

## Step 5: Complete code to generate the portfolio instance

```python
# Step 5. Complete code to generate the portfolio instance

##############################
# Provide your code here

portfolio2 =
qp2 =

##############################
```

## Step 6: Let's solve the problem using QAOA

**Quantum Approximate Optimization Algorithm (QAOA)** is another variational algorithm that has applications for solving combinatorial optimization problems on near-term quantum systems. This algorithm can also be used to calculate ground states of a Hamiltonian and can be easily implemented by using Qiskit's [**QAOA**](https://qiskit.org/documentation/stubs/qiskit.algorithms.QAOA.html) application. (You will get to learn about QAOA in detail in challenge 4. Let us first focus on the basic implementation of QAOA using Qiskit in this exercise.)

```python
# Step 6. Now let's use QAOA to solve this problem.

optimizer = SLSQP(maxiter=1000)
algorithm_globals.random_seed = 1234
backend = Aer.get_backend('statevector_simulator')

##############################
# Provide your code here

qaoa =

##############################

qaoa_meo = MinimumEigenOptimizer(qaoa) #please do not change this code

result2 = qaoa_meo.solve(qp2)  #please do not change this code

print(result2) #please do not change this code
```

Note: The QAOA execution may take up to a few minutes to complete.

# Submit your answer

```python
# Check your answer and submit using the following code
from qc_grader import grade_ex1c
grade_ex1c(qaoa, qp2)
```

### Further Reading:
For those who have successfully solved the first introductory level challenge, **congratulations!**

I hope you were able to learn something about optimizing portfolios and how you can use Qiskit's Finance module to solve the example problem.
 If you are interested in further reading, here are a few literature to explore:

1. [**Quantum optimization using variational algorithms on near-term quantum devices. Moll et al. 2017**](https://arxiv.org/abs/1710.01022)

2. [**Improving Variational Quantum Optimization using CVaR. Barkoutsos et al. 2019.**](https://arxiv.org/abs/1907.04769)

### Good luck and have fun with the challenge!

## Additional information

**Created by:** Yuri Kobayashi

**Version:** 1.0.0