## Learning Objectives

By the end of this session you should be able to...

1. Take the derivative of a function over one variable
1. Take the partial derivative of a function over all of its variables
1. Find the minimum of the function to obtain the best line that represents relationships between two variables in a dataset

## Why are derivatives important?

Derivatives are the foundation for Linear Regression (a topic we'll cover later in the course) that allows us to obtain the best line that represents relationships between two variables in a dataset.

## Introduction to Derivatives

The process of fidning a derivative is called **Differentiation**, which is a technique used to calculate the slope of a graph at different points.

### Activity - Derivative Tutorial:

1. Go through this [Derivative tutorial from Math Is Fun](https://www.mathsisfun.com/calculus/derivatives-introduction.html) (15 min)
1. When you're done, talk with a partner about topics you still have questions on. See if you can answer each other's questions. (5 min)
1. We'll then go over questions on the tutorial as a class (10 min)

### Review Diagram

Review the below diagram as a class, and compare with what you just learned in the above Derivative Tutorial. Note that a Gradient Function is just another name for the Derivative of a function:

## Derivative Formula

- Choose small $\Delta x$

- $f^\prime(x) = \frac{d}{dx}f(x) = \frac{\Delta x}{\Delta y}  = \frac{f(x + \Delta x) - f(x)}{\Delta x}$

Remember that $\Delta x$ approaches 0. So if plugging in a value in the above formula, choose a _very_ small number, or simplify the equation further such that all $\Delta x = 0$, like we saw in the tutorial

## Activity: Write a Python function that calculates the gradient of $x^2$ at $x = 3$ and $x = -2$ using the above definition

```python
# this is how to compute derative numerically
def f(x):
    return x**2

eps = 1e-6
x = 3
print((f(x + eps) - f(x)) / eps)
x = -2
print((f(x + eps) - f(x)) / eps)
```

    6.000001000927568
    -3.999998999582033

Note that these values match $2x$, our derivative of $x^2$:

$2*3 = 6$

$2 * -2 = -4$

## Derivative Table

As a shortcut, use the second page of this PDF to find the derivative for common formulas. Utilize this as a resource going forward!

- https://www.qc.edu.hk/math/Resource/AL/Derivative%20Table.pdf

## Extend Gradient into Two-Dimensional Space

Now we know how to calculate a derivative of one variable. But what if we have two?

To do this, we need to utilize **Partial Derivatives**. Calculating a partial derivative is essentially calculating two derivatives for a function: one for each variable, where they other variable is set to a constant.

### Activity - Partial Derivative Video

Lets watch this video about Partial Derivative Intro from Khan Academy: https://youtu.be/AXqhWeUEtQU

**Note:** Here are some derivative shortcuts that will help in the video:

$\frac{d}{dx}x^2 = 2x$

$\frac{d}{x}sin(x) = cos(x)$

$\frac{d}{dx}x = 1$

### Activity - Now You Try!
Consider the function $f(x, y) = \frac{x^2}{y}$

- Calculate the first order partial derivatives ($\frac{\partial f}{\partial x}$ and $\frac{\partial f}{\partial y}$) and evaluate them at the point $P(2, 1)$.

## We can use the Symbolic Python package (library) to compute the derivatives and partial derivatives

```python
!python3 -m pip install sympy
```

    Requirement already satisfied: sympy in /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages (1.4)
    Requirement already satisfied: mpmath>=0.19 in /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages (from sympy) (1.1.0)
    WARNING: You are using pip version 19.3; however, version 19.3.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.

```python
!which
```

    usage: which [-as] program ...

```python
from sympy import symbols, diff
# initialize x and y to be symbols to use in a function
x, y = symbols('x y', real=True)
f = (x**2)/y
# Find the partial derivatives of x and y
fx = diff(f, x, evaluate=True)
fy = diff(f, y, evaluate=True)
print(fx)
print(fy)
# print(f.evalf(subs={x: 2, y: 1}))
print(fx.evalf(subs={x: 2, y: 1}))
print(fy.evalf(subs={x: 2, y: 1}))
```

## Optional Reading: Tensorflow is a powerful package from Google that calculates the derivatives and partial derivatives numerically

```python
import tensorflow as tf

x = tf.Variable(2.0)
y = tf.Variable(1.0)

with tf.GradientTape(persistent=True) as t:
    z = tf.divide(tf.multiply(x, x), y)

# Use the tape to compute the derivative of z with respect to the
# intermediate value x and y.
dz_dx = t.gradient(z, x)
dz_dy = t.gradient(z, y)

print(dz_dx)
print(dz_dy)

# All at once:
gradients = t.gradient(z, [x, y])
print(gradients)

del t
```

## Optional Reading: When x and y are declared as constant, we should add `t.watch(x)` and `t.watch(y)`

```python
import tensorflow as tf

x = tf.constant(2.0)
y = tf.constant(1.0)

with tf.GradientTape(persistent=True) as t:
    t.watch(x)
    t.watch(y)
    z = tf.divide(tf.multiply(x, x), y)

# Use the tape to compute the derivative of z with respect to the
# intermediate value y.
dz_dx = t.gradient(z, x)
dz_dy = t.gradient(z, y)
```

# Calculate Partial Derivative from Definition

```python
def f(x, y):
    return x**2/y

eps = 1e-6
x = 2
y = 1
print((f(x + eps, y) - f(x, y)) / eps)
print((f(x, y + eps) - f(x, y)) / eps)
```

Looks about right! This works rather well, but it is just an approximation. Also, you need to call `f()` at least once per parameter (not twice, since we could compute `f(x, y)` just once). This makes this approach difficult to control for large systems (for example neural networks).

## Why Do we need Partial Gradients?

In many applications, more specifically DS applications, we want to find the Minimum of a cost function

- **Cost Function:** a function used in machine learning to help correct / change behaviour to minimize mistakes. Or in other words, a measure of how wrong the model is in terms of its ability to estimate the relationship between x and y. [Source](https://towardsdatascience.com/machine-learning-fundamentals-via-linear-regression-41a5d11f5220)

Why do we want to find the minimum for a cost function? Given that a cost function mearues how wrong a model is, we want to _minimize_ that error!

In Machine Learning, we frequently use models to run our data through, and cost functions help us figure out how badly our models are performing. We want to find parameters (also known as **weights**) to minimize our cost function, therefore minimizing error!

We find find these optimal weights by using a **Gradient Descent**, which is an algorithm that tries to find the minimum of a function (exactly what we needed!). The gradient descent tells the model which direction it should take in order to minimize errors, and it does this by selecting more and more optimal weights until we've minimized the function! We'll learn more about models when we talk about Linear Regression in a future lesson, but for now, let's review the Gradient Descent process with the below images, given weights $w_0$ and $w_1$:

Look at that bottom right image. Looks like we're using partial derivatives to find out optimal weights. And we know exactly how to do that!

## Finding minimum of a function

Assume we want to minimize the function $J$ which has two weights $w_0$ and $w_1$

We have two options to find the minimum of $J(w_0, w_1)$:

1. Take partial derivatives of $J(w_0, w_1)$ with relation to $w_0$ and $w_1$:

$\frac{\partial J(w_0, w_1)}{\partial w_0}$

$\frac{\partial J(w_0, w_1)}{\partial w_1}$

And find the appropriate weights such that the partial derivatives equal 0:

$\frac{\partial J(w_0, w_1)}{\partial w_0} = 0$

$\frac{\partial J(w_0, w_1)}{\partial w_1} = 0$

In this approach we should solve system of linear or non-linear equation

2. Use the Gradient Descent algorithm:

First we need to define two things:

- A step-size alpha ($\alpha$) as a small number (like $1^(e-6)$ small)
- An arbitrary random initial value for $w_0$ and $w_1$: $w_0 = np.random.randn()$ and $w_1 = np.random.randn()$

Finally, we need to search for the most optimal $w_0$ and $w_1$ by using a loop to update the weights until we find the most optimal weights. We'll need to establish a threshold to compare weights to, toi know when to stop the loop. For example, if a weight from one iteration is within 0.0001 of the weight from the next iteration, we can stop the loop (0.0001 is our threshold here)

Let's review some pseudocode for how to implement this algorithm:

```
initialize the following:
    a starting weight value
    the learning rate (alpha) (very small)
    the threshold (small)
    the current threshold (start at 1)

while the current threshold is less than the threshold:
    store the current values of the weights into a previous value variable
    set the weight values to new values based on the algorithm
    set the current threshold from the difference of the current weight value and the previous weight value
```

How do we `set the weight values to new values based on the algorithm`? by using the below equations:

$w_0 = w_0 - \alpha \frac{\partial J(w_0, w_1)}{\partial w_0}$

$w_1 = w_1 - \alpha \frac{\partial J(w_0, w_1)}{\partial w_1}$

Try to write the function yourself, creating real code from the pseudocode!

**Stretch Challenge:** We may also want to limit the number of loops we do, in addition to checking the threshold. Determine how we may go about doing that

## Resources

- [Derivative tutorial from Math Is Fun](https://www.mathsisfun.com/calculus/derivatives-introduction.html)
- [Derivative Table](https://www.qc.edu.hk/math/Resource/AL/Derivative%20Table.pdf)
- [Khan Academy - Partial Derivatives video](https://www.youtube.com/watch?v=AXqhWeUEtQU&feature=youtu.be)
- [Towards Data Science - Machine Learning Fundamentals: cost functions and gradient Descent](https://towardsdatascience.com/machine-learning-fundamentals-via-linear-regression-41a5d11f5220)

```python

```


Implementing Softmax Regression (Multinomial Logistic Regression)

This notebook follows this Stanford file: http://deeplearning.stanford.edu/tutorial/supervised/SoftmaxRegression/
and this tutorial by Nikhil Kumar:https://www.geeksforgeeks.org/softmax-regression-using-tensorflow/

**Again like the last notebook, I recomend just following those two tutorials above. This notebook doesnt add much to both those wonderful tutorials

$$ \text{Softmax regression (or multinomial logistic regression) is a generalization of logistic regression to the case where we want to handle multiple classes.} \\ \text{In logistic regression we assumed that the labels were binary: } y_i = \in \{0, 1\} $$
$$ \text{Softmax regression allows us to handle: } y_i = \in \{0, 1, \ldots, K\} \text{where K is the number of classes.} $$
$$ \text{Given a test input x, we want our hypothesis to estimate the probability that } P(y=k|x) \\ \text{ for each value of } k = 1,\ldots,K \text{. I.e., we want to estimate the probability of the class label taking on each of the K different possible values. Thus, our hypothesis will output a } \\ \text{K-dimensional vector (whose elements sum to 1) giving us our K estimated probabilities.} $$
$$ \text{Let } X = \begin{equation} \begin{bmatrix} 1 & x_{1,1} & \ldots & x_{1,p} \\ 1 & x_{2,1} & \ldots & x_{2,p} \\ \vdots & \vdots & \ddots & \vdots \\ 1 & x_{n,1} & \ldots & x_{n,p} \end{bmatrix} \label{eq:aeqn} \end{equation}$$
$$ \text{Where the dataset has ‘m’ features and ‘n’ observations. Also, there are ‘k’ class labels, i.e every observation can be classified} \\ \text{as one of the ‘k’ possible target values. For example, if we have a dataset of 100 handwritten digit images of vector size 28×28 for} \\ \text{digit classification, we have, n = 100, m = 28×28 = 784 and k = 10.} $$
$$ \text{Then our prediction function is: } P(y_i=k|x_i;\theta) = \begin{bmatrix} P(y=0|x;\theta) \\ P(y=1|x;\theta) \\ \vdots \\ P(y=K|x;\theta) \end{bmatrix} = \frac{e^{\theta_k^Tx_i}}{\sum_{j=1}^{K}e^{\theta_j^Tx}}$$

We now describe the cost function that we’ll use for softmax regression. In the equation below, 1{⋅} is the ”‘indicator function,”’ so that 1{a true statement}=1, and 1{a false statement}=0. For example, 1{2+2=4} evaluates to 1; whereas 1{1+1=5} evaluates to 0. Our cost function will be:

$$ J(\theta) = -[\sum_{i=1}^{n}\sum_{k=1}^{K}1\{y_i=k\}\log_{10}(\frac{e^{\theta_k^Tx_i}}{\sum_{j=1}^{K}e^{\theta_j^Tx_i}})] $$

Notice that this generalizes the logistic regression cost function, which could also have been written:

$$ J(\theta) = -[\sum_{i=1}^{n}\log_{10}(1 - h_\theta(x_i)) + y_i(\log_{10}(h_\theta(x_i))] $$
$$ = -[\sum_{i=1}^{n}\sum_{k=0}^{1}1\{y_i=k\}\log_{10}(P(y_i=k|x_i;\theta)] $$

We cannot solve for the minimum of J(θ) analytically, and thus as usual we’ll resort to an iterative optimization algorithm. Taking derivatives, one can show that the gradient is:

$$ \triangledown_{\theta_k}J(\theta) =  -\sum_{i=1}^{n}[x_i(1\{y_i=k\} - P(y_i=k|x_i;\theta)] \text{ or: } $$
$$ \triangledown_{\theta_k}J(\theta) =  -\sum_{i=1}^{n}[x_i(1\{y_i=k\} - \frac{e^{\theta_k^Tx_i}}{\sum_{j=1}^{K}e^{\theta_j^Tx}}] $$

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

def loadCSV(filename):
    '''
    function to load dataset
    '''
    with open(filename,"r") as csvfile:
        lines = csv.reader(csvfile)
        dataset = list(lines)
        for i in range(len(dataset)):
            dataset[i] = [float(x) for x in dataset[i]]
    return np.array(dataset)

def normalize(X):
    '''
    function to normalize feature matrix, X
    '''
    mins = np.min(X, axis = 0)
    maxs = np.max(X, axis = 0)
    rng = maxs - mins
    norm_X = 1 - ((maxs - X)/rng)
    return norm_X

def P(theta, X):
    #return np.exp(np.matmul(theta.T, X)) / np.sum(np.exp(np.matmul(theta.T, X)))
    return np.exp(theta.T * X) / np.sum(np.exp(theta.T * X))

def J(theta, X, Y, K):
    J = 0
    for i in range(len(Y)):
        for k in range(1, K):
            if Y[i] == k:
                multiplier = 1
            else:
                multiplier = 0
            J += multiplier*np.log10(P(theta, X))
    return J

def del_J(theta, X, Y, K):
    J = 0
    for i in range(len(Y)):
        for k in range(1, K):
            if Y[i] == k:
                multiplier = 1
            else:
                multiplier = 0
            J += X[i]*(multiplier - P(theta, X))
    return J

def grad(X, Y, K, lr, epochs):
    theta = np.random.random(size=X.shape)
    for _ in range(epochs):
        theta -= lr*del_J(theta, X, Y, K)
    return theta

def fit(X, Y, lr=0.01, epochs=25):
    #Note: after 25 epochs data points get closer and closer to nan so crashes. This is fine for learning purposes
    y, K = OneHotEncode(Y)
    thetas = grad(X, y, K, lr, epochs)
    return thetas

def predict(X, thetas):
    return np.round(P(thetas, X))
```

```python
def warn(*args, **kwargs):
    pass
import warnings
warnings.warn = warn

from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
xtrain, xtest, ytrain, ytest = train_test_split(
        X, y, test_size = 0.25, random_state = 0)
clf = LogisticRegression(random_state=0).fit(xtrain, ytrain)
y_pred = clf.predict(xtest)
clf.predict_proba(xtrain[:2, :])
print(clf.score(xtest, ytest))

from sklearn.metrics import confusion_matrix
cm = confusion_matrix(ytest, y_pred)

print ("Confusion Matrix : \n", cm)
from sklearn.metrics import accuracy_score
print ("Accuracy : ", accuracy_score(ytest, y_pred))
```

    (150, 4) (150,)
    0.9736842105263158
    Confusion Matrix :
     [[13  0  0]
     [ 0 15  1]
     [ 0  0  9]]
    Accuracy :  0.9736842105263158

7. Softmax Regression (Multinomial Logistic Regression).ipynb


```python
import math
a = 2
math.sqrt(a)
```

    1.4142135623730951

```python
import sympy
sympy.var("X")
equation = sympy.Eq(X**3 + 2 * X**2 - 5 * X - 6, 0)
sympy.solve(equation)
```

    [-3, -1, 2]

```python

```


# Teleportation

https://en.wikipedia.org/wiki/Quantum_teleportation

## Imports

```python
%matplotlib inline
```

```python
from sympy import init_printing
init_printing(use_latex=True)
```

```python
from sympy import sqrt, symbols, Rational
from sympy import expand, Eq, Symbol, simplify, exp, sin
from sympy.physics.quantum import *
from sympy.physics.quantum.qubit import *
from sympy.physics.quantum.gate import *
from sympy.physics.quantum.grover import *
from sympy.physics.quantum.qft import QFT, IQFT, Fourier
from sympy.physics.quantum.circuitplot import circuit_plot
```

## Teleportation

```python
a,b = symbols('a b', real=True)
state = Qubit('000')*a + Qubit('001')*b
state
```

```python
entangle1_2 = CNOT(1,2)*HadamardGate(1)
entangle1_2
```

```python
state = qapply(entangle1_2*state)
state
```

```python
entangle0_1 = HadamardGate(0)*CNOT(0,1)
entangle0_1
```

```python
circuit_plot(entangle0_1*entangle1_2, nqubits=3);
```

```python
state = qapply(entangle0_1*state)
state
```

```python
result = measure_partial(state, (0,1))
```

```python
state = (result[2][0]*2).expand()
state
```

```python
state = qapply(XGate(2)*state)
state
```


```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import patches
from sympy import *
from scipy.optimize import fsolve, fmin, root
from collections import Counter, defaultdict
init_printing(use_unicode=True)
```

```python
r, c, u, v, E = symbols('r c u v E')
```

## Simple Case

$f = \dfrac{1}{2}rc^{2} - Ec$ with $r > 0$

$c = \dfrac{E}{r}$

```python
def fsimple(r,c,E):
    return (1/2)*r*c**2-E*c

def csimple(E,r):
    return E/r
```

```python
c = np.linspace(-5,5,1001)
```

```python
plt.figure(figsize=(11,8))
plt.gca().spines['left'].set_position('zero')
plt.gca().spines['bottom'].set_position('zero')
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
plt.gca().xaxis.set_ticks_position('bottom')
plt.gca().yaxis.set_ticks_position('left')
plt.xlim(-5,5), plt.ylim(-5,15)
plt.xlabel('c', fontsize=14)
plt.ylabel('f', rotation='horizontal',verticalalignment='center',  fontsize=14)
plt.gca().yaxis.set_label_coords(0.53,1)
plt.gca().xaxis.set_label_coords(1.03,0.22)
yticks1 = plt.gca().yaxis.get_major_ticks()
yticks1[1].label1.set_visible(False)
plt.plot(c,fsimple(0.5,c,1),label='r = 0.5')
plt.plot(c,fsimple(1,c,1),label='r = 1')
plt.plot(c,fsimple(2,c,1),label='r = 2')
plt.legend(loc='best');
```

```python
E = np.linspace(0,5)
```

```python
plt.figure(figsize=(11,8))
plt.plot(E,csimple(E,0.5),label='r = 0.5')
plt.plot(E,csimple(E,1),label='r = 1')
plt.plot(E,csimple(E,2),label='r = 2')
plt.plot(E,csimple(E,4),label='r = 4')
plt.plot(E,csimple(E,8),label='r = 8')
plt.plot(E,csimple(E,16),label='r = 16')
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
plt.gca().xaxis.set_ticks_position('bottom')
plt.gca().yaxis.set_ticks_position('left')
plt.ylabel('c',fontsize=18,rotation='horizontal',labelpad=15)
plt.xlabel('E',fontsize=18)
plt.legend(loc='best');
```

### Not so simple case

$f = \dfrac{1}{2}rc^{2} + \dfrac{1}{4}|u|c^{4} - Ec$

```python
def f2(r,c,u,E):
    return (1/2)*r*c**2+(1/4)*u*c**4-E*c
```

```python
plt.figure(figsize=(11,8))
plt.xlabel('c',fontsize=18)
plt.ylabel('f',fontsize=18,rotation='horizontal',labelpad=15)
plt.xlim(-3,3), plt.ylim(-5,15)
plt.gca().spines['left'].set_position('zero')
plt.gca().spines['bottom'].set_position('zero')
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
plt.gca().xaxis.set_ticks_position('bottom')
plt.gca().yaxis.set_ticks_position('left')
plt.gca().yaxis.set_label_coords(0.53,1)
plt.gca().xaxis.set_label_coords(1.03,0.27)
yticks2 = plt.gca().yaxis.get_major_ticks()
yticks2[1].label1.set_visible(False)
plt.plot(c,f2(1,c,1,1),label='r = 1')
plt.plot(c,f2(5,c,1,1),label='r = 5')
plt.legend(loc='best');
```

```python
def please(r,u,E):
    return ((np.sqrt(12*r**3*u**3+81*u**4*E**2)+9*u**2*E)/2)**(1/3)/(3**(2/3)*u) - (r*(2/3)**(1/3)/((np.sqrt(12*r**3*u**3+81*u**4*E**2)+9*u**2*E))**(1/3))
```

```python
E2 = np.linspace(-1,5)
```

```python
plt.figure(figsize=(11,8))
plt.xlim(0,5),plt.ylim(0,2)
plt.xlabel('E',fontsize=18)
plt.ylabel('c',fontsize=18,rotation='horizontal',labelpad=15)
plt.plot(E,please(1,1,E),label='r = 1')
plt.plot(E,please(2,1,E),label='r = 2')
plt.plot(E,please(3,1,E),label='r = 3')
plt.plot(E,please(4,1,E),label='r = 4')
plt.plot(E,please(5,1,E),label='r = 5')
plt.legend(loc='best');
```

```python
def Eofc1(r,c,u):
    return r*c+u*c**3
```

```python
plt.figure(figsize=(11,8))
plt.xlim(-1,5),plt.ylim(0,2)
plt.gca().spines['left'].set_position('zero')
plt.gca().spines['bottom'].set_position('zero')
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
plt.gca().xaxis.set_ticks_position('bottom')
plt.gca().yaxis.set_ticks_position('left')
plt.xlim(-1,5), plt.ylim(0,2)
plt.xlabel('E', fontsize=18)
plt.ylabel('c', rotation='horizontal',verticalalignment='center',  fontsize=18)
plt.gca().yaxis.set_label_coords(0.2,1)
plt.gca().xaxis.set_label_coords(1.03,0.03)
yticks3 = plt.gca().yaxis.get_major_ticks()
yticks3[0].label1.set_visible(False)
plt.plot(Eofc1(-1,c,1),c,label='r = -1')
plt.plot(Eofc1(1,c,1),c,label='r = 1')
plt.plot(Eofc1(5,c,1),c,label='r = 5')
plt.legend(loc='best');
```

### Even less simple

$f = \dfrac{1}{2}rc^{2} - \dfrac{1}{4}|u|c^{4} + \dfrac{1}{6}|v|c^{6} - Ec$

```python
def f3(c,r,u,v,E):
    return (1/2)*r*c**2-(1/4)*abs(u)*c**4+(1/6)*abs(v)*c**6-E*c

def df3(c,r,u,v,E):
    return r*c+u*c**3+v*c**5-E
```

```python
f = (1/2)*r*c**2+(1/4)*u*c**4+(1/6)*v*c**6-E*c
f
```

```python
E_c = expand(solve(f.diff(c),E)[0])
E_c
```

```python
plt.plot(df3(np.linspace(0,1),0.3,-1,1,0),np.linspace(0,1))
# plt.scatter(Eu(0.3,-1,1),0.35608274213,c='g',label='$E_U,\ c_L$')
# plt.scatter(El(0.3,-1,1),0.687899033839,c='r',label='$E_L,\ c_U$')
plt.xlim(0,0.1),plt.ylim(0,1)
plt.legend(fontsize=16);
```

```python
simplify(c_U.subs([(r,0.3),(u,-1),(v,1)])),simplify(c_L.subs([(r,0.3),(u,-1),(v,1)]))
```

```python
0.217532774716107*np.sqrt(10),0.112603250061049*np.sqrt(10)
```

```python
solve(simplify(E_c.diff(c)),c)
```

```python
c_L = solve(simplify(E_c.diff(c)),c)[1]
c_U = solve(simplify(E_c.diff(c)),c)[3]
```

```python
c_L,c_U
```

```python
E_c.subs(c,c_U)
```

```python
E_c.subs(c,c_L)
```

```python
E_U = simplify(E_c.subs(c,c_L))
E_L = simplify(E_c.subs(c,c_U))
E_U,E_L
```

```python
r = np.linspace(0,0.5)
```

```python
def E_U(r,u,v):
    return (np.sqrt(10*v*(-3*u-np.sqrt(9*u**2-20*r*v)))*(20*r*v-3*u**2-u*np.sqrt(9*u**2-20*r*v))/(250*v**2))

def E_L(r,u,v):
    return (np.sqrt(10*v*(-3*u+np.sqrt(9*u**2-20*r*v)))*(20*r*v-3*u**2+u*np.sqrt(9*u**2-20*r*v))/(250*v**2))
```

```python
plt.figure(figsize=(11,8))
plt.xlabel('c',fontsize=18)
plt.ylabel('f',fontsize=18,rotation='horizontal',labelpad=15)
plt.xlim(-2,2), plt.ylim(-5,5)
plt.gca().spines['left'].set_position('zero')
plt.gca().spines['bottom'].set_position('zero')
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
plt.gca().xaxis.set_ticks_position('bottom')
plt.gca().yaxis.set_ticks_position('left')
plt.gca().yaxis.set_label_coords(0.53,0.97)
plt.gca().xaxis.set_label_coords(1.04,0.52)
yticks4 = plt.gca().yaxis.get_major_ticks()
yticks4[3].label1.set_visible(False)
plt.plot(c,f3(c,1,1,1,1),label='r = 1')
plt.plot(c,f3(c,3,1,1,1),label='r = 3')
plt.plot(c,f3(c,5,1,1,1),label='r = 5')
plt.legend(loc='best');
```

```python
def Eofc2(r,c,u,v):
    return r*c-u*c**3+v*c**5
```

```python
plt.figure(figsize=(11,8))
plt.gca().spines['left'].set_position('zero')
plt.gca().spines['bottom'].set_position('zero')
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
plt.gca().xaxis.set_ticks_position('bottom')
plt.gca().yaxis.set_ticks_position('left')
plt.xlim(0,0.17), plt.ylim(0,1)
plt.yticks([0,0.2,0.4,0.6,0.8,1.0],('0','0.2','0.4','0.6','0.8','1.0'),fontsize=18)
plt.xticks([0,0.05,0.1,0.15],('0','0.05','0.1','0.15'),fontsize=18)
plt.xlabel('E-Field (E)', fontsize=18)
plt.ylabel('Tilt ($\mathregular{\\theta}$)', rotation='horizontal',verticalalignment='center', fontsize=18)
plt.title('Figure 2',y=-0.15,fontsize=20)
plt.gca().text(0.15, 0.85, 'High Tilt', fontsize=18)
plt.text(0.01, 0.79, 'Low Temp', fontsize=18,color='blue')
plt.text(0.15, 0.36, 'High Temp', fontsize=18,color='red')
plt.gca().text(0.02, 0.2, 'Low Tilt', fontsize=18)
plt.gca().yaxis.set_label_coords(0.07,1)
plt.gca().xaxis.set_label_coords(1.1,0.02)
yticks5 = plt.gca().yaxis.get_major_ticks()
yticks5[0].label1.set_visible(False)
# plt.plot(Eofc2(0.15,c,1,1),c,label='r(T) = 0.15')
# plt.plot(Eofc2(0.2,c,1,1),c,label='r(T) = 0.2')
plt.plot(Eofc2(0.3,c,1,1),c,label='r(T) = 0.3')
# plt.plot([0.13145,0.13145],[0,1],'--k')
# plt.plot(Eofc2(0.4,c,1,1),c,label='r(T) = 0.4')
plt.plot(Eofc2(0.45,c,1,1),c,label='r(T) = 0.45')
plt.plot(Eofc2(0.5,c,1,1),c,label='r(T) = 0.5')
plt.legend(loc='lower right',fontsize=14);
```

```python
s = np.linspace(0,0.5,101)
```

```python
r3 = 3/16
Ec = np.sqrt(3/10)*(6/25)
r_T, E_T = [],[]
u=-1
v=1
n = 100
for i in np.linspace(0,Ec,n): #E
    for j in np.linspace(r3,9/20,n): #r
        if (E_L(j,u,v) < i) and (i < E_U(j,u,v)):
            c1 = fsolve(df3,np.array([0,1]),args=(j,u,v,i))

            f1 = f3(c1[0],j,u,v,i)
            f2 = f3(c1[1],j,u,v,i)

            if f1 <= f2:
                r_T.append(j)
                E_T.append(i)
                r3 = j
                break
```

```python
plt.figure(figsize=(11,8))
plt.plot(s,E_U(s,u,v),label='$\mathregular{E_{Upper}}$')
plt.plot(s,E_L(s,u,v),label='$\mathregular{E_{Lower}}$')
plt.fill_between(s,E_U(s,u,v),E_L(s,u,v), color='#D98880',alpha='0.5')
plt.plot(r_T,E_T,'--r',label='$\mathregular{E_{T}}$')
plt.scatter(0.45,E_L(s,-1,1)[90],c='k')
plt.plot([0,0.45],[E_L(s,-1,1)[90],E_L(s,-1,1)[90]],'--k')
plt.plot([0.45,0.45],[0,E_L(s,-1,1)[90]],'--k')
plt.gca().spines['left'].set_position('zero')
plt.gca().spines['bottom'].set_position('zero')
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
plt.gca().xaxis.set_ticks_position('bottom')
plt.gca().yaxis.set_ticks_position('left')
yticks6 = plt.gca().yaxis.get_major_ticks()
plt.title('Figure 3',y=-0.15,fontsize=20)
yticks6[0].label1.set_visible(False)
plt.gca().yaxis.set_label_coords(0.13,0.97)
plt.gca().xaxis.set_label_coords(1.05,0.02)
plt.xticks([0,0.1875,0.25,0.45,0.5],('$\mathregular{r_{2}}$','$\mathregular{r_{T}}$','$\mathregular{r_{1}}$','$\mathregular{r_{C}}$','0.5'),fontsize=18)
plt.yticks([0,0.05,0.10,0.13145,0.15,0.20],('0','0.05','0.10','$\mathregular{E_{C}}$','0.15','0.20'),fontsize=18)
plt.gca().annotate('phase boundary', xy=(0.37,0.09), xytext=(0.2, 0.12),arrowprops=dict(arrowstyle="->",facecolor='black'),fontsize=16)
plt.gca().text(0.1, 0.06, 'High Tilt', fontsize=18)
plt.gca().text(0.35, 0.04, 'Low Tilt', fontsize=18)
plt.gca().text(0.47, 0.142, 'B', fontsize=16)
plt.gca().text(0.1, 0.1, '$\mathregular{u_{\\theta} = -1}$', fontsize=14)
plt.gca().text(0.1, 0.09, '$\mathregular{v_{\\theta} = 1}$', fontsize=14)
plt.gca().annotate("A", xy=(0.385, 0.06), xytext=(0.245, 0.075), arrowprops=dict(arrowstyle="->",connectionstyle='arc3'),fontsize=16)
plt.gca().annotate("", xy=(0.4125,0.131), xytext=(0.4165,0.135),arrowprops=dict(arrowstyle="->",connectionstyle='arc3'))
plt.gca().annotate("", xy=(0.465,0.122), xytext=(0.48,0.126),arrowprops=dict(arrowstyle="->",connectionstyle='arc3'))
e1 = patches.Arc((0.45, 0.13145), 0.07, 0.02, angle=0, theta1=-60, theta2=165, linewidth=1, fill=False, zorder=2)
plt.gca().add_patch(e1)
plt.xlabel('r(T)',fontsize=18)
plt.ylabel('E',fontsize=18,rotation='horizontal')
plt.ylim(0,0.15),plt.xlim(-0.05,0.6)
plt.legend(loc='lower right',fontsize=16);
```

### $r_{1} = 0.25$

### $r_{2} = 0$

### $r_{C} = 0.45$

### $r_{C} = 0.1875$

### $E_{C} = 0.13145$

```python
Elower(0.45,-1,1),Eupper(0.45,-1,1)
```

    (0.13145341380123987, 0.13145341380123987)

```python
for i in range(s.shape[0]):
    if Elower(s,-1,1)[i]==Eupper(s,-1,1)[i]:
        print(i,s[i])
```

    90 0.45

    /usr/local/lib/python3.4/dist-packages/IPython/kernel/__main__.py:9: RuntimeWarning: invalid value encountered in sqrt
    /usr/local/lib/python3.4/dist-packages/IPython/kernel/__main__.py:4: RuntimeWarning: invalid value encountered in sqrt

```python
for i in range(s.shape[0]):
    if Eupper(s,-1,1)[i]==0:
        print(i,s[i])
```

    50 0.25

    /usr/local/lib/python3.4/dist-packages/IPython/kernel/__main__.py:4: RuntimeWarning: invalid value encountered in sqrt


```python
from sympy import *
from sympy.plotting import *
init_printing(use_latex=True)
```

# Revisión de ejercicios de la tarea

## Evaluaciones

2. $f(x,y)=3x^2-5xy+y^3$

halle el valor de: $f(1,2)$, $f(-1,-5)$ y $f(3,-2)$.

3. Si $$g(x,y)=e^{2x^2y-5x+2y}$$

evaluar $g$ en los siguientes puntos: $(0,0)$ y $(-1,1)$.

4. Considere $$h(x,y)=ln(x^2+4x+y^2-1)$$

calcule:

$h(1,2)$, $h(0,-0.5)$ y $h\left(\frac{1}{2},\frac{1}{2}\right)$.

## Ejercicios Aplicados

1. Una empresa produce dos productos, $X$ y $Y$. Las unidades de costos de mano de obra y de materiales
   son de 5 dólares en el caso del producto $X$ y de 12 dólares por lo que
   respecta a $Y$. Además, la empresa también tiene costos fijos
   de  3000 dólares al mes. Exprese el costo mensual $C$ (en dólares)
   como una función de las unidades de $X$ y $Y$ producidas.
   ¿Cuál es el costo total de producir 200 unidades de $X$ y 150
   unidades de $Y$?

2. Se lanza un nuevo producto al mercado. El volumen de ventas $x$ se incrementa como una función del tiempo $t$ y depende        también de la cantidad $A$ gastada en la campaña publicitaria. Si, con $t$ medido en meses y A en dólares,

   $$x=200(5-e^{-0.002A})(1-e^{-t})$$

   Determine el volumen de ventas si ha transcurrido 1 mes y la inversión fue de 400 dólares.

3. La función de producción de cierta empresa está dada por

   $$P=5L + 2L^2+ 3LK + 8K + 3K^2$$

   en donde $L$ es el insumo mano de obra medido en miles de horas-hombre por semana, $K$ es el monto de capital invertido        medido en miles de dólares por semana y $P$ es la producción semanal en miles de artículos. Determine la productividad          cuando $L=5$ y $K=12$.

4. Suponga que el costo de producción de una empresa que fabrica    dos artículos $A$ y $B$ viene dada por

   $$C(x, y) = 1500 -7.5x- 15y-0.3xy+0.3x^2 + 0.2y^2$$

   Si la empresa puede vender cada unidad de $A$ a 20 dólares y      cada unidad de $B$ a
   16 dólares, encuentre la función de utilidad por la producción    y venta de los articulos $A$ y $B$.

5. Juguetes Mónica produce dos tipos diferentes de
   cochecitos de plástico con un costo de 300 pesos y 900 pesos      cada uno. Las demandas anuales $x_1$ y $x_2$ (en miles) están    dadas por

    $$x_1 = 30 + 2p_2 - 5p_1 \ \ \ \  y \ \ \ \ x_2 = 100 + p_1 -     2p_2 $$

   con $p_1$ y $p_2$ los precios unitarios (en pesos) de los dos    tipos de cochecitos. Determine la utilidad en función de los      precios $p_1$ y $p_2$.

6. Los costos de producción de una empresa que fabrica los artículos $A,\ B,\ C$ y $D$ vienen dados por
$$C(x,y,z,w)=0.1x+2.2y+1.7z+4w+2xy+0.5zw+2000$$
    siendo $x,\ y,\ z$ y $w$ la cantidad de artículos producidos de los productos $A,\ B,\ C$ y $D$ respectivamente. Halle el costo de fabricar 200 unidades de $A$, 450 de $B$, 635 de $C$, y 1230 de $D$.

7. Halle la función de utilidades de la empresa del punto anterior si se sabe que los productos $A,\ B,\ C$ y $D$ se pueden vender cada uno a $2\ ,3\ ,2$ y $6$ dólares cada uno.

# Derivadas parciales y su significado

Cuando calculamos la derivada parcial de una función encontramos las funciones marginales de la función dada.

Cuando cálculamos $f_x$ estamos encontrando la razón de cambio en la función $f$ si cambiamos el valor de $x$ a la siguiente unidad, mientras que todas las demás variables se mantienen iguales.

La interpretación es análoga para todas las derivadas parciales.

```python
x,y=symbols("x,y")
```

```python
f=x**2+2*x*y+3*y
f
```

```python
f.subs([(x,10), (y,15)])
```

```python
dfx=diff(f,x)
dfx
```

```python
dfx.subs([(x,10), (y,15)])
```

Significa que la función $f$ incrementa aproximadamente 50 unidades si aplicamos un cambio en $x$ de 10 a 11 unidades, mientras que $y$ se mantiene en 15.

```python
f.subs([(x,11), (y,15)])
```

```python
dfy=diff(f,y)
dfy
```

```python
dfy.subs([(x,10), (y,15)])
```

Significa que la función $f$ incrementa aproximadamente 23 unidades si aplicamos un cambio en $y$ de 15 a 16 unidades, mientras que $x$ se mantiene en 10.

```python
f.subs([(x,10), (y,16)])
```

## Ejercicio
La función de producción de cierta empresa está dada por
$$P=5L + 3L^2 + 4LK + 8K + 6K^2$$
en donde $L$ es el insumo mano de obra medido en miles de horas-hombre por semana, $K$ es el monto de capital invertido medido en miles de dólares por semana y P
es la producción semanal en miles de artículos. Determine las productividades marginales cuando $L=10$ y $K=15$ e interprete el resultado.

## Ejercicio

Se descubre que la utilidad por
acre de cierto cultivo de trigo es
$$P =40L + 5S + 20F - 3L^2 - S^ 2 - 2F^2 - 4SF$$
en donde L es el costo de la mano de obra, S es el costo de
la semilla y F es el costo del fertilizante.

Calcule $P_L$, $P_S$, $P_F$,
 y evalúelas cuando $L = 10$, $S = 3$ y $F = 4$.

Interprete estas derivadas.

# Relaciones de demanda

## Productos competitivos y complementarios

Antes supusimos
que la demanda de un artículo sólo depende del precio por unidad del artículo
particular. En la práctica, esto no siempre es cierto porque la demanda de un
artículo puede verse afectada por el precio de algún otro artículo relacionado.

### Ejemplo

Suponga que hay dos marcas de celulares, marca A y marca B, las cuales son las líderes a nivel mundial en celulares de alta gama.

¿Qué sucede con la demanda de la marca A, si ella aumenta el precio de sus celulares?

*R:* La demanda debería disminuir. Lo mismo ocurriría para la marca B.

Este hecho se puede entender matemáticamente como que la derivada de la demanda de A respecto al precio de A es negativa, lo mismo para el producto de la marca B:

$$\frac{\partial X_A}{\partial P_A}<0;\ \ \ \frac{\partial X_B}{\partial P_B}<0$$

Esto es normal que suceda con cualquier producto.

¿Qué sucede con la demanda de la marca A si B aumenta su precio?

*R:* La demanda de A debería aumentar cuando B aumente su precio. De manera análoga sucedería con la demanda de B si A aumenta su precio.

$$\frac{\partial X_A}{\partial P_B}>0;\ \ \ \frac{\partial X_B}{\partial P_A}>0$$

Cuando esto sucede se dice que los productos son **COMPETITIVOS**.

### Ejemplo

Suponga que se tienen dos productos, A el cual es un celular y B el forro protector del mismo.

¿Qué sucede con la demanda de los forros si el precio del celular aumenta?

*R:* Al aumentar el precio de A (los celulares), la demanda de A  disminuye y  por lo tanto la demanda de B (los forros) disminuye, es decir

$$\frac{\partial X_A}{\partial P_B}<0;\ \ \ \frac{\partial X_B}{\partial P_A}<0$$

Cuando esto sucede los productos se dicen **COMPLEMENTARIOS**

## Ejemplo
Las demandas $x_A$ y $x_B$ de los productos $A$ y $B$ están dadas por las funciones

$$x_A= 300 + 5p_B - 7p_A^2;\ \ \ x_B = 250 - 9p_B + 2p_A$$

en donde $p_A$ y $p_B$ son los precios unitarios de $A$ y $B$, respectivamente.

Determine las cuatro funciones de demanda marginal e investigue si los productos $A$ y $B$ son competitivos o complementarios entre sí.

## Ejercicio

Las demandas $x_A$ y $x_B$ de los productos $A$ y $B$ están dadas por las funciones

$$x_A= 150 - 0.3p^2_B - 2p^2_A;\ \ \ x_B = 200 - 0.2p^2_A - 3p^2_B$$

en donde $p_A$ y $p_B$ son los precios unitarios de $A$ y $B$, respectivamente.

Determine las cuatro funciones de demanda marginal e investigue si los productos $A$ y $B$ son competitivos o complementarios entre sí.

## Elasticidad

La elasticidad de la demanda mide que tan sensible es un producto repecto al cambio en el precio.

Si la demanda del producto $A$ depende de los precios $P_A$ y $P_B$ ésta se puede derivar respecto a cualquiera de las dos varibles, por lo que es posible calcular dos razones de cambio porcentuales:

1. **Elasticidad de la demanda de A** la cual se define como:
$$\eta_{p_A}=\frac{\ \frac{\partial x_A}{\partial p_A}\ }{\frac{x_A}{p_A}}=\ \frac{\partial x_A}{\partial p_A}\frac{p_A}{x_A}$$

2. **Elasticidad de la demanda cruzada de A** la cual se puede ver como:
$$\eta_{p_B}=\frac{\ \frac{\partial x_A}{\partial p_B}\ }{\frac{x_A}{p_B}}=\ \frac{\partial x_A}{\partial p_B}\frac{p_B}{x_A}$$

Cuando:
* $|\eta_{p_A}|>1$ el producto se dice elástico.
* $|\eta_{p_A}|<1$ se conoce como inelástico.
*  $|\eta_{p_A}|=1$ la demanda se dice unitaria.

Por ejemplo, si $\eta_{p_A}=-3$ se interpreta como que un aumento del $1\%$ en el precio implica una caída del $3\%$ en las ventas del producto.

## Ejemplo
La función de demanda del producto A está dada por
$$x_A = 250 +0.3p_B - 5p^2_A$$

Determine $\eta_{p_A}$ y $\eta_{p_B}$ cuando $p_A = 6$ y $p_B = 50$.

```python
pA,pB = symbols("pA,pB")
```

```python
xA=250+0.3*pB-5*pA**2
xA
```

```python
dxApA=diff(xA,pA)
dxApA
```

```python
dxApB=diff(xA,pB)
dxApB
```

**Elasticidad de la demanda de A** la cual se define como:
$$\eta_{p_A}=\frac{\ \frac{\partial x_A}{\partial p_A}\ }{\frac{x_A}{p_A}}=\ \frac{\partial x_A}{\partial p_A}\frac{p_A}{x_A}$$

```python
npA=(dxApA)/(xA/pA)
npA
```

```python
npA.subs([(pA, 6),(pB, 50)])
```

Podemos decir que un incremento aproximado del 1% en el precio de A
provocará una caída del 4.24% en la demanda de este producto.

**Elasticidad de la demanda cruzada de A** la cual se puede ver como:
$$\eta_{p_B}=\frac{\ \frac{\partial x_A}{\partial p_B}\ }{\frac{x_A}{p_B}}=\ \frac{\partial x_A}{\partial p_B}\frac{p_B}{x_A}$$

```python
npB=(dxApB)/(xA/pB)
npB
```

```python
npB.subs([(pA, 6),(pB, 50)])
```

Un incremento
del 1% en el precio de B da como resultado un aumento del 0.176% en la
demanda de A.

## Ejercicio

En el caso de las siguientes
funciones de demanda del producto A, determine $\eta_{p_A}$ y $\eta_{p_B}$
en los niveles de precio dados para los dos productos relacionados
A y B.

1. $x_A = 250 + 0.3p_B + 2p^2_A; p_A = 5, p_B =40$
2. $x_A = \cfrac{60p_B}{\sqrt{p_A}}; p_A = 9, p_B =2$

En cada caso interprete sus resultados.

Aplicaciones de las derivadas parciales.ipynb


```python
from geoscilabs.dcip import DCLayers
from IPython.display import display
%matplotlib inline
```

```python
from matplotlib import rcParams
rcParams['font.size'] = 14
```

# Purpose

## Investigating DC Resistivity Data

Using the widgets contained in this notebook we will explore the physical principals governing DC resistivity including the behavior of currents, electric field, electric potentials in a two layer earth.

The measured data in a DC experiment are potential differences, we will demonstrate how these  provide information about subsurface physical properties.

### Background: Computing Apparent Resistivity

In practice we cannot measure the potentials everywhere, we are limited to those locations where we place electrodes. For each source (current electrode pair) many potential differences are measured between M and N electrode pairs to characterize the overall distribution of potentials. The widget below allows you to visualize the potentials, electric fields, and current densities from a dipole source in a simple model with 2 layers. For different electrode configurations you can measure the potential differences and see the calculated apparent resistivities.

In a uniform halfspace the potential differences can be computed by summing up the potentials at each measurement point from the different current sources based on the following equations:

\begin{align}
    V_M = \frac{\rho I}{2 \pi} \left[ \frac{1}{AM} - \frac{1}{MB} \right] \\
    V_N = \frac{\rho I}{2 \pi} \left[ \frac{1}{AN} - \frac{1}{NB} \right]
\end{align}
where $AM$, $MB$, $AN$, and $NB$ are the distances between the corresponding electrodes.

The potential difference $\Delta V_{MN}$ in a dipole-dipole survey can therefore be expressed as follows,

\begin{equation}
    \Delta V_{MN} = V_M - V_N = \rho I \underbrace{\frac{1}{2 \pi} \left[ \frac{1}{AM} - \frac{1}{MB} - \frac{1}{AN} + \frac{1}{NB} \right]}_{G}
\end{equation}

and the resistivity of the halfspace $\rho$ is equal to,

$$
    \rho = \frac{\Delta V_{MN}}{IG}
$$

In this equation $G$ is often referred to as the geometric factor.

In the case where we are not in a uniform halfspace the above equation is used to compute the apparent resistivity ($\rho_a$) which is the resistivity of the uniform halfspace which best reproduces the measured potential difference.

In the top plot the location of the A electrode is marked by the red +, the B electrode is marked by the blue -, and the M/N potential electrodes are marked by the black dots. The $V_M$ and $V_N$ potentials are printed just above and to the right of the black dots. The calculted apparent resistivity is shown in the grey box to the right. The bottom plot can show the resistivity model, the electric fields (e), potentials, or current densities (j) depending on which toggle button is selected. Some patience may be required for the plots to update after parameters have been changed.

# LayeredEarth app

## Parameters:

 - **A**: (+) Current electrode  location
 - **B**: (-) Current electrode  location
 - **M**: (+) Potential electrode  location
 - **N**: (-) Potential electrode  location
 - **$\rho_1$**: Resistivity of the first layer
 - **$\rho_2$**: Resistivity of the second layer
 - **h**: Thickness of the first layer
 - **Plot**: Choice of 2D plot (Model, Potential, Electric field, Currents)

```python
out = DCLayers.plot_layer_potentials_app()
display(out)
```

    MyApp(children=(FloatSlider(value=-30.0, continuous_update=False, description='A', max=40.0, min=-40.0, step=1…

```python

```


# Blackbody

A [blackbody](http://en.wikipedia.org/wiki/Black_body) or *planckian radiator* is an ideal thermal radiator that absorbs completely all incident radiation, whatever the wavelength, the direction of incidence or the polarization. [1]

A *blackbody* in [thermal equilibrium](http://en.wikipedia.org/wiki/Thermal_equilibrium) emits electromagnetic radiation called [blackbody radiation](http://en.wikipedia.org/wiki/Black-body_radiation).

## Planck's Law

The spectral radiance of a blackbody at thermodynamic temperature $T [K]$ in a medium having index of refraction $n$ is given by the [Planck's law](http://en.wikipedia.org/wiki/Planck%27s_law) equation: [2]

$$
\begin{equation}
L_{e\lambda}(\lambda,T)=\cfrac{C_1n^{-2}\lambda^{-5}}{\pi}{\Biggl[\exp\biggl(\cfrac{C_2}{n\lambda T}\biggr)-1\Biggr]^{-1}}
\end{equation}
$$
where
$$
\begin{equation}
\begin{aligned}
C_1&=2\pi hc^2\\
C_2&=\cfrac{hc}{k}
\end{aligned}
\end{equation}
$$

$h$ is Planck's constant, $c$ is the speed of light in vacuum, $k$ is the Boltzmann constant and $\lambda$ is the wavelength.

As per *CIE 015:2004 Colorimetry, 3rd Edition* recommendation $C_2$ value when used in colorimetry should be $C_2= 1,4388x10^{-2}mK$ as defined by the International Temperature Scale (ITS-90).

$C_1$ value is given by the Committee on Data for Science and Technology (CODATA) and should be $C_1=3,741771x10^{16}Wm^2$.

In the current *CIE 015:2004 Colorimetry, 3rd Edition* recommendation, colour temperature and correlated colour temperature are calculated with $n=1$.

[Colour](https://github.com/colour-science/colour/) implements various *blackbody* computation related objects in the `colour.colorimetry` sub-package:

```python
import colour.colorimetry
```

> Note: `colour.colorimetry` package public API is also available from the `colour` namespace.

The *Planck's law* is called using either the `colour.planck_law` or `colour.blackbody_spectral_radiance` definitions, they are expecting the wavelength $\lambda$ to be given in nanometers and the temperature $T$ to be given in degree kelvin:

```python
import colour

colour.colorimetry.planck_law(500 * 1e-9, 5500)
```

    20472701909806.578

Generating the spectral distribution of a *blackbody* is done using the `colour.sd_blackbody` definition:

```python
with colour.utilities.suppress_warnings(python_warnings=True):
    colour.sd_blackbody(6500, colour.SpectralShape(0, 10000, 10))
```

With its temperature lowering, the blackbody peak shifts to longer wavelengths while its intensity decreases:

```python
from colour.plotting import *
```

```python
colour_style();
```

```python
# Plotting various *blackbodies* spectral distributions.
blackbodies_sds = [colour.sd_blackbody(i, colour.SpectralShape(0, 10000, 10))
                   for i in range(1000, 15000, 1000)]
with colour.utilities.suppress_warnings(python_warnings=True):
    plot_multi_sds(blackbodies_sds,
                   y_label='W / (sr m$^2$) / m',
                   use_sds_colours=True,
                   normalise_sds_colours=True,
                   legend_location='upper right',
                   bounding_box=[0, 1000, 0, 2.25e15]);
```

Let's plot the blackbody colours from temperature in domain [150, 12500, 50]:

```python
plot_blackbody_colours(colour.SpectralShape(500, 12500, 50));
```

## Stars Colour

Let's compare the extraterrestrial solar spectral irradiance to the blackbody spectral radiance of a thermal radiator with a temperature of 5778 K:

```python
# Comparing theoretical and measured *Sun* spectral distributions.
# Arbitrary ASTMG173_ETR scaling factor calculated with
# :def:`colour.sd_to_XYZ` definition.
ASTMG173_sd = ASTMG173_ETR.copy() * 1.37905559e+13

blackbody_sd = colour.sd_blackbody(
    5778,
    ASTMG173_sd.shape)
blackbody_sd.name = 'The Sun - 5778K'

plot_multi_sds([ASTMG173_sd, blackbody_sd],
               y_label='W / (sr m$^2$) / m',
               legend_location='upper right');
```

As you can see the *Sun* spectral distribution is very close to the one from a blackbody at similar temperature $T$.

Calculating theoritical colour of any star is possible, for example the [VY Canis Majoris](http://en.wikipedia.org/wiki/VY_Canis_Majoris) red hypergiant in the constellation *Canis Major*.

```python
plot_blackbody_spectral_radiance(temperature=3500, blackbody='VY Canis Majoris');
```

Or [Rigel](http://en.wikipedia.org/wiki/Rigel) the brightest star in the constellation *Orion* and the seventh brightest star in the night sky.

```python
plot_blackbody_spectral_radiance(temperature=12130, blackbody='Rigel');
```

And finally the [Sun](http://en.wikipedia.org/wiki/Sun), our star:

```python
plot_blackbody_spectral_radiance(temperature=5778, blackbody='The Sun');
```

## Bibliography

1. ^ CIE. (n.d.). 17-960 Planckian radiator. Retrieved June 26, 2014, from http://eilv.cie.co.at/term/960
2. ^ CIE TC 1-48. (2004). APPENDIX E. INFORMATION ON THE USE OF PLANCK’S EQUATION FOR STANDARD AIR. In *CIE 015:2004 Colorimetry, 3rd Edition* (pp. 77–82). ISBN:978-3-901-90633-6


[Task Scheduler](https://leetcode.com/problems/task-scheduler/)。给一大写字母的数组，代表$26$种任务的集合。CPU任一时间片可以挑选任一任务去执行，但是同种任务的两次执行之间需要有$n$个时间片的间隔。问执行完所有任务最少需要多少时间片。如```tasks = ["A","A","A","B","B","B"]```，```n = 2```，执行时间片为：```A -> B -> idle -> A -> B -> idle -> A -> B```。

思路：首先对同种任务进行计数，同种任务之间的执行至少要间隔$n$，那么易得最短时间取决于数量最多的那种任务。易得完成$k$个同种任务至少需要$(k-1)*(n+1)+1$个时间片，若数量最多的任务有$s$种，那么至少需要$(k-1)*(n+1)+s$个时间片。

```python
def leastInterval(tasks, n: int) -> int:
    res = len(tasks)

    cnts = [0]*26
    for ch in tasks:
        cnts[ord(ch)-65] += 1
    cnts.sort(reverse=True)

    neet_t = (cnts[0]-1)*(n+1)
    for cnt in cnts:
        if cnt == cnts[0]:
            neet_t += 1

    return max(res, neet_t)
```

    8

[Fraction to Recurring Decimal](https://leetcode.com/problems/fraction-to-recurring-decimal/)。将分数转换成小数形式，当出现循环小数时，将循环部分加上括号。

思路：首先明确分数中分子为除数，分母为被除数。手动做一下除法就不难发现，当某一次的余数在之前出现过时，肯定就会出现循环小数，所以该题解法是先正常的做除法，并记录每一次余数出现的位置，当余数重复时即出现循环小数。

```python
def fractionToDecimal(numerator: int, denominator: int) -> str:
    res = '-' if numerator*denominator < 0 else ''    # 符号
    numerator, denominator = abs(numerator), abs(denominator)
    integer, remainder = divmod(numerator, denominator)    # 首个商与余数
    res += str(integer)

    if remainder:
        res += '.'
    else:
        return res

    remainder_idx = dict()    # 记录余数位置的字典
    while remainder:
        if remainder not in remainder_idx:
            remainder_idx[remainder] = len(res)
            integer, remainder = divmod(remainder*10, denominator)
            res += str(integer)
        else:
            idx = remainder_idx[remainder]
            res = '{}({})'.format(res[:idx], res[idx:])
            break

    return res
```

求倒数第$Q$个排列，**2019网易笔试题**。给$n$个自然数，然后指定其中一个排列，假设选中的是第$Q$个排列，求倒数第$Q$个排列。如$n=3$，选中排列$123$，它是第$1$个排列，那么要求倒数第$i$个排列$321$。

思路：找规律，第$Q$个排列和倒数第$Q$个排列，对应位的数字相加恰好等于$n+1$。

```python
def func(n, num):
    num = list(str(num))
    res = list()

    for bit in num:
        res.append(n+1-int(bit))

    return int(''.join(map(str, res)))
```

    321

[Beautiful Arrangement II](https://leetcode.com/problems/beautiful-arrangement-ii/)。给$n$个正整数，重排这些正整数，使得任意两相邻值的绝对差值只有$k$种取值。如$[1,2,3]$只有一种绝对差值$1$。

思路：找规律。易得```func(4,1)=[1,2,3,4]```，```func(4,2)=[4,1,2,3]```，```func(4,3)=[1,4,2,3]```。发现交替放置$k$个大小值如```[1,4,3,2]```可以构成$k-1$种差值，而令最后一种差值为$1$，顺序放置剩余的元素即可。在$[1,n]$区间设置首位双指针，若$k$为奇数，则放置$i$；若$k$为偶数则放置$j$。

```python
def constructArray(n: int, k: int):
    res = list()
    i, j = 1, n
    while i <= j:
        if k > 1:
            if k & 1:    # 奇数
                res.append(i)
                i += 1
            else:    # 偶数
                res.append(j)
                j -= 1
            k -= 1
        else:
            res.append(i)
            i += 1
    return res
```

    [6, 1, 5, 2, 3, 4]

[Number of Digit One](https://leetcode.com/problems/number-of-digit-one/)。求$[1,n]$范围内$1$出现的次数。

思路：直接取出最大值$n$，由低往高依次将每一位都置成$1$，然后判断高位与地位各有多少种取值。设由掩码$mask$提取出的数字是$idx$，有以下三种情况;
- $idx=0$，该位若要置$1$的话那么高位数字只能先减$1$，高位数字在$[0,high-1]$任取，低位数字任取，即$high\times{mask}$；
- $idx=1$，当高位数字在$[0,high-1]$时，低位数字任取，当高位数字取最大值时，低位只能在$[0,low]$里取值，即$high\times{mask}+low+1$；
- $idx>1$，高位数字可在$[0,high]$中任取，低位数字任取，即$(high+1)\times{mask}$。

```python
def countDigitOne(n: int) -> int:
    res = 0
    mask = 1

    while mask <= n:
        idx = n//mask % 10
        high = n//mask//10
        low = n % mask

        if idx == 0:
            res += high*mask
        elif idx == 1:
            res += high*mask+low+1
        else:
            res += (high+1)*mask

        mask *= 10

    return res

countDigitOne(11)
```

    4

[Nth Digit](https://leetcode.com/problems/nth-digit/)。给一自然数的顺序排序，求该字符串中的第$n$位数字。

思路：$1$位数有$9$个数字，$2$位数有$90$个数字，$3$位数有$900$个数字。按该规律$n$位数有$9\times{10}^{n-1}$个数字。

```python
def findNthDigit(n: int) -> int:
    if n < 10:
        return n

    bits = 2
    n -= 10

    while n > 9*(10**(bits-1))*bits:
        n -= 9*(10**(bits-1))*bits
        bits += 1

    start = 10**(bits-1)    # bits位数字的起点数字，1，10，100，...
    number = start+n//bits  # 第n位在哪一个数字中

    return str(number)[n % bits]
```

    1 2 10

    '0'

[Count Primes](https://leetcode.com/problems/count-primes/)。计数$n$以内的质数个数。

思路：设置一个长度为$n$的状态数组，以倍数法来标记非质数，然后对该数组求和即可。有一个数学定理，$n$以内的最大质数不会超过$\sqrt{n}$。

```python
def countPrimes(self, n: int) -> int:
    if n < 2:
        return 0

    import math
    thresh = int(math.sqrt(n))
    isPrime = [True]*n
    isPrime[0] = isPrime[1] = False    # 0,1不算质数

    for i in range(2, thresh+1):
        if isPrime[i]:
            isPrime[i*i:n:i] = [False]*len(isPrime[i*i:n:i])

    return sum(isPrime)
```

    4

[Roman to Integer](https://leetcode.com/problems/roman-to-integer/)。**2019老虎证券笔试题**。罗马数字转阿拉伯数字。

思路：找规律题。如$IV=4$而$VI=6$，若两罗马字符$ab$中$a\ge{b}$，说明$a$是正的，反之表明$a$是负的。

```python
def romanToInt(s: str) -> int:
    lookup = {'I': 1, 'V': 5, 'X': 10, 'L': 50,
              'C': 100, 'D': 500, 'M': 1000}

    res = 0
    for i in range(len(s)-1):
        if lookup[s[i]] < lookup[s[i+1]]:
            res -= lookup[s[i]]
        else:
            res += lookup[s[i]]

    return res+lookup[s[-1]]
```

    False

[Gray Code](https://leetcode.com/problems/gray-code/)。指定二进制的位数$n$，输出在该位数下的十进制格雷码序列。

思路：纯规律题。
$$
\begin{align}
n=0 \ & [0] \\
n=1 \ & [0,1] \\
n=2 \ & [00,01,11,10] \\
n=3 \ & [000,001,011,010,110,111,101,100] \\
\end{align}
$$
观察到每下一级的格雷码可以对半分成两份，前半部分与上一级数字相同，只是在高位加$0$；而后半部分则是上一级序列的逆序，然后再高位加$1$，相当于加$2^{(i-1)}$。

```python
def grayCode(n: int):
    res=[0]
    if n==0:
        return res

    for i in range(1,n+1):
        res.extend([item+2**(i-1) for item in res[::-1]])

    return res
```

    [0, 1, 3, 2]

[1-bit and 2-bit Characters](https://leetcode.com/problems/1-bit-and-2-bit-characters/)。现有两种特殊字符，第一种字符被编码成$0$，第二种字符编码成$10$和$11$。现给一编码串，判断该编码串的是否是以第一种字符结尾。

思路：两种编码有明显的区别，第一种字符编码以$0$开头，一位；第二种字符编码以$1$开头，两位。使用工作指针线性扫描字串，遇到$0$前进一位，遇到$1$前进两位，判断指针能否停在最后一位即可。

```python
def isOneBitCharacter(bits) -> bool:
    n = len(bits)
    idx = 0

    while idx < n-1:
        if bits[idx] == 0:
            idx += 1
        else:
            idx += 2

    return idx == n-1
```

    True

[Self Dividing Numbers](https://leetcode.com/problems/self-dividing-numbers/)。给定一个范围，求出范围内所有的SDN。SDN定义为能被自身中所有数字整除的数，并且SDN不允许包含0。

思路：逻辑实现题。

```python
def selfDividingNumbers(left: int, right: int):
    res=list()
    if left>right:
        return res

    for num in range(left,right+1):
        tmp=num

        while tmp:
            bit=tmp%10
            if bit==0 or num%bit!=0:
                break
            else:
                tmp//=10

        if tmp==0:
            res.append(num)

    return res
```

    [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 22]

判断点是否在多边形内。**2019去哪儿笔试题**。首先给出用户坐标，后跟的是多边形的点的坐标，保证顺序。判断该点是否在多边形内部。

思路：经典计算几何题，思路来源于[网上](https://www.cnblogs.com/anningwang/p/7581545.html)。由于笔试不允许使用Python，所以只测试了两个最简单的用例，不保证正确性。

```python
import sys

def func(x_u, y_u, xs, ys):
    res = False
    n = len(xs)
    i, j = 0, n - 1
    while i < n:
        if ((ys[i] > y_u) != (ys[j] > y_u)
                and (x_u < (xs[j] - xs[i]) * (y_u - ys[i]) / (ys[j] - ys[i]) + xs[i])):
            res = not res
        j = i
        i += 1

    return res

coord = sys.stdin.readline().strip().split()
x_u, y_u = list(map(float, coord[0].split(',')))
xs = list(map(float, [item.split(',')[0] for item in coord[1:]]))
ys = list(map(float, [item.split(',')[1] for item in coord[1:]]))

print(xs, ys, x_u, y_u)
print(func(x_u, y_u, xs, ys))
```


```python
from sympy.physics.mechanics import *
import sympy as sp
mechanics_printing(pretty_print=True)
```

```python
m1,m2,l01,l02,k1,k2 = sp.symbols(r'm_1,m_2,l_{0\,1},l_{0\,2},k_1,k_2')
t,g = sp.symbols('t,g')
x1, x2, y1, y2, z1, z2 = dynamicsymbols('x_1 x_2 y_1 y_2 z_1 z_2')
dx1, dx2, dy1, dy2, dz1, dz2 = dynamicsymbols('x_1 x_2 y_1 y_2 z_1 z_2', 1)
```

```python
V = m1*g*y1+m2*g*y2+sp.Rational(1,2)*k1*(sp.sqrt(x1**2+y1**2+z1**2)-l01)**2+sp.Rational(1,2)*k2*(sp.sqrt((x2-x1)**2+(y2-y1)**2+(z2-z1)**2)-l02)**2
T = sp.Rational(1,2)*m1*(dx1**2+dy1**2+dz1**2)+sp.Rational(1,2)*m2*(dx2**2+dy2**2+dz2**2)
L = sp.simplify(T - V)
```

```python
LM = LagrangesMethod(L, [x1, y1, z1, x2, y2, z2])
```

```python
soln = LM.form_lagranges_equations()
```

```python
solvedsoln = sp.solve((sp.Eq(soln[0]),sp.Eq(soln[1]),sp.Eq(soln[2]),sp.Eq(soln[3]),sp.Eq(soln[4]),sp.Eq(soln[5])),(x1.diff(t,t),y1.diff(t,t),z1.diff(t,t),x2.diff(t,t),y2.diff(t,t),z2.diff(t,t)))
```

```python
solvedsoln
```


# Qaintellect implementation of the Quantum Approximation Optimization Algorithm (QAOA)

In this example, we implement the optimization of a circuit based on the QAOA algorithm, demonstrated via the Max-Cut problem.

Reference:
- E. Farhi, J. Goldstone, S. Gutmann: "A Quantum Approximate Optimization Algorithm", [arXiv:1411.4028](http://arxiv.org/abs/1411.4028) (2014)

```julia
using Qaintessent
using Qaintellect
using LinearAlgebra
using Flux
using IterTools: ncycle
```

```julia
# visualization
using Plots
using LaTeXStrings
```

    ┌ Info: Precompiling Plots [91a5bcdd-55d7-5caf-9e0b-520d859cae80]
    └ @ Base loading.jl:1278
    ┌ Info: Precompiling LaTeXStrings [b964fa9f-0449-5b57-a5c2-d3ea65f4040f]
    └ @ Base loading.jl:1278

## Definition of the optimization problem

The target function
\begin{equation}
C(b) = \sum_{\alpha=1}^m C_{\alpha}(b) \quad \text{with} \quad C_{\alpha}(b) \in \{ 0, {\color{orange}{1}} \}
\end{equation}
should be maximized with respect to bit strings $b = b_{n-1} \dots b_1 b_0$ of length $n$.

We consider _Max-Cut_ as specific example: for a given graph with $n$ vertices and $m$ edges, the task is to partition its vertices into two subsets $\color{blue}{S_0}$ and $\color{green}{S_1}$, such that the number of edges between these subsets is maximized.

Expressed in terms of the above target function, for $j = 0, 1, \dots, n-1$, the entry $b_j \in \{ \color{blue}{0}, \color{green}{1} \}$ specifies whether the $j$-th vertex belongs to subset $\color{blue}{S_0}$ or $\color{green}{S_1}$. For the $\alpha$-th edge, we define $\mathsf{C}_{\alpha}$ as

```julia
# number of vertices
n = 5

# graph edges corresponding to the above graph
edges = [(0, 1), (1, 2), (1, 3), (2, 4), (3, 4)];
```

## Quantum formulation and construction of a quantum circuit

Term $C_{\alpha}$ of target function in quantum language:
\begin{equation}
C_{\alpha} = \frac{1}{2} (I - Z_j Z_k),
\end{equation}
with $j$ and $k$ the vertices corresponding to the $\alpha$-th edge in the graph. Each basis state $\lvert b \rangle$ is an eigenvector of $C_{\alpha}$ with eigenvalue $\mathsf{C}_{\alpha}(b)$.

Accordingly, the target function operator reads $C = \sum_{\alpha = 1}^m C_{\alpha}$ (which is diagonal with respect to the standard basis).

The QAOA Ansatz may be motivated by the [adiabatic theorem](https://en.wikipedia.org/wiki/Adiabatic_theorem), stating that a wavefunction remains an instantaneous eigenstate of a Hamiltonian $H(t)$, if $H(t)$ is slowly (adiabatically) changing with time $t$. Here we want to transition from a "simple", easy to construct eigenstate of some operator $B$, to the maximizing eigenstate of the target operator $C$. We setup the transition from $B$ to $C$ as $H(t) = \left(1 - \frac{t}{T}\right) B + \frac{t}{T} C$ for $t \in [0, T]$.

The operator $B$ is chosen as
\begin{equation*}
B = \sum_{j=0}^{n-1} X_j
\end{equation*}
with $X_j$ the Pauli-$X$ matrix acting on qubit $j$.

The largest eigenvalue of $B$ is $n$ (since each $X_j$ in the sum contributes the value $1$). The eigenvector of $B$ corresponding to its largest eigenvalue is the uniform superposition state:
\begin{equation}
\lvert s \rangle = \lvert + \rangle^{\otimes n} = \frac{1}{\sqrt{2^n}} \sum_{b=0}^{2^n - 1} \lvert b \rangle.
\end{equation}

```julia
# define uniform superposition state s
s_uni = fill(1/√(2^n) + 0.0im, 2^n);
```

According to the Schrödinger equation, a time step $\tau$ governed by a time-independent Hamiltonian $H$ is described by the unitary matrix $e^{-i \tau H}$. Even if $H$ slowly changes with time, $e^{-i \tau H(t)}$ is still a good approximation. Together with a splitting approximation for the matrix exponential, $e^{A + B} \approx e^A e^B$ for matrices $A$ and $B$ with small norm, we arrive at $e^{-i \tau H(t)} \approx e^{-i \tau (1 - \frac{t}{T}) B} \, e^{- i \tau \frac{t}{T} C}$ to describe a time step. This motivates the following Ansatz to find the maximizing eigenstate of $C$:
\begin{equation}
\lvert\psi_p(\gamma, \beta)\rangle = e^{-i \beta_p B} e^{-i \gamma_p C} \cdots e^{-i \beta_1 B} e^{-i \gamma_1 C} \lvert s \rangle
\end{equation}
with real parameters $\beta_1, \dots, \beta_p$ and $\gamma_1, \dots, \gamma_p$. The number of "layers" $p$ can be tuned depending on the application.

Note: we simulate the construction of $\lvert\psi_p(\gamma, \beta)\rangle$ here, but this would be performed on an actual quantum computer.

We can represent the unitary time step effected by $C_{\alpha}$ as $e^{-i \gamma C_{\alpha}} = e^{-i \gamma / 2} e^{i \gamma Z_j Z_k / 2}$. The global phase factor $e^{-i \gamma / 2}$ will drop out of the optimization, and the second term is the ZZ-"entanglement" gate.

```julia
function assemble_time_step_gates(p::Int, n::Int, edges::Vector{Tuple{Int,Int}})
    cgs = CircuitGate[]
    for _ in 1:p
        # C operator
        for e in edges
            # circuit gate uses 1-based indexing
            push!(cgs, circuit_gate(e[1] + 1, e[2] + 1, EntanglementZZGate(0.01*randn())))
        end
        # B operator
        for j in 1:n
           push!(cgs, circuit_gate(j, RxGate(0.01*randn())))
        end
    end
    return cgs
end
```

    assemble_time_step_gates (generic function with 1 method)

In practice, one performs parameter optimization to maximize the expectation value
\begin{equation}
\langle\psi_p(\gamma, \beta)\rvert C \lvert\psi_p(\gamma, \beta)\rangle,
\end{equation}
such that $\lvert\psi_p(\gamma, \beta)\rangle$ approaches the optimal solution $\lvert b^{\text{opt}} \rangle$ (or a superposition in case there are several optimizers). The preparation of $\lvert\psi_p(\gamma, \beta)\rangle$ and evaluation of the expectation value is to be performed on a quantum computer, and the parameter optimization on a classical computer. A standard measurement of $\lvert\psi_p(\gamma, \beta)\rangle$ then results in $b^{\text{opt}}$ with high probability.

```julia
# create measurement operator representing C
Cmat = zeros(2^n, 2^n)
for edge in edges
    k1 = circuit_gate(edge[1] + 1, ZGate())
    k2 = circuit_gate(edge[2] + 1, ZGate())
    Cmat += 0.5*(I - sparse_matrix([k1, k2], n))
end
@show(diag(Cmat))
Cop = MeasurementOperator(Cmat, Tuple(1:n));
```

    diag(Cmat) = Complex{Float64}[0.0 + 0.0im, 1.0 + 0.0im, 3.0 + 0.0im, 2.0 + 0.0im, 2.0 + 0.0im, 3.0 + 0.0im, 3.0 + 0.0im, 2.0 + 0.0im, 2.0 + 0.0im, 3.0 + 0.0im, 3.0 + 0.0im, 2.0 + 0.0im, 4.0 + 0.0im, 5.0 - 0.0im, 3.0 + 0.0im, 2.0 + 0.0im, 2.0 + 0.0im, 3.0 + 0.0im, 5.0 - 0.0im, 4.0 + 0.0im, 2.0 + 0.0im, 3.0 + 0.0im, 3.0 + 0.0im, 2.0 + 0.0im, 2.0 + 0.0im, 3.0 + 0.0im, 3.0 + 0.0im, 2.0 + 0.0im, 2.0 + 0.0im, 3.0 + 0.0im, 1.0 + 0.0im, 0.0 + 0.0im]

```julia
# example
circ = Circuit{n}(assemble_time_step_gates(2, n, edges), [Cop])
```

        5 ————————————————————□—————□————[Rx]———————————————□—————□————[Rx]—
                              |     |                       |     |
        4 ——————————————□———————————□————[Rx]—————————□———————————□————[Rx]—
                        |     |                       |     |
        3 ————————□———————————□————[Rx]—————————□———————————□————[Rx]———————
                  |     |                       |     |
        2 ——□—————□—————□——————————[Rx]———□—————□—————□——————————[Rx]———————
            |                             |
        1 ——□——————————————————————[Rx]———□——————————————————————[Rx]———————

```julia
# example: apply circuit gates and compute expectation value
apply(s_uni, circ)
```

    1-element Array{Float64,1}:
     2.499208652309536

## Perform optimization

```julia
# first attempt with p = 1

circ1 = Circuit{n}(assemble_time_step_gates(1, n, edges), [Cop])

# gather parameters from circuit
paras = Flux.params(circ1)

# there is not actually any input data for training
data = ncycle([()], 500)

# define optimizer
opt = Descent(0.5)

# define evaluation function
evalcb() = @show(apply(s_uni, circ1))

# perform minimization with the negated target function to achieve maximization
Flux.train!(() -> -apply(s_uni, circ1)[1], paras, data, opt, cb=Flux.throttle(evalcb, 0.5))
```

    apply(s_uni, circ1) = [2.5001403700650973]
    apply(s_uni, circ1) = [3.9999999999999973]
    apply(s_uni, circ1) = [4.0]

```julia
# corresponding optimized quantum wavefunction
ψ1 = apply(s_uni, circ1.moments);
```

It turns out that one layer is insufficient to unambiguously determine the optimal solution $b^{\text{opt}}$:

```julia
tags = [join(reverse(digits(i, pad=n, base=2))) for i in 0:2^n-1]
bar(tags, abs2.(ψ1), xticks=:all, xrotation=45, ylabel=L"|\psi|^2", legend=false)
```

```julia
# increase number of layers to p = 2

circ2 = Circuit{n}(assemble_time_step_gates(2, n, edges), [Cop])

# gather parameters from circuit
paras = Flux.params(circ2)

# there is not actually any input data for training
data = ncycle([()], 500)

# define optimizer
opt = Descent(0.5)

# define evaluation function
evalcb() = @show(apply(s_uni, circ2))

# perform minimization with the negated target function to achieve maximization
Flux.train!(() -> -apply(s_uni, circ2)[1], paras, data, opt, cb=Flux.throttle(evalcb, 0.5))
```

    apply(s_uni, circ2) = [2.5002312727553515]
    apply(s_uni, circ2) = [4.999920933578631]
    apply(s_uni, circ2) = [4.9999384211975295]
    apply(s_uni, circ2) = [4.999949989885429]
    apply(s_uni, circ2) = [4.9999587023966185]

```julia
# corresponding optimized quantum wavefunction
ψ2 = apply(s_uni, circ2.moments);
```

```julia
tags = [join(reverse(digits(i, pad=n, base=2))) for i in 0:2^n-1]
bar(tags, abs2.(ψ2), xticks=:all, xrotation=45, ylabel=L"|\psi|^2", legend=false)
```

Note that there are actually two optimal solutions, $b^{\text{opt}} = 01101$ and $b^{\text{opt}} = 10010$ (since one can swap the role of $\color{blue}{S_0}$ and $\color{green}{S_1}$), which indeed occur with the highest probability.


# Irregular elements and higher-order approximation

#### Imports

```python
import matplotlib.pyplot as plt;
import numpy as np;
import numpy.linalg as linalg;
import math;

from sympy import *;
from sympy.utilities import *;
from ipywidgets import *;
```

#### Constants

```python
# Symbol for symbolic expression
x = Symbol('x');

# Lagrange polynomial
def lagrange(p, i):
    width = 1.0/p;
    this = i * width;
    # Numerator
    num = 1;
    for j in range(0, p + 1):
        if(j != i):
            num = Mul(num, x - (j*width));
    # Denominator
    denom = 1;
    for j in range(0, p + 1):
        if(j != i):
            denom = Mul(denom, this - (j*width));
    denom = Pow(denom, -1);
    return Mul(num, denom);

degree = 4;
phi = [];
for i in range(0, degree + 1):
    phi += [lagrange(degree, i)];

# Constant
c = 1;

# Delta time
dt = 0.0001;

# Amount of iterations
iterations = 150000;

# Stepsize
stepSize = 1000;

# Time
t = 0;
```

#### Elements and nodes

```python
# Elements
elements = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5, 2.0];
elementCount = len(elements) - 1;

# Extremal nodes
minNode = min(elements);
maxNode = max(elements);

# Generate nodes
nodeCount = 1 + (degree * elementCount);

nodes = [];
for i in range(0, elementCount):
    elementWidth = elements[i + 1] - elements[i];
    partWidth = elementWidth/degree;
    for p in range(0, degree):
        nodes += [elements[i] + (partWidth * p)];
nodes += [maxNode];
```

#### Matrix construction

```python
# Time coefficient matrix
T = np.zeros((nodeCount, nodeCount));

def integ(phi1, phi2, h):
    product = Mul(phi1, phi2);
    return h*integrate(product, (x, 0, 1));

for i in range(0, elementCount):
    elementWidth = elements[i + 1] - elements[i];
    for n in range(0, degree + 1):
        for m in range(0, degree + 1):
            T[(degree * i) + n, (degree * i) + m] += integ(phi[n], phi[m], elementWidth);

for i in range(0,nodeCount):
    T[0,i]           = 0;
    T[nodeCount-1,i] = 0;
T[0,0] = 1;
T[nodeCount-1,nodeCount-1] = 1;

#Time coefficient matrix

S = np.zeros((nodeCount, nodeCount));

def diffInteg(phi1, phi2, h, trans, invtrans):
    # Transform local to global
    gPhi1 = phi1.subs(x, invtrans);
    gPhi2 = phi2.subs(x, invtrans);
    # Take global derivative
    derGPhi1 = (diff(gPhi1, x));
    derGPhi2 = (diff(gPhi2, x));
    # Transform global to local
    derPhi1 = derGPhi1.subs(x, trans);
    derPhi2 = derGPhi2.subs(x, trans);
    product = Mul(derPhi1, derPhi2);
    return h*integrate(product, (x,0,1));

for i in range(0, elementCount):
    elementWidth = elements[i + 1] - elements[i];
    transform = (elementWidth * x) + (elements[i]);
    inversetransform = (x - elements[i])/elementWidth;
    for n in range(0, degree + 1):
        for m in range(0, degree + 1):
            S[(degree * i) + n, (degree * i) + m] += diffInteg(phi[n], phi[m], elementWidth, transform, inversetransform);

for i in range(0,nodeCount):
    S[0,i]           = 0;
    S[nodeCount-1,i] = 0;
```

#### Iteration and evaluation

```python
# A single time step
def iteration(v, vDer):
    vNew = v + dt*vDer;
    q = -c*c*S@v;
    r = linalg.solve(T, q);
    vDerNew = vDer + dt*r;
    return (vNew, vDerNew);

# Evaluation
def ev(u, pos):
    # Find element the pos(ition) is in
    for i in range(0, elementCount):
        if(elements[i] <= pos <= elements[i+1]):
            elementWidth = elements[i + 1] - elements[i];
            transformedPos = (pos - elements[i])/elementWidth;
            # Calculate linear sum
            sum = 0;
            for p in range(0, degree+1):
                sum += u[(i*degree) + p]*phi[p].subs(x, transformedPos);
            return sum;
    return 0;
```

#### Initial value

```python
# The real solution
def realU(x, t):
    return np.cos(2*np.pi*t)*np.sin(2*np.pi*x) + np.cos(4*np.pi*t)*np.sin(4*np.pi*x) ;

# Set initial value
u    = np.zeros((nodeCount, 1));
uDer = np.zeros((nodeCount, 1));

k = 2*math.pi;

for i in range(0, nodeCount):
    coord   = nodes[i];
    u[i]    = realU(coord, 0);
    uDer[i] = 0;
```

#### Interactive Simulation

```python
data = [];
data.append(u);
bigspacing = np.linspace(0.0, 2.0, 100);

uNew    = np.zeros((nodeCount, 1));
uDerNew = np.zeros((nodeCount, 1));

for i in range(0, iterations):
    uNew, uDerNew = iteration(u,uDer);
    u = uNew;
    uDer = uDerNew;
    data.append(u);
    t += dt;

def animation(t):
    plt.rcParams["figure.figsize"] = (6,4);
    axes = plt.gca();
    axes.set_ylim([-1.5,1.5]);
    axes.set_xlim([0,2]);
    plt.title("t = " + str(t*dt));
    plt.ylabel("u(x,t)");
    plt.xlabel("x");
    evaluation = np.linspace(0.0, 2.0, 100);
    for i in range(0, 100):
        evaluation[i] = ev(data[t], evaluation[i]);
    plt.plot(bigspacing, realU(bigspacing, t*dt), "r--");
    plt.plot(bigspacing, evaluation, "b-");
    plt.plot(nodes, data[t], "bo");
    plt.plot(elements, np.zeros((len(elements),1)), "go");
    plt.plot([0,2],[0,0], "g-", linewidth=0.5);

interact(animation, t = widgets.IntSlider(min = 0, max = iterations, step = stepSize, value = 0));
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

#### Static plots

```python
plt.rcParams["figure.figsize"] = (6,12)

timespace = [0, (int)(2.0/(3.0*dt)), (int)(5.0/(3.0*dt)), (int)(3.0/dt), (int)(10.25/dt)];

print(timespace);

maxY = 2;
minY = -2;

plt.subplots_adjust(hspace=.0);

ax1 = plt.subplot("511");
ax1.set_ylim([minY, maxY]);
ax1.set_xlim([0,2]);
plt.plot(bigspacing, realU(bigspacing, timespace[0]*dt), "r--", label='actual');
evaluation = np.linspace(0.0, 2.0, 100);
for i in range(0, 100):
    evaluation[i] = ev(data[timespace[0]], evaluation[i]);
plt.plot(bigspacing, evaluation, "b-", label="numerical");
plt.plot(nodes, data[timespace[0]], "bo", label="nodes");
plt.plot(elements, np.zeros(len(elements)), "go", label="element boundaries");
plt.text(-0.55,0,"t="+str(dt*timespace[0]));
plt.text(-0.25,0.1,"u(x,t)", rotation=90);
plt.text(0.975, 1.45, "x");
plt.legend(bbox_to_anchor=(0.5, 1.2, 0., 0.1), loc=3,
       ncol=2, mode="expand", borderaxespad=0.)
ax1.xaxis.tick_top();

ax2 = plt.subplot("512", sharex=ax1);
ax2.set_ylim([minY, maxY]);
ax2.set_xlim([0,2]);
plt.plot(bigspacing, realU(bigspacing, timespace[1]*dt), "r--");
evaluation = np.linspace(0.0, 2.0, 100);
for i in range(0, 100):
    evaluation[i] = ev(data[timespace[1]], evaluation[i]);
plt.plot(bigspacing, evaluati on, "b-", label="numerical");
plt.plot(nodes, data[timespace[1]], "bo");
plt.text(-0.55,0,"t=0.6666");#+str(dt*timespace[1]));
plt.text(-0.25,0.1,"u(x,t)", rotation=90);
plt.setp(ax2.get_xticklabels(), visible=False);

ax3 = plt.subplot("513", sharex=ax1);
ax3.set_ylim([minY, maxY]);
ax3.set_xlim([0,2]);
plt.plot(bigspacing, realU(bigspacing, timespace[2]*dt), "r--");
evaluation = np.linspace(0.0, 2.0, 100);
for i in range(0, 100):
    evaluation[i] = ev(data[timespace[2]], evaluation[i]);
plt.plot(bigspacing, evaluation, "b-", label="numerical");
plt.plot(nodes, data[timespace[2]], "bo");
plt.text(-0.55,0,"t="+str(dt*timespace[2]));
plt.text(-0.25,0.1,"u(x,t)", rotation=90);
plt.setp(ax3.get_xticklabels(), visible=False);

ax4 = plt.subplot("514", sharex=ax1);
ax4.set_ylim([minY, maxY]);
ax4.set_xlim([0,2]);
plt.plot(bigspacing, realU(bigspacing, timespace[3]*dt), "r--");
evaluation = np.linspace(0.0, 2.0, 100);
for i in range(0, 100):
    evaluation[i] = ev(data[timespace[3]], evaluation[i]);
plt.plot(bigspacing, evaluation, "b-", label="numerical");
plt.plot(nodes, data[timespace[3]], "bo");
plt.text(-0.55,0,"t="+str(dt*timespace[3]));
plt.text(-0.25,0.1,"u(x,t)", rotation=90);
plt.setp(ax4.get_xticklabels(), visible=False);

ax5 = plt.subplot("515", sharex=ax1);
ax5.set_ylim([minY, maxY]);
ax5.set_xlim([0,2]);
plt.plot(bigspacing, realU(bigspacing, timespace[4]*dt), "r--");
evaluation = np.linspace(0.0, 2.0, 100);
for i in range(0, 100):
    evaluation[i] = ev(data[timespace[4]], evaluation[i]);
plt.plot(bigspacing, evaluation, "b-", label="numerical");
plt.text(-0.55,0,"t="+str(dt*timespace[4]));
plt.text(-0.25,0.1,"u(x,t)", rotation=90);
plt.setp(ax5.get_xticklabels(), visible=False);
```

```python

```


# CSE 330 Numerical Analysis Lab

### Lab 6: Polynomial Root Finding

### Instructions
---

In this week's assignment we will try to find out roots of a polynomial.
1. Finding root via binomial method.
2. Finding root via Fixed point (Newton method).

### `pandas` Dataframe:
Before we start we will know a little about `pandas` dataframe. `pandas` is a python library. Dataframes are classes that are used to store complex data. You can initialize one as a python dictionary. Let's take a look.

```
import pandas as pd

x = [0, 1, 2, 3, 4, 5, 6, 7]
y = [1.0, 5.2, 3.9, 8.4, 14.6, 12.3, 8.9, 10.2]

dictionary = {
    "x": x,
    "y": y,
}
df = pd.DataFrame(dictionary)
print(df)
```

       x     y
    0  0   1.0
    1  1   5.2
    2  2   3.9
    3  3   8.4
    4  4  14.6
    5  5  12.3
    6  6   8.9
    7  7  10.2

We can use it to print data in a tabular format. We can even use more `numpy` arrays with it.

```
import numpy as np

x = np.linspace(0, np.pi/2, 15)
y = np.sin(x)

dictionary = {
    "x": x,
    "sin(x)": y,
}
df = pd.DataFrame(dictionary)
print(df)
```

               x    sin(x)
    0   0.000000  0.000000
    1   0.112200  0.111964
    2   0.224399  0.222521
    3   0.336599  0.330279
    4   0.448799  0.433884
    5   0.560999  0.532032
    6   0.673198  0.623490
    7   0.785398  0.707107
    8   0.897598  0.781831
    9   1.009798  0.846724
    10  1.121997  0.900969
    11  1.234197  0.943883
    12  1.346397  0.974928
    13  1.458597  0.993712
    14  1.570796  1.000000

### Root Finding:
Let $f(x)$ be a function of $x$. if for some $x=\alpha, f(x) = f(\alpha) = 0$, we say $\alpha$ is a root of function $x$.

Let,
$$f(x) = x^5 + 2.5x^4 - 2x^3 -6x^2 + x + 2\tag{6.1}$$

The graph of $f(x)$ looks like this.

```
from numpy.polynomial import Polynomial
import matplotlib.pyplot as plt

f = Polynomial([2.0, 1.0, -6.0, -2.0, 2.5, 1.0])
x = np.linspace(-2.5, 1.6, 100)
y = f(x)
dictionary = {
    'x': x,
    'y': y
}
plt.axhline(y=0, color='k')
plt.plot(x, y)
plt.plot(f.roots(), f(f.roots()), 'ro')
print(f.roots())
```

Roots are the points where the graph intersects with the $X$-axis.

### Bisection

One way to find out root's are to use bisection method. Here is the strategy, if $\alpha$ is a root between and interval $[a, b]$ then graph will cross the $X$-axis at $\alpha$. So, $sign( f(\alpha-h) ) = - sign( f(\alpha+h) )$, for small value of $h$.

So, we can work our way up towards the root by taking average of $a$ and $b$, as long as the signs are different.

we will start with $a_0$ and $b_0$, such that, $f(a_0) f(b_0) < 0$.
Then we iterate as this,
\begin{align}
m_k &= \frac{a_k + b_k}{2} \\
\text{if, } f(a_k) f(m_k) < 0, \text{ then, } a_{k+1} &= a_k \text{ and } b_{k+1} = m_k\\
\text{else, } a_{k+1} &= m_k \text{ and, } b_{k+1} = b_k
\end{align}

We keep iterating until we find the root with sufficient precision. We usually use a formula like this,
$$
\frac{|m_{k+1} - m_k|}{|m_{k+1}|} \leq \epsilon \\
$$
Where, $\epsilon$ is a very small value, like $\epsilon < 10^{-6}$

### A. Complete the code below
Complete the code below to iterate and solve for a root of $Eq. 6.1$, between the interval, $[-0.5, 1.3]$.

```
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from numpy.polynomial import Polynomial

f = Polynomial([2.0, 1.0, -6.0, -2.0, 2.5, 1.0])
a = -0.5
b = 1.3
m = (a+b)/2
e = 1e-6

roots_in_range = list(filter(lambda x: x >= a and x <= b, f.roots()))
print(f'Actual root(s): {roots_in_range} in range [{a}, {b}]')

list_a = []
list_f_a = []
list_b = []
list_f_b = []
list_m = []
list_f_m = []

root = None

# +----------------------+
# | Start of your code   +
# +----------------------+

epsilon = e + 1  # tricking the machine to start the iteration
prev_m = None  # backup variable to store previus m value
# in order to calculate epsilon later

while True:
    if prev_m is not None:
        if abs((m-prev_m)/m) >= e:
            epsilon = abs((m-prev_m)/m)  # prev_m = list_m[k-1]
        else:
            break
    prev_m = m

    list_a.append(a)  # a = list_a[k]
    list_b.append(b)  # b = list_b[k]
    list_m.append(m)  # m = list_m[k]

    f_a = f(a)
    f_b = f(b)
    f_m = f(m)

    list_f_a.append(f_a)
    list_f_b.append(f_b)
    list_f_m.append(f_m)  # f_m = listf[k]

    if f_a * f_m < 0:
        b = m
    else:
        a = m
    m = (a + b) / 2

root = prev_m  # current m value is overfitting

# +--------------------+
# | End of your code   +
# +--------------------+

print()

#  print(pd.DataFrame({'a':list_a, 'b':list_b, 'm':list_m, 'f(x)':list_f_m}))
df = {'a': list_a, 'f(a)': list_f_a,
      'b': list_b, 'f(b)': list_f_b,
      'm': list_m, 'f(m)': list_f_m}
print(pd.DataFrame(df))
print(f'\nRoot:\t\t{root}\nEpsilon:\t{epsilon}')

xs = np.linspace(-2.5, 1.6, 100)
ys = f(xs)

plt.axhline(y=0, color='k', linewidth=1.2)
plt.plot(xs, ys)
plt.plot(root, f(root), 'ro')
plt.show()
```

### Fixed point iteration

#### Fixed point
A number $\xi$ is called a **fixed point** to function $g(x)$ if $g(\xi) = \xi$. Using fixed points are a nice strategy to find roots of an equation. In this method if we are trying to find a root of $f(x) = 0$, we try to write the function in the form, $x = g(x)$. That is,
$$
f(x) = x - g(x) = 0
$$

So, if $\xi$ is a fixed point of $g(x)$ it would also be a root of $f(x)=0$, because,
$$
f(\xi) = \xi - g(\xi) = \xi - \xi = 0
$$

We can find a suitable $g(x)$ in any number of ways. Not all of them would converge, whereas, Some would converge very fast. For example, consider $Eq. 6.1$.

\begin{align}
&         &    f(x) &=x^5 + 2.5x^4 - 2x^3 -6x^2 + x + 2 \\
&\implies &x - g(x) &=x^5 + 2.5x^4 - 2x^3 -6x^2 + x + 2 \\
&\implies &    g(x) &=-x^5 - 2.5x^4 + 2x^3 + 6x^2 - 2 \tag{6.2}\\
\end{align}

again,
$$
f(x) = x^5 + 2.5x^4 - 2x^3 -6x^2 + x + 2 = 0\\
$$
\begin{align}
&\implies &6x^2 &= x^5 + 2.5x^4 - 2x^3 + x + 2 \\
&\implies &x^2  &= \frac{1}{6}(x^5 + 2.5x^4 - 2x^3 + x + 2)\\
&\implies &x    &= \sqrt{\frac{1}{6}(x^5 + 2.5x^4 - 2x^3 + x + 2)}\\
&\implies &g(x) &= \sqrt{\frac{1}{6}(x^5 + 2.5x^4 - 2x^3 + x + 2)}\tag{6.3}\\
\end{align}

Similarly,
\begin{align}
&         &2.5x^4 &= -x^5 + 2x^3 + 6x^2 - x - 2 \\
&\implies &x^4  &= \frac{1}{2.5}(-x^5 + 2x^3 + 6x^2 - x - 2)\\
&\implies &x    &= \sqrt[\leftroot{-2}\uproot{2}4]{\frac{1}{2.5}(-x^5 + 2x^3 + 6x^2 - x - 2)}\\
&\implies &g(x) &= \sqrt[\leftroot{-2}\uproot{2}4]{\frac{1}{2.5}(-x^5 + 2x^3 + 6x^2 - x - 2)}\tag{6.4}\\
\end{align}

### B. Complete the code below
For this example we will use a couple of $g(x)$ function to find out which one converges faster.

```
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from numpy.polynomial import Polynomial

f = Polynomial([2.0, 1.0, -6.0, -2.0, 2.5, 1.0])
g1 = Polynomial([-2.0, 0.0, 6.0, 2.0, -2.5, -1.0])

def g2(x):
    p = Polynomial([2.0, 1.0, 0.0, -2.0, 2.5, 1.0])
    p_x = p(x)
#    print(p_x)
    return np.sign(p_x) * np.sqrt(np.abs(p_x)/6)

def g3(x):
    p = Polynomial([-2.0, -1.0, 6.0, 2.0, 0.0, -1.0])
    p_x = p(x)
#    print(p_x)
    return np.sign(p_x) * np.power(np.abs(p_x)/2.5, 1.0/4.0)

a1 = 0.8
g1_a = []

a2 = 0.8
g2_a = []

a3 = 0.8
g3_a = []

#+----------------------+
#| Start of your code   +
#+----------------------+

tolerance = 1e-7
for i in range(50):
    g1_a1 = g1(a1)
    g2_a2 = g2(a2)
    g3_a3 = g3(a3)
    if i > 0:
        is_repeating_value = g1_a1 - g1_a[i-1] <= tolerance
        is_repeating_value = is_repeating_value and g2_a2 - g2_a[i-1] <= tolerance
        is_repeating_value = is_repeating_value and g3_a3 - g3_a[i-1] <= tolerance
        if is_repeating_value:
            break
    a1 = g1_a1
    g1_a.append(a1)
    a2 = g2_a2
    g2_a.append(a2)
    a3 = g3_a3
    g3_a.append(a3)

print()

#+--------------------+
#| End of your code   +
#+--------------------+

xs = np.linspace(-2.5, 1.6, 100)
ys = f(xs)
dictionary = {
    'x': xs,
    'y': ys
}
plt.axhline(y=0, color='k')
plt.plot(xs, f(xs), label='f(x)')
plt.plot(xs, g1(xs), label='g1(x)')
plt.plot(xs, g2(xs), label='g2(x)')
plt.plot(xs, g3(xs), label='g3(x)')
plt.legend()
if len(g1_a) > 0:
    root = np.array([g1_a[len(g1_a)-1], g2_a[len(g2_a)-1], g3_a[len(g3_a)-1]])
    plt.plot(root, f(root), 'ro')

print(f'Roots: {f.roots()}')
print(pd.DataFrame({'g1(x)':g1_a, 'g2(x)':g2_a, 'g3(x))':g3_a,}))
```

```

```


<p align="center">

</p>

## About Hefty Number
Hefty Number is a python library built upon Numpy for doing arithmetic in arbitrary bases with arbitrary precision.

## Libraries

```python
import numpy as np
import math
import sympy as sp

sp.init_printing(order='rev-lex',use_latex='mathjax')
```

## Numbers
Our modern positional number system can be described as a polynomial.

$$
\large{\begin{align}
a_{n-1}b^{n-1} + a_{n-2}b^{n-2} \ldots a_{1}b^{1} + a_{0}b^{0} \tag{1}
\end{align}}
$$

Where:
* $a$ is a vector of coeficients representing the digits of a number and
* $b$ is the base of the number in base 10.
* $n$ is the legth of the number.

We can introduce the notion of a base vector to achieve an easier to work with linear form. If $a_n$ is our vector of coeficients and b is the base of the number then:

$$
\large
\dot{a} = [b^{n-1},b^{n-2},\ldots,b^{1},b^{0}] \tag{2}
$$

Given this notation we get the equivalent forms:

$$
\large{\begin{align}
a_{n-1}b^{n-1} + a_{n-2}b^{n-2} \ldots a_{1}b^{1} + a_{0}b^{0} =  \\
a_{n-1}\dot{a}_{n-1} + a_{n-2}\dot{a}_{n-2} \ldots a_{1}\dot{a}_{1} + a_{0}\dot{a}_{0} = \sum_{i=0}^{n-1}{a_i}{\dot{a}_i} = \\
a\cdot\dot{a}^\intercal
\end{align}}
$$

Where:
* $a$ is a vector of coeficients representing the digits of a number and
* $b$ is the base of the number.
* $\dot{a}$ is the base vector

Converting from base is a simple matter of multiplying the coeficients of the number by its base vector transposed. $a\cdot\dot{a}^\intercal$

# Arithmetic

### Multiplication

Multiplication is built upon the primitive addition operation. Any multiplication algorithm converts its opperands to a series of additions. Consider the multiplication of $425 * 323$. The naive approach is to add one of the opperands as many times as the other one. We could list $425$ to the count of $323$ and then add the list. This is inneficient in terms of the number of operations performed. The more efficient approach, long multiplication, is taught in grade school.

Let's think about what's going on when perform $425 * 323$ in respect to how these numbers are composed in the positional number system. We define a function that allows us to convert base 10 numbers into a vector representing the decomposition of the numbers:

```python
def b10ToUnitVect(b10Num):
    rollingSum = 0
    ls = []
    for i in range(1,len(str(b10Num))+1):
        res = b10Num % 10**i
        m   = res - rollingSum
        ls = [m] + ls
        rollingSum += m

    return np.matrix(ls)
```

```python
a = b10ToUnitVect(425)
b = b10ToUnitVect(323)

print(a)
print(b)
```

    [[400  20   5]]
    [[300  20   3]]

Multiplying the two numbers corresponds to multipying each element in one vector with every element of the other. This always produces a square matrix of numbers to be added. We can perform this enumeration by performing row vector by column vector dot product. As follows:

```python
a.T*b
```

    matrix([[1200000,   80000,   12000],
            [  60000,    4000,     600],
            [  15000,    1000,     150],
            [    900,      60,       9]])

```python
(a.T*b).flatten()
```

    matrix([[120000,   8000,   1200,   6000,    400,     60,   1500,    100,
                 15]])

```python
print(425*323 == int((a.T*b).flatten().sum()))
```

    True

```python
a = b10ToUnitVect(4329402105)
a.T*b
```

    matrix([[1200000000000,   80000000000,   12000000000],
            [  90000000000,    6000000000,     900000000],
            [   6000000000,     400000000,      60000000],
            [   2700000000,     180000000,      27000000],
            [    120000000,       8000000,       1200000],
            [            0,             0,             0],
            [       600000,         40000,          6000],
            [        30000,          2000,           300],
            [            0,             0,             0],
            [         1500,           100,            15]], dtype=int64)

```python
a = b10ToUnitVect(4253)
b = b10ToUnitVect(323)

a.T*b
```

    matrix([[1200000,   80000,   12000],
            [  60000,    4000,     600],
            [  15000,    1000,     150],
            [    900,      60,       9]])

```python
np.log10(1000)/np.log10(10)
```

$$3.0$$

```python
5*3+5*20+5*300+20*3+20*20+20*300+400*3+400*20+400*300
```

    137275

```python
k = np.divide(np.log(1000),np.log(10))
# k is numpy.float64 with the value"3.0"

z = k
k == "3.0"
```

    False

```python
k.
```

$$12.8$$

```python
12*5
```

$$60$$

```python
23/16
```

$$1.4375$$

Where a >= 1 and arg min is an non-negative integer.

$$\large{\begin{align}
\underset{a \ b \ c}{\operatorname{arg \ min}}\ 23-4^2a-4^1b-4^0c\\
\underset{a}{\operatorname{arg \ min}}\ 23-4^2a = a \\
\underset{b}{\operatorname{arg \ min}}\ 23-4^2a-4^1b = b \\
\underset{c}{\operatorname{arg \ min}}\ 23-4^2b-4^1b-4^0c = c \\
\end{align}}
$$

### Non-vector case

```python
def argmin_s(x,base,power) -> "[min,arg_min]":

    minVal,lastMinVal = 1,1
    fundamental       = base**power

    for i in range(0,base):
        minVal = x - fundamental * i

        if(minVal < 0):
            return [lastMinVal,i-1]
        if(minVal == 0):
            return [minVal,i]

        lastMinVal = minVal

    return [minVal,i]

def b10bn(b10Num,base)->"ndarry":
    if(base == 1):
        return np.ones(b10Num)

    logNum = math.log(b10Num,base)
    num    = []

    if logNum == 1:
        nDigits=2
    else:
        nDigits = math.ceil(logNum)

    for i in range(nDigits-1,0,-1):
        b10Num,idx = argmin_s(b10Num,base,i)
        num.append(idx)

    num.append(b10Num)

    return np.array(num)

def powerVect(base,n):
    num = [1]

    if n == 1:
        return np.array(num)
    else:
        r = 1
        for i in range(n):
            r = base*r
            num = [r] + num

        return np.matrix(num)

def vectToB10(vect,base):
    rVect = powerVect(base,vect.shape[0]-1)
    return vect*rVect.T

a = 9234094032940239493043829839823238928382938329823982389328923898238329832983239823982389232989838938928393893**3
b = b10bn(a,20)
print(b)
print(vectToB10(b,20))
```

    [ 2  3 10  7 16  1  0 18  5  8 11 19 13  0  8  0  3 16  4  5  3  0 19 19 11
      3  9 14 16  9 13  2 14  8  3  1  7 18 13 15 16  7 13  2 13  0 12 16  4  7
     16  5 19 15 17  8 10  3 16 19 10 12 17 18  2  1 13 19 13 16 13  3  4 10  0
      5  1  3  0  3 15 15 16  1 13  8  2  5 11 13 10 10  3 11 19  0 12  3 17 14
      5 11  1 17  9 11 12 13  4 13  0 11 10 17  7  2  8  6 16  3 18 14  5 10  2
     12 10 16  4  7 17 12  4 16 14  3 13  2  5 19 10 11  3  0  3  2 11 16  8 19
      5  5  6 14 12 18  5 18 13  9 19 18 10  8 17 17 11  3 11  7  1  3  2 12 19
     13 17 14 14 12 17  8  6 14 16 19 18  3 16 14  2 19  3 13  2  1  5  4  2 10
     13 14  2 16  9  1 11  4 13  5 19  5  2 12 11  9 19  8 11  5 11  0 17 13 17
      9  9  8  7 19  4 13 15  3  9 18 10  7 14  0  2  5 15 11 16 11  2  6 16 12
      7 17]
    [[ 787377278800261375797310609626570169255923439036910297102455296922779142265402741036829965897731387072437550921294373519676860926877849046796053515656186390238191719866517824579364401567959339626467440107729622759612596819782635071948294923475127692380421387128460609278029340193136528647715203699828982676533576148782791492957]]

```python
math.log(a)
```

    752.7062776521827

```python

```


# Continuous Signals

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Standard Signals

Certain [signals](https://en.wikipedia.org/wiki/Signal_%28electrical_engineering%29) play an important role in the theory and practical application of [signal processing](https://en.wikipedia.org/wiki/Signal_processing). They emerge from the theory of signals and systems, are used to characterize the properties of linear time-invariant (LTI) systems or frequently occur in practical applications. These standard signals are introduced and illustrated in the following. The treatise is limited to one-dimensional deterministic time- and amplitude-continuous signals.

### Complex Exponential Signal

The complex exponential signal is defined by the [complex exponential function](https://en.wikipedia.org/wiki/Exponential_function#Complex_plane)

\begin{equation}
x(t) = e^{s t}
\end{equation}

where $s = \sigma + j \omega$ denotes the complex frequency with $\sigma, \omega \in \mathbb{R}$. The signal is often used as a generalized representation of harmonic signals. Using [Euler's formula](https://en.wikipedia.org/wiki/Euler's_formula) above definition can be reformulated as

\begin{equation}
x(t) = e^{(\sigma + j \omega) t} = e^{\sigma t} \cos(\omega t) + j e^{\sigma t} \sin(\omega t)
\end{equation}

The real/imaginary part of the exponential signal is given by a weighted cosine/sine with angular frequency $\omega = 2 \pi f$. For $t>0$, the time-dependent weight $e^{\sigma t}$ is

* exponentially decaying over time for $\sigma < 0$,
* constantly one for $\sigma = 0$,
* exponentially growing over time for $\sigma > 0$,

and vice-versa for $t<0$. The complex exponential signal is used to model harmonic signals with exponentially decreasing or increasing amplitude.

**Example**

The following example illustrates the complex exponential signal and its parameters. The Python module [SymPy](http://docs.sympy.org/latest/index.html) is used for this purpose. It provides functionality for symbolic functions and mathematics. The required symbolic variables need to be defined explicitly before usage. In the example $t$, $\omega$ and $\sigma$ are defined as symbolic variables, followed by the definition of the exponential signal itself.

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

t, sigma, omega = sym.symbols('t sigma omega', real=True)
s = sigma + 1j*omega
x = sym.exp(s*t)
x
```

Now specific values for the complex frequency $s = \sigma + j \omega$ are considered for illustration. A new signal is defined by substituting both $\sigma$ and $\omega$ with specific values. The real and imaginary part of the signal is plotted for illustration.

```python
y = x.subs({omega: 10, sigma : -.1})

sym.plot(sym.re(y), (t, 0, 2*sym.pi), ylabel=r'Re{$e^{st}$}')
sym.plot(sym.im(y), (t, 0, 2*sym.pi), ylabel=r'Im{$e^{st}$}');
```

**Exercise**

* Try out other values for `omega` and `sigma` to create signals with increasing/constant/decreasing amplitude and different frequencies.

### Dirac Impulse

The Dirac impulse is one of the most important signals in the theory of signals and systems. It is used for the characterization of LTI systems and the modeling of impulse-like signals. The Dirac impulse is defined by way of the [Dirac delta function](https://en.wikipedia.org/wiki/Dirac_delta_function) which is not a function in the conventional sense. It is a generalized function or *distribution*. The Dirac impulse is denoted as $\delta(t)$. The Dirac delta function is defined by its effect on other functions. A rigorous treatment is beyond the scope of this course material. Please refer to the literature for a detailed discussion of the mathematical foundations of the Dirac delta distribution. Fortunately for its application in signal processing it is suitable to consider only certain properties. The most relevant ones are

1. **Sifting property**
    \begin{equation}
    \int_{-\infty}^{\infty} \delta(t) \cdot x(t) = x(0)
    \end{equation}
    where $x(t)$ needs to be differentiable at $t=0$. The sifting property implies $\int_{-\infty}^{\infty} \delta(t) = 1$.

2. **Multiplication**
    \begin{equation}
    x(t) \cdot \delta(t) = x(0) \cdot \delta(t)
    \end{equation}
    where $x(t)$ needs to be differentiable at $t=0$.

3. **Linearity**
    \begin{equation}
    a \cdot \delta(t) + b \cdot \delta(t) = (a+b) \cdot \delta(t)
    \end{equation}

4. **Scaling**
    \begin{equation}
    \delta(a t) = \frac{1}{|a|} \delta(t)
    \end{equation}
    where $a \in \mathbb{R} \setminus 0$. This implies that the Dirac impulse is a function with even symmetry.

5. **Derivation**
    \begin{equation}
    \int_{-\infty}^{\infty} \frac{d \delta(t)}{dt} \cdot x(t) \; dt = - \frac{d x(t)}{dt} \bigg\vert_{t = 0}
    \end{equation}

It is important to note that the product $\delta(t) \cdot \delta(t)$ of two Dirac impulses is not defined.

**Example**

This example illustrates some of the basic properties of the Dirac impulse. Let's first define a Dirac impulse by way of the Dirac delta function

```python
delta = sym.DiracDelta(t)
delta
```

Now let's check the sifting property by defining an arbitrary signal (function) $f(t)$ and integrating over its product with the Delta impulse

```python
f = sym.Function('f')(t)
sym.integrate(delta*f, (t, -sym.oo, sym.oo))
```

**Exercise**

* Derive the sifting property for a shifted Dirac impulse $\delta(t-\tau)$ and check your results by modifing above example.

### Heaviside Signal

The Heaviside signal is defined by the [Heaviside step function](https://en.wikipedia.org/wiki/Heaviside_step_function)

\begin{equation}
\epsilon(t) = \begin{cases} 0 & t<0 \\  \frac{1}{2} & t=0 \\ 1 & t > 0 \end{cases}
\end{equation}

Note that alternative definitions exist that differ with respect to the value of $\epsilon(t)$ at $t=0$. The Heaviside signal is used to represent a signal that switches on at a specified time and stays switched on indefinitely. The Heaviside signal can be related to the Dirac impulse by

\begin{equation}
\int_{-\infty}^{t} \delta(\tau) \; d\tau = \epsilon(t)
\end{equation}

**Example**

In the following, a Heaviside signal $\epsilon(t)$ is defined and plotted. Note that `Sympy` denotes the Heaviside function by $\theta(t)$.

```python
step = sym.Heaviside(t)
step
```

```python
sym.plot(step, (t, -2, 2), ylim=[-0.2, 1.2], ylabel=r'$\epsilon(t)$');
```

Let's construct a harmonic signal $\cos(\omega t)$ with $\omega=2$ which is switched on at $t=0$. Considering the definition of the Heaviside function, the desired signal is given as

\begin{equation}
x(t) = \cos(\omega t) \cdot \epsilon(t)
\end{equation}

```python
x = sym.cos(omega*t) * sym.Heaviside(t)
sym.plot(x.subs(omega,2), (t, -2, 10), ylim=[-1.2, 1.2], ylabel=r'$x(t)$');
```

### Rectangular Signal

The rectangular signal is defined by the [rectangular function](https://en.wikipedia.org/wiki/Rectangular_function)

\begin{equation}
\text{rect}(t) = \begin{cases} 1 & |t| < \frac{1}{2} \\ \frac{1}{2} & |t| = \frac{1}{2} \\ 1 & |t| > \frac{1}{2} \end{cases}
\end{equation}

Note that alternative definitions exist that differ with respect to the value of $\text{rect}(t)$ at $t = \pm \frac{1}{2}$. The rectangular signal is used to represent a signal which is switched on for a limited period of time or has finite duration. The rectangular signal can be related to the Heaviside signal by

\begin{equation}
\text{rect}(t) = \epsilon \left(t + \frac{1}{2} \right) - \epsilon \left(t - \frac{1}{2} \right)
\end{equation}

**Example**

The Heaviside function is used to define a rectangular function in `Sympy`. This function is then used as rectangular signal.

```python
class rect(sym.Function):

    @classmethod
    def eval(cls, arg):
        return sym.Heaviside(arg + sym.S.Half) - sym.Heaviside(arg - sym.S.Half)
```

```python
sym.plot(rect(t), (t, -1, 1), ylim=[-0.2, 1.2], ylabel=r'rect$(t)$');
```

**Exercise**

* Use $\text{rect}(t)$ to construct a harmonic signal $\cos(\omega t)$ with $\omega=2$ which is switched on at $t=-\frac{1}{2}$ and switched off at $t=+\frac{1}{2}$.

### Sign Signal

The sign signal is defined by the [sign/signum function](https://en.wikipedia.org/wiki/Sign_function) which evaluates the sign of its argument

\begin{equation}
\text{sgn}(t) = \begin{cases} 1 & t>0 \\ 0 & t=0 \\ -1 & t < 0  \end{cases}
\end{equation}

The sign signal is useful to represent the absolute value of a real-valued signal $x(t) \in \mathbb{R}$ by a multiplication

\begin{equation}
|x(t)| = x(t) \cdot \text{sgn}(x(t))
\end{equation}

It is related to the Heaviside function by

\begin{equation}
\text{sgn}(t) = 2 \cdot \epsilon(t) - 1
\end{equation}

**Example**

The following example illustrates the sign signal $\text{sgn}(t)$. Note that the sign function is represented as $\text{sign}(t)$ in `Sympy`.

```python
sgn = sym.sign(t)
sgn
```

```python
sym.plot(sgn, (t, -2, 2), ylim=[-1.2, 1.2], ylabel=r'sgn$(t)$');
```

**Exercise**

* Check the values of $\text{sgn}(t)$ for $t \to 0^-$, $t = 0$ and $t \to 0^+$ as implemented in `SymPy`. Do they conform to above definition?

**Copyright**

The notebook is provided as [Open Educational Resource](https://de.wikipedia.org/wiki/Open_Educational_Resources). Feel free to use the notebook for your own educational purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Signals and Systems* by Sascha Spors.