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

def comparison_plot2D(
    u, f,           # Function expressions in x and y
    value=0.5,      # x or y equals this value
    variation='y',  # independent variable
    n=100,          # no of intervals in plot
    tol=1E-8,       # tolerance for points inside the domain
    plottitle='',   # heading in plot
    filename='tmp', # stem of filename
    ):
    """
    Plot u and f along a line in x or y dir with n intervals
    and a tolerance of tol for points inside the domain.
    """
    v = np.linspace(-1+tol, 1-tol, n+1)
    # Compute points along specified line:
    points = np.array([(value, v_)
                       if variation == 'y' else (v_, value)
                       for v_ in v])
    u_values = [u(point) for point in points] # eval. Function
    f_values = [f(point) for point in points]
    plt.figure()
    plt.plot(v, u_values, 'r-', v, f_values, 'b--')
    plt.legend(['u', 'f'], loc='upper left')
    if variation == 'y':
        plt.xlabel('y'); plt.ylabel('u, f')
    else:
        plt.xlabel('x'); plt.ylabel('u, f')
    plt.title(plottitle)
    plt.savefig(filename + '.pdf')
    plt.savefig(filename + '.png')

import fenics as fe
import sympy as sym
x, y = sym.symbols('x[0] x[1]')

def problem(f, nx=8, ny=8, degrees=[1,2]):
    """
    Plot u along x=const or y=const for Lagrange elements,
    of given degrees, on a nx times ny mesh. f is a SymPy expression.
    """
    f = sym.printing.ccode(f)
    f = fe.Expression(f, degree=2)
    mesh = fe.RectangleMesh(
        fe.Point(-1, 0), fe.Point(1, 2), 2, 2)
    for degree in degrees:
        if degree == 0:
            # The P0 element is specified like this in FEniCS
            V = fe.FunctionSpace(mesh, 'DG', 0)
        else:
            # The Lagrange Pd family of elements, d=1,2,3,...
            V = fe.FunctionSpace(mesh, 'P', degree)
        u = fe.project(f, V)
        u_error = fe.errornorm(f, u, 'L2')
        print(('||u-f||=%g' % u_error, degree))
        comparison_plot2D(
            u, f,
            n=50,
            value=0.4, variation='x',
            plottitle='Approximation by P%d elements' % degree,
            filename='approx_fenics_by_P%d' % degree,
            tol=1E-3)
        #fe.plot(u, title='Approx by P%d' % degree)

if __name__ == '__main__':
    # x and y are global SymPy variables
    f = 2*x*y - x**16
    f = 2*x*y - x**2
    problem(f, nx=2, ny=2, degrees=[0, 1, 2])
    plt.show()

```

```python

```

Data Science and Machine Learning/Machine-Learning-In-Python-THOROUGH/EXAMPLES/FINITE_ELEMENTS/INTRO/SRC/02_APPROX_FENICS.ipynb


**Competing in different settings**

In this project we consider 2 firms who compete in the same duopolistic market. We will look at three possible competition forms, which are characterized by

**Cournot**

- Firms compete in quantities, and decide upon these independently and simultaneously

- Firms profit maximize given the other's choice of quantity

- Both firms have market power, thus one's decision of output affects the price of the other's goods

**Bertrand**

- Firms compete in prices, and decide upon these simultaneously and as if they were in a perfect competition setting

- We assume that consumers seek the lowest price (assuming the two products are homogenous)

- When prices are set, output is given as

$$D\left(p_{i},p_{j}\right)=\begin{cases}
D\left(p_{i}\right) & p_{i}<p_{j}\\
\frac{D\left(p_{i}\right)}{2} & p_{i}=p_{j}\\
0 & p_{i}>p_{j}
\end{cases}$$

**Stackelberg**

- Firms compete in quantities, and decide upon these sequentially.

- One firm is assumed to be the leader, and the other is the follower.

In the following we will analyse a sitaution with linear demand with $a=101$, $b=0.1$ and cost equal to 1 for all firms under all three situations.

```python
#Import packages
from scipy import optimize,arange
from numpy import array
import numpy as np
import matplotlib.pyplot as plt
import random
import sympy as sm
from math import *
%matplotlib inline
from IPython.display import Markdown, display

```

**Bertand**

First we wanna solve the case of Bertand competetion, i.e a sitatuion, where the firms compete in prices. Here we consider two companies who are selling differentiated products. The quantity each firm produces is dependent on the on it's own price $p_1$ and the competitor's price $p_2$.
$$x_1 = a - p_1 +bp_2$$
$$x_2 = a - p_2 + bp_1$$
The individual companies profits can be defined as:
$$\pi_1 = (p_1 - c_1)(a - p_1 +bp_2)$$
$$\pi_2 = (p_2 - c_2)(a - p_2 + bp_1).$$
For the following parts we assume that both companies have identical marginal costs and thus $$c_1 = c_2 = c.$$

```python
#Variables
a = sm.symbols('a')
p1 = sm.symbols('p1')
p2 = sm.symbols('p2')
c = sm.symbols('c')
b = sm.symbols('b')
alpha=sm.symbols('alpha')

#Frist we define the profit funtioncs for the two firms:
pi1 = (p1-c)*(a-p1+b*p2)
pi2 = (p2-c)*(a-p2+b*p1)
```

In order to get the equilibrium price we first have to determine the first order conditions (FOC) by maximizing the profit functions of the individual firms with repsect to the prices they offer.
$$\frac{\partial\pi_1}{\partial p_1}= a - 2p_1 + bp_2 + c=0$$
$$\frac{\partial\pi_2}{\partial p_2}= a - 2p_2 + bp_1 + c=0$$

By solving the first order conditions for the respective prices we arrive at the reaction functions.
$$p_1^{*} = \frac{a+ b*p_2 + c}{2}$$
$$p_2^{*} = \frac{a+ b*p_1 + c}{2}$$

By setting them equal, since it is the best repsonse for both firms to set their price equal to the competitior's price, we arrive at our equilibrium price:
$$p^{*} = \frac{-(a+c)}{b-2}$$

```python
#The first order conditions are then:
FOC1 = sm.diff(pi1, p1)
FOC2 = sm.diff(pi2, p2)

#Reactionsfuncions
RF1 = sm.solve(FOC1, p1)[0]
RF2 = sm.solve(FOC2, p2)[0]

#Equilibrium price
RF12 = sm.solve(FOC2,p1)[0]
EP1 = sm.solve(RF1-RF12,p2)[0]
print(EP1) #Equilibrium price both companies will charge
RF21 = sm.solve(FOC1,p2)[0]
EP2 = sm.solve(RF2-RF21,p1)[0]
```

    -(a + c)/(b - 2)

Using the lambdify feature from the sympy package we can now make a function to solve the bertrand competition for chosen values. In this case we set $a$ equal to 101, $b$ equal to 0.1 and $c$ equal to 1 (the numbers were decided on arbitrarily). This yields an equilibrium price of 53.

```python
#make it a function to solve it
bertrand = sm.lambdify((a,b,c), EP1)
price1=bertrand(101,0.1,1)
quantity1 = 101 - 0.9*bertrand(101,0.1,1)
profit1 = (bertrand(101,0.1,1)-1)*(101 - 0.9*bertrand(101,0.1,1))

#Defining a way to print the solution in a fancy way
def fancy(string):
    display(Markdown(string))

fancy(f'The equilibrium price is $p$ = {price1:.0f}, the equilibrium quantity is $q$ = {quantity1:.0f} and the profits for both firms are $π$ = {profit1:.0f}')
```

The equilibrium price is $p$ = 54, the equilibrium quantity is $q$ = 53 and the profits for both firms are $π$ = 2776

**Stackelberg**

We now turn to the Stackelberg, i.e a sequantiacal competion in quantities, thus one company is the leader and the other is a follower.

```python
#values
p = sm.symbols('p')
x1 = sm.symbols('x1')
x2 = sm.symbols('x2')

#Giving values to our parameters
a = 101
c = 1
b = 0.1
```

Each firm chooses a quantitiy $x_{1}, x_{2}\geq0$ where the cost of production is $c_{1}=c_{2}$. Each firm set the price $p = a-b\left(x_{1}+x_{2}\right)$. The profit function for firm $i$ is then written as $\pi_{i}\left(x_{i}\right)=p\left(x_{i}\right)-c_{i}x_{i}$ Assume firm 1 is the leader and 2 is the follower. The game is solved by backward induction.

We will first define the inverse demand function $p$:

```python
#Defining price
p = a - b*(x1+x2)
```

Then, we set up the profit function for firm 1 and firm 2:

```python
#Profit function of firm 1
profit_1 = p*x1 - c*x1
profit_2 = p*x2 - c*x2
```

We will say that firm 1 decides first on its output. Firm 2 observes the decision of firm 1 and decides how to reacht. We can solve the problem backwards and find optimal output of firm 2 for each possible output level of firm 1.
It means that we maximize the profit of firm 2 by differentiating it by $x_2$.

```python
#Differentiating the profit of firm 2 by q2

diff_profit_2 = sm.diff(profit_2, x2)
diff_profit_2
```

$\displaystyle - 0.1 x_{1} - 0.2 x_{2} + 100$

```python
#Finding optimal q2 by isolating q2 from the differentiation
opt_2 = sm.solve(diff_profit_2, x2)
opt_2[0]
```

$\displaystyle 500.0 - 0.5 x_{1}$

Then, we can substitute $q_2$ into the profit function of firm 1. Thanks to that, we have a single variable function.

```python
#Substituting q2 into the profit function of firm 1
profit_1_subs = profit_1.subs(x2,opt_2[0])
profit_1_subs
```

$\displaystyle x_{1} \left(51.0 - 0.05 x_{1}\right) - x_{1}$

Once again, we need to differentiate the profit of firm 1 by $q_1$ to find the optimal value of $q_1$.

```python
#Differentiating the profit of firm 1 by q1

diff_profit_1 = sm.diff(profit_1_subs, x1)
diff_profit_1
```

$\displaystyle 50.0 - 0.1 x_{1}$

```python
#Finding the optimal value of q1

opt_1 = sm.solve(diff_profit_1, x1)
opt_1[0]
```

$\displaystyle 500.0$

Now, we can find the optimal value of $q_2$.

```python
opt_2_subs = opt_2[0].subs(x1,opt_1[0])
opt_2_subs
```

$\displaystyle 250.0$

We can also find the equilibrium market price, profit of firm 1 and profit of firm 2:

```python
#Finding equilibrium market price
p = a - b*(opt_1[0] + opt_2_subs)

#Finding profit of firm 1
profit_1 = p*opt_1[0] - c*opt_1[0]

#Finding profit of firm 2
profit_2 = p*opt_2_subs - c*opt_2_subs

fancy(f'The equilibrium price is $p$ = {int(p)}, and the profits for both firms are $π_1$ = {int(profit_1)} and $π_2$ = {int(profit_2)}')
```

The equilibrium price is $p$ = 26, and the profits for both firms are $π_1$ = 12500 and $π_2$ = 6250

We now want to make a figure that show how the beste responses relate.

```python

```

```python
#Defining the x-axis
qu_1 = np.linspace(0, 1000, 1000)

#Defining the lambda functions for our best responses functions
BR2 = lambda qu_1 : 500 - qu_1/2
BR1 = lambda qu_1 : 500 - qu_1/2

#Plotting the figure
plt.figure(figsize=(15,10))
plt.plot(qu_1, BR2(qu_1), label = "Best Response of Firm 1")
plt.plot(BR1(qu_1),qu_1 , label = "Best Response of Firm 2")
plt.grid(True)
plt.xlabel('$q1$')
plt.ylabel('$q2$')
plt.title('Best response functions')
plt.legend()

#To include a quantity of 0 for 1 firm, we set the axis like this
plt.xlim(0,1000) # sets the x-axis
plt.ylim(0,600) # Sets the y-axis

plt.show()
```

This tells that when firm chooses a quantity of 500 in the first stage, firm 2 will choose a quantity half the size of firm 1 in second stage. In a case where firm 1 chooses to produce 1000 units, firm 2 will then produce 0.

**Cournot**

For the Cournot case we are gonna set up some functions to solve the model in order to make an extention with n firms later on.

Consider the linear invese demand curve $p_{i}\left(x_{i},x_{j}\right)=a-x_{i}-bx_{j}$ where $b\in\left(0,1\right)$ is the elasticity of substitution between the goods, i.e. indicates that the goods are heterogenous for $0<b<1$.

We write the inverse demand:

```python
def price(x1,x2,a,b):
    """A function that computes prices
    args:
    x1 : quantity for firm 1
    x2 : quantity for firm 2
    a :total demand
    b : elasticity of substitution
    """
    return a-b*(x1+x2)

```

Firms have constant marginal costs, where total costs are given by $c\left(x_{i,j}\right)=cx_{i,j}$

```python
def cost(x,c):
    """A function that computes total cost and sets it equal to 0 if nothing is produced
    args:
    c = cost for firm i
    x = quantity for firm i
    """
    if x == 0:
     cost = 0
    else:
     cost = c*x
    return cost
```

Firms maximize profits given the choice of the other firm such that $$\pi\left(x_{i},x_{j}\right)=p_{i}\left(x_{i},x_{j}\right)x_{i}-c\left(x_{i}\right)$$

```python
def profit(x1,x2,c1,a,b):
    """A function that computes profit for firm 1
    args:
    x1 : quantity for firm 1
    x2 : quantity for firm 2
    c1 : cost for firm 1
    a : total demand
    b : elasticity of substitution
    """
    return price(x1,x2,a,b)*x1-cost(x1,c1)
```

In a Cournot setting, firm $i$ chooses $x_{i}$ taking $x_{j}$ as given. Thus, the Nash equilibrium for $x_{i}^{*},x_{j}^{*}$ holds for $$x_{i}^{*}=\text{arg}\underset{x_{i}}{\text{ max }}\pi\left(x_{i},x_{j}^{*}\right)\;\;\;\;\;\;\text{for each }i\neq j$$ Thus, we are able to find firm $i$ optimal production level given the choice of $x_{j}$ by creating the function 'reaction'. Hence, taking the first order condition to find the Nash equilibrium for $x_{i}^{*},x_{j}^{*}$ yields $$\frac{\partial\pi\left(x_{i},x_{j}\right)}{\partial x_{i,j}}=0$$

In this case, we also want to calculate the profit maximizing outcome, so we use the function 'brute'. This allows one to look for the global maximum of $\pi$. Likewise, we keep in mind that 'scipy optimize' is bound to minimize problems, and since we want to maximize the profit function, we then minimize $-\pi\left(x_{i},x_{j}\right)$.

```python
def reaction(x2,c1,b,a):
    """A function that computes the optimal quantity
    args
    x2 : quantity for firm 2
    c1 : cost for firm 1
    b : elasticity of substitution
    a : total demand
    """

    #bounds = ((0,a/b-x2),)
    x1 = optimize.minimize(lambda x: -profit(x,x2,c1,a,b),x0 = [x2],method='L-BFGS-B')
    #print(x1.message)

    return x1.x[0]
```

```python
def reaction(x2,c1,b,a):
    """A function that computes the optimal quantity
    args
    x2 : quantity for firm 2
    c1 : cost for firm 1
    b : elasticity of substitution
    a : total demand
    """
    x1 = optimize.brute(lambda x: -profit(x,x2,c1,a,b), ((0,1,),))

    return x1[0]
```

```python
reaction(333.3715739255503,1,0.1,101)
```

    333.31425781250056

Now, we must find an equilibrium for price and quantity given their best response. For this reason, consider the vector function $f\left(x_{i},x_{j}\right)$ which takes firm $i$'s best response $r_{i}\left(x_{j}\right)$ to $j$'s production level $x_{j}$ into consideration. We can write this as $$f\left(x_{i},x_{j}\right)=\left[\begin{array}{c}
r_{i}\left(x_{j}^{*}\right)\\
r_{j}\left(x_{i}^{*}\right)
\end{array}\right]$$

We are then looking for the quantities produced is equal to best response given what the other firm produce, i.e the point where:

$$\left(\begin{array}{c}
x_{1}^{*}\\
x_{2}^{*}
\end{array}\right)=\left(\begin{array}{c}
r_{1}(x_{2}^{*})\\
r_{2}(x_{1}^{*})
\end{array}\right)$$

Thus, we create a function 'vec_reaction' given by the difference between an array of the quantaites subtracted by the best response functions and pass $b,a, c_{i},c_{j}$ as a vector of parameters.

```python
def vec_reaction(x,param):
    """A function that computes the difference between the quantities and the beste response functions
    args
    x : quantaties
    param : list of parameters"""
    return array(x)-array([reaction(x[1],param[2],param[0],param[1]),reaction(x[0],param[3],param[0], param[1])])
```

Now, moving on to find the actual Cournot equilibrium, we are able to set the values of each paramter. Using the 'fsolve' from 'scipy.optimize' to find $x$. Also, giving 'fsolve' the inital guess of the quantaites, while passing the paramater vector 'param' as arguments 'args' to 'fsolve', this yields:

```python
param = [0.1,101,1,1]
x0 = [100,100]

EQ = optimize.fsolve(vec_reaction, x0, args = (param))
EQ

fancy(f'The Nash equilibrium quantity for both firms are $x^* =[x_1^*,x_2^*]$ = {EQ} where the equilibrium price for both firms are $p^*$ = {price(EQ[0],EQ[1],101,0.1)} with a profit of $\pi^*$= {profit(EQ[0],EQ[1],1,101,0.1)}')

```

The Nash equilibrium quantity for both firms are $x^* =[x_1^*,x_2^*]$ = [333.33330078 333.33330078] where the equilibrium price for both firms are $p^*$ = 34.333339843749854 with a profit of $\pi^*$= 11111.11219618032

We now want to show, how the quantity produced changes, when the cost of firm 1 changes.

```python
range_c = np.linspace(1,0,20)
range_x = [optimize.fsolve(vec_reaction, x0, args = ([0.1,101, c, 1])) for c in range_c]

plt.style.use('seaborn')
plt.clf()
plt.plot(range_c,array(range_x)[:,0], label = '$x_1$')
plt.plot(range_c,array(range_x)[:,1], label = '$x_2$')
plt.xlabel('$c_1$')
plt.ylabel('$x$')
plt.legend()
plt.show()
```

In the figure, we see, that as the cost of firm 1 goes up, the quantity produced by firm 1 falls, while the quantity produced by firm 2 goes up.

**Compare all three cases**

We have now tried to solve a situation with two firms competing with three different models, giving them the same parameter values. This leads to the following results:

```python
fancy(f'For Bertrand: The equilibrium price is $p$ = {int(price1)}, the equilibrium quantity is $q$ = {int(quantity1)} and the profits for both firms are $π$ = {int(profit1)}')
fancy(f'For Stackelberg: the equilibrium price is $p$ = {int(p)}, and the profits for both firms are $π_1$ = {int(profit_1)} and $π_2$ = {int(profit_2)}')
fancy(f'For Cournot: the equilibrium quantity for both firms are $x^* =[x_1^*,x_2^*]$ = {EQ} where the equilibrium price for both firms are $p^*$ = {price(EQ[0],EQ[1],101,0.1)} with a profit of $\pi^*$= {profit(EQ[0],EQ[1],1,101,0.1)}')

```

For Bertrand: The equilibrium price is $p$ = 53, the equilibrium quantity is $q$ = 52 and the profits for both firms are $π$ = 2775

For Stackelberg: the equilibrium price is $p$ = 26, and the profits for both firms are $π_1$ = 12500 and $π_2$ = 6250

For Cournot: the equilibrium quantity for both firms are $x^* =[x_1^*,x_2^*]$ = [333.33330078 333.33330078] where the equilibrium price for both firms are $p^*$ = 34.333339843749854 with a profit of $\pi^*$= 11111.11219618032

**Cournot with n> 2 firms**

In order to do an extention to one of the models, we now want to investigate the case of N > 2 firms. First we set up the situation theoretically. In the following theoretical set-up we will follow the set-up in this note, adapting it to our situation: http://people.exeter.ac.uk/dgbalken/micro07/CournotHandout.pdf (we add the posibility of different cost for each firms)

Following the notation from ealier we now add:

- Firm $i$'s output: $x_i$

- Total output in in industry: $X=x_1+x_2+...+x_n$

- The opponent's output: $x_{-i} = X-x_i$

Each firm will then max profit given the expectation of $x_{-i}$:

$$\Pi_i(x_{-i},x_i)=(p(x_{-i}+x_i)-c_i)x_i$$

as with the n = 2 case we use the invese demand funcktion in a linear case:

$$p = a-b(x_{-i}+x_i)=a-bX$$

The FOC of is then:
$$\frac{\partial \Pi_i}{\partial x_i} = \frac{\partial p}{\partial x_i} \cdot x_i + p-c_i = 0$$

where $\frac{\partial p}{\partial x_i} = -b$ giving us

$$\frac{\partial\Pi_i}{\partial x_i} : -bx_i + p-c_i = 0$$

inserting the inverse demand function:

$$\frac{\partial\Pi_i}{\partial x_i} : -bx_i + (a-b(x_{-i}+x_{i}))-c_i = 0$$

solving for $x_i$ to get the reaction function for firm $i$ given its expectation for the amount the rest of the firms produce:

$$x_i(x_{-i}) = \frac{a-c}{2b}-\frac{1}{2}x_{-i}$$

which is the case for all firms $i=1,..,n$. We have $X=x_i+x_{-i}$ so the FOC found earlier can be rewritten to:
$$ -bx_i + (a-bX-c_i) = 0$$

which for the n firms:
$$-bX + n(a-bX)-n\bar c = 0$$

where we use the average cost level: $\bar c = \frac{c_1+c_2+...+c_n}{n}$

we can now rewrite this and get the quantity produced and the price in the market:

$$bX + n(bX) = n(A-\bar c)$$

$$(n+1)bX  = n(A-\bar c)$$

Quantity is then:

$$X = \frac{n}{n+1} \frac{a-\bar c}{b}$$

Price is:
$$p = a-bX = a-b[\frac{n}{n+1} \frac{a-\bar c}{b}] = \frac{1}{n+1}a + \frac{n}{n+1}\bar c$$

Using the found functions we can now try to set a program that is able to solve cases with N firms.

```python
def Round(a):
    """A function that prints floats rounded as strings
    args:
    a : a float"""
    return str("{:.0f}".format(a))
```

First we define a function, that calculates the prices $p = \frac{1}{n+1}a + \frac{n}{n+1}\bar c $ rewriting using the definition of the average cost level gives: $p= \frac{1}{n+1}a + \frac{c_1+c_2+...+c_n}{n+1}$ which we use in our function.

```python
def price_eq(a, n, c): #Returns the equilibirum price given n firms
    """A function that computes price
    args:
    a : total demand
    n : number of firms
    c : cost of firm i
    """

    price = max((a+sum(c))/(1+n), 0)
    return price
```

We now want to define a function that returns the quantity produced for firm $i$.
this is given by:
$$x_i = \frac{a-bX-c_i}{b}=\frac{a-c_i}{b}-\frac{n}{n+1}\frac{a-\bar c}{b} = \frac{1}{n+1}\frac{a}{b}+ \frac{n(\bar c -c_i)-c_i}{(n+1)b} = \frac{1}{n+1}\frac{1}{b}[a- n(\bar c -c_i)-c_i]$$

use that $\bar c = \frac{c_1+c_2+...+c_n}{n}$
$$x_i = \frac{1}{n+1}\frac{1}{b}[a+ nc_i+(c_1+c_2+...+c_n-c_i)]$$
which we now use to define a function, that returns the quantity:

```python
def xi_eq(a, b, n, c, i): #Returns equilibrium quantity for firm i
    """A function that computes quantity for firm i
    args:
    a (float):total demand
    b (float):elasticity of substitution between the good
    n (float):number of firms
    c (float): cost of firm i
    i (integer): the number of the firm
    Returns a float"""

    #x_i = (1/b)*((a+n*c[i]+(sum(c)-c[i]))/(1+n))
    #if x_i < 1:
     #   x_i = 0
    x_i = (1/b)*((a+(n-1)*c[i]-sum(c))/(1+n))
    return x_i
```

We can then define a function, that returns the quantity for the entire number of firms

```python
def X_eq(a, b, n, c):
    """A function that computes quantity for the total amount of firms
    args:
    a : total demand
    b : elasticity of substitution between the good
    n : number of firms
    c : cost of firm i
    """

    X = []
    for i in range(0, n, 1):
        X.append(xi_eq(a, b, n, c, i))

    return sum(X)
```

```python

```

```python
n= 10
c_vec = [3 for i in range(n)]
X_eq(a,b,n,c_vec)
```

    890.909090909091

```python
len(c_vec)
```

    10

```python
n= 1000
c_vec = [random.uniform(1,3) for i in range(n)]
X_eq(a,b,n,c_vec)
```

    989.1567070866106

```python
np.sum([a/b/(n+1)+(n*(np.mean(c_vec)-c)-c)/((n+1)*b) for c in c_vec])
```

    989.1567070866333

```python
n/(n+1) * (a-np.mean(c_vec))/b
```

    989.1567070866319

Based on the assumtion that $$c_i = \bar c = c $$ the profit for firm $i$ can be found by:
$$\Pi_{i}=(p-c)x_{i}=\left(\frac{a+nc}{n+1}-c\right)\frac{1}{n+1}\frac{1}{b}(a-c)$$

Without identical marginal costs $$c_i \neq c $$ $$\bar c \neq c$$ the profit for firm $i$ would be:
$$\Pi_{i}=(p-c_i)x_{i}=\left(\frac{a+n\bar c}{n+1}-c_i\right)\frac{1}{n+1}\frac{1}{b}(a+n(\bar c - c_i)-c_1) = \frac{1}{(n+1)^2 * b}(a+n(\bar c - c_1)-c_i)^2$$

By maximizing the previously derived profit function we find the equilibrium profit.

```python
def profit_eq(a, b, n, c, i):
    """A function that computes profit for firm i and sets profit to zero, if there is no production
    args:
    a : total demand
    b : elasticity of substitution between the good
    n : number of firms
    c : cost of firm i
    i : the number of the firm
    """

    if xi_eq(a, b, n, c, i) == 0:
        profit = 0
    else:
        profit = max((1/b)*(((a-n*c[i]+(sum(c)-c[i]))/(1+n))**2), 0)

    return profit

```

We are now ready to define a function, that can solve a general case of cournot competetion with n firms. We choose the number of firms to be drawn randomely using random.randint, while the cost for each firm will be drawn from a uniform destribution using random.uniform.

```python
def EQ_N(a, b, N, c_low, c_high):
    """A function that draws number of firms and compute quantaties, price and average profit
    args:
    a (float):total demand
    b (float):elasticity of substitution between the good
    N (float): the maximal number of firms that can be drawn
    c_low (float): the lower bound of the interval for the cost
    c_high (float): the upper bound of the interval for the cost
    Returns a float"""

    #draw the number of firms:
    n = random.randint(2, N)

    #drawing of costs for each firm:
    c = []
    for i in range(0, n+1, 1):
        c_i = random.uniform(c_low, c_high)
        c.append(c_i)

    #the price:
    price = price_eq(a, n, c)

    #quantities and profits
    xi_list = []
    profiti_list = []
    for i in range(0, n+1, 1):
        xi_list.append(xi_eq(a, b, n, c, i))
        profiti_list.append(profit_eq(a, b, n, c, i))

    #the quantity for the entire number of firms
    X = X_eq(a, b, n, c)

    output = print('\n The number of firms drawn are ' + Round(n)  +
                  '\n The total amount produced is ' + Round(X) +',' + ' while the price is ' + Round(price) +
                   '\n The average profit for each firm is ' + Round(np.array(profiti_list).mean()) + '.'
                   '\n')

    return output

```

We are now ready to try a case with parameters as in the n = 2, but with a randomw number of firms drawn:

```python
a = 101
b = 0.1
N = 25
c_low = 1
c_high = 5
EQ_N(a, b, N, c_low, c_high)

```

     The number of firms drawn are 9
     The total amount produced is 848, while the price is 13
     The average profit for each firm is 1032.

```python
def EQ_N(a, b, n, c_low, c_high):
    """A function that draws number of firms and compute quantaties, price and average profit
    args:
    a (float):total demand
    b (float):elasticity of substitution between the good
    N (float): the maximal number of firms that can be drawn
    c_low (float): the lower bound of the interval for the cost
    c_high (float): the upper bound of the interval for the cost
    Returns a float"""

    #draw the number of firms:
    #n = random.randint(2, N)

    #drawing of costs for each firm:
    c = []
    for i in range(0, n, 1):
        c_i = random.uniform(c_low, c_high)
        c.append(c_i)

    #the price:
    price = price_eq(a, n, c)

    #quantities and profits
    xi_list = []
    profiti_list = []
    #for i in range(0, n+1, 1):
        #xi_list.append(xi_eq(a, b, n, c, i))
        #profiti_list.append(profit_eq(a, b, n, c, i))

    #the quantity for the entire number of firms
    X = X_eq(a, b, n, c)

    return X,price, c
    output = print('\n The number of firms drawn are ' + Round(n)  +
                  '\n The total amount produced is ' + Round(X) +',' + ' while the price is ' + Round(price) +
                   '\n The average profit for each firm is ' + Round(np.array(profiti_list).mean()) + '.'
                   '\n')

    return output

```

```python
X,price, c_vec =  EQ_N(a, b, 1000, c_low, c_high)
print(X,price)
```

    979.4548976585473 3.054510234142361

```python
n_vec = list(range(100,1000))

output_vec1 = array([EQ_N(a,b,n,c_low,c_high) for n in n_vec])

fig,ax = plt.subplots()
ax.plot(n_vec,output_vec1[:,0], label = '$X$')
plt.legend()
plt.show()
```

```python

fig,ax = plt.subplots()
ax.plot(n_vec,output_vec1[:,1], label = '$X$')
plt.legend()
plt.show()
```

```python
def solution(a,b,c_vec):
    n= len(c_vec)
    q = n/(n+1) * (a-np.mean(c_vec))/b
    p = a-b*q
    return q,p
```

```python
solution(a,b,c_vec)
```

    (979.4548976585766, 3.0545102341423416)

```python
solution(a,b,[1,2,3])
```

    (742.5, 26.75)

```python
def setup_solve(a,b,n,c_low,c_high):
    c_vec = [random.uniform(c_low, c_high) for i in range(n)]
    return solution(a,b,c_vec)
```

```python
n_vec = list(range(100,1000))
c = 3

#output_vec = array([setup_solve(a,b,n,1,5) for n in n_vec])
output_vec = array([solution(a,b,c_vec) for c_vec in output_vec1[:,2]])

fig,ax = plt.subplots()
ax.plot(n_vec,output_vec[:,0], label = '$X$')
plt.legend()
plt.show()
```

```python
plt.style.use('seaborn')
plt.clf()
plt.plot(n_vec,output_vec[:,1], label = '$price$')
plt.legend()
plt.show()
```

```python
output_vec[-1,1]
```

    3.129127017124361

**Solving using python**

```python
N = 100
c_low, c_high = 3,5
c_vec = np.random.uniform(c_low,c_high,size=N)
```

```python
def focs(a,b,q_vec,c_vec):
    # Using analytical FOCs from pdf-link
    return -b*q_vec+a-b*np.sum(q_vec)-c_vec
```

```python
def solve(a,b,c_vec):
    obj = lambda q_vec : focs(a,b,q_vec,c_vec)

    res = optimize.root(obj, x0 = [90 for n in range(N)])

    q_vec_star = res.x
    Q = np.sum(q_vec_star)

    # Use inverse demand to find price
    p = a- b*Q
    return Q, p

```

```python
solve(a,b,c_vec)
```

    (960.2800162703653, 4.971998372963469)

```python
# Compare
solution(a,b,c_vec)
```

    (960.2800162703655, 4.971998372963441)

**Conclusion**

In this paper we examine two firms who have the possibility of competing in Bertrand, Stackelberg and Cournot. We use the same parameter values for all competition types, and find that the firms benefit the most from competing in a Cournot setting as this yields the highest total profits of 22,222. This is intuitive, as in contrast to Bertrand, the firms will undercut each other until their price is equal to their marginal costs to obtain efficiency. Therefore, this setting yields a price as in perfect competition, whereas Cournot yields an oligopoly equilibrium.

As Cournot yields the best outcome in terms of profits for two firms, we extend this setting for $n$ firms. We find that for $n\rightarrow\infty$, the average profit for a firm decreases because competing in quantities becomes more intense as the competition setting moves from oligopoly to infinitely many firms competing in quantities.


```python
from sympy import *
EI, a, q = var("EI, a, q")
pprint("\nFEM-Solution:")

# 1: Stiffness Matrices:
# Element 1
l = 2*a
l2 = l*l
l3 = l*l*l
K = EI/l3 * Matrix(
    [
    [  4*l2 ,   -6*l ,   2*l2 ,    6*l ,   0   ,   0   ],
    [ -6*l  ,   12   ,  -6*l  ,  -12   ,   0   ,   0   ],
    [  2*l2 ,   -6*l ,   4*l2 ,    6*l ,   0   ,   0   ],
    [  6*l  ,  -12   ,   6*l  ,   12   ,   0   ,   0   ],
    [  0    ,    0   ,   0    ,    0   ,   0   ,   0   ],
    [  0    ,    0   ,   0    ,    0   ,   0   ,   0   ],
    ]
    )
# Element 2
l = a
l2 = l*l
l3 = l*l*l
K += EI/l3 * Matrix(
    [
    [  0   ,   0   ,  0    ,   0   ,  0    ,   0       ],
    [  0   ,   0   ,  0    ,   0   ,  0    ,   0       ],
    [  0   ,   0   ,  4*l2 ,  -6*l ,  2*l2 ,   6*l     ],
    [  0   ,   0   , -6*l  ,  12   , -6*l  , -12       ],
    [  0   ,   0   ,  2*l2 ,  -6*l ,  4*l2 ,   6*l     ],
    [  0   ,   0   ,  6*l  , -12   ,  6*l  ,  12       ],
    ]
    )

# 2: BCs:
p0,w0,p1,w1,p2,w2 = var("ψ₀,w₀,ψ₁,w₁,ψ₂,w₂")
M0,F0,M1,F1,M2,F2 = var("M₀,F₀,M₁,F₁,M₂,F₂")

Mq1, Fq1 = -q/12*a*a, q/2*a
Mq2, Fq2 = -Mq1, Fq1

#              0                         1              2
#                                        qqqqqqqqqqqqqqqq
#              |-------------------------A---------------
u = Matrix([  0,0,                     p1,0,         p2,w2      ] )
f = Matrix([ M0,F0,                    Mq1,Fq1+F1,  Mq2,Fq2     ] )
unks =     [ M0,F0,                    p1,F1,        p2,w2      ]

# 3: Solution:
eq = Eq(K*u, f)
sol = solve(eq, unks)
pprint(sol)

pprint("\nMinimum-Total-Potential-Energy-Principle-Solution:")

# 1: Ansatz:
a0, a1, a2, a3 = var("a0, a1, a2, a3")
b0, b1, b2 = var("b0, b1, b2")

order = 2
if (order == 2):
    # w2 has order 2
    b3, b4 = 0, 0
else:
    # w2 has order 4
    b3, b4 = var("b3, b4")

x1, x2 = var("x1, x2")
w1 = a0 + a1*x1 + a2*x1**2 + a3*x1**3
w1p = diff(w1, x1)
w1pp = diff(w1p, x1)

w2 = b0 + b1*x2 + b2*x2**2 + b3*x2**3 + b4*x2**4
w2p = diff(w2, x2)
w2pp = diff(w2p, x2)

pprint("\nw1 and w2:")
pprint(w1)
pprint(w2)

# 2: Using BCs:
pprint("\nElimination of a0, a1, a2, a3, b0 using BCs:")
# w1(0)=0
e1 = Eq(w1.subs(x1, 0))
# w1'(0)=0
e2 = Eq(w1p.subs(x1, 0))
# w1(2l)=0
e3 = Eq(w1.subs(x1, 2*a))
# w2(0)=0
e4 = Eq(w2.subs(x2, 0))
# w1p(2a)=w2p(0)
e5 = Eq(w1p.subs(x1, 2*a), w2p.subs(x2, 0))

eqns, unks = [e1, e2, e3, e4, e5], [a0, a1, a2, a3, b0]
sol = solve(eqns, unks)
pprint(sol)

sub_list=[
    (a0, sol[a0]),
    (a1, sol[a1]),
    (a2, sol[a2]),
    (a3, sol[a3]),
    (b0, sol[b0]),
    ]

pprint("\nw1 and w2:")
w1 = w1.subs(sub_list)
w2 = w2.subs(sub_list)
pprint(w1)
pprint(w2)

pprint("\nw1'' and w2'':")
w1pp = w1pp.subs(sub_list)
w2pp = w2pp.subs(sub_list)
pprint(w1pp)
pprint(w2pp)

# 3: Using Principle:
pprint("\nU1, U2, Uq:")
i1 = w1pp*w1pp
I1 = integrate(i1, x1)
I1 = I1.subs(x1,2*a) - I1.subs(x1,0)
U1 = EI*I1/2
pprint(U1)

i2 = w2pp*w2pp
I2 = integrate(i2, x2)
I2 = I2.subs(x2,a) - I2.subs(x2,0)
U2 = EI*I2/2
pprint(U2)

i2 = q*w2
I2 = integrate(i2, x2)
I2 = I2.subs(x2,a) - I2.subs(x2,0)
Uq = I2
pprint(Uq)

pprint("\nParameters for U1 + U2 - Uq = Min:")
U = U1 + U2 - Uq

e1 = Eq(diff(U, b1))
e2 = Eq(diff(U, b2))
if (order == 2):
    eqns = [e1, e2]
    unks = [b1, b2]
    sol = solve(eqns, unks)
    sub_list=[
        (b1, sol[b1]),
        (b2, sol[b2]),
        ]
    w2 = w2.subs(sub_list)
else:
    e3 = Eq(diff(U, b3))
    e4 = Eq(diff(U, b4))
    eqns = [e1, e2, e3, e4]
    unks = [b1, b2, b3, b4]
    sol = solve(eqns, unks)
    sub_list=[
        (b1, sol[b1]),
        (b2, sol[b2]),
        (b3, sol[b3]),
        (b4, sol[b4]),
        ]
    w2 = w2.subs(sub_list)

pprint(sol)

pprint("\nw2:")
pprint(w2)

pprint("\nw2(a):")
w2 = w2.subs(x2, a)
pprint(w2)

# FEM-Solution:
# ⎧                               2            4          3             3   ⎫
# ⎪    3⋅a⋅q      -11⋅a⋅q       -a ⋅q       3⋅a ⋅q      -a ⋅q       -5⋅a ⋅q ⎪
# ⎨F₀: ─────, F₁: ────────, M₀: ──────, w₂: ──────, ψ₁: ──────, ψ₂: ────────⎬
# ⎪      8           8            4          8⋅EI        4⋅EI        12⋅EI  ⎪
# ⎩                                                                         ⎭
#
# 2. Minimum-Total-Potential-Energy-Principle-Solution:
#
# w1 and w2:
#                   2        3
# a₀ + a₁⋅x₁ + a₂⋅x₁  + a₃⋅x₁
#                   2
# b₀ + b₁⋅x₂ + b₂⋅x₂
#
# Elimination of a0, a1, a2, a3, b0 using BCs:
# ⎧                  -b₁        b₁        ⎫
# ⎪a₀: 0, a₁: 0, a₂: ────, a₃: ────, b₀: 0⎪
# ⎨                  2⋅a          2       ⎬
# ⎪                            4⋅a        ⎪
# ⎩                                       ⎭
#
# w1 and w2:
#        2        3
#   b₁⋅x₁    b₁⋅x₁
# - ────── + ──────
#    2⋅a         2
#             4⋅a
#              2
# b₁⋅x₂ + b₂⋅x₂
#
# w1'' and w2'':
#   b₁   3⋅b₁⋅x₁
# - ── + ───────
#   a         2
#          2⋅a
# 2⋅b₂
#
# U1, U2, Uq:
#      2
# EI⋅b₁
# ──────
#   a
#          2
# 2⋅EI⋅a⋅b₂
#  3         2
# a ⋅b₂⋅q   a ⋅b₁⋅q
# ─────── + ───────
#    3         2
#
# Parameters for U1 + U2 - Uq = Min:
# ⎧     3          2  ⎫
# ⎪    a ⋅q       a ⋅q⎪
# ⎨b₁: ────, b₂: ─────⎬
# ⎪    4⋅EI      12⋅EI⎪
# ⎩                   ⎭
#
# w2:
#  3         2     2
# a ⋅q⋅x₂   a ⋅q⋅x₂
# ─────── + ────────
#   4⋅EI     12⋅EI
#
# w2(a):
#  4
# a ⋅q
# ────
# 3⋅EI

```


```python
from sympy import *
```

# Доказать (или опровергнуть), что для известного трёхмерного вектора v единичной длины матрица является матрицей поворота.

$R = \begin{bmatrix}
\vec{k}\times(\vec{k}\times\vec{v}) & \vec{k}\times\vec{v} &  \vec{k}
\end{bmatrix}^T$

$|\vec{v}|=1$

$R = \begin{bmatrix}
 & \vec{k}\times(\vec{k}\times\vec{v}) & \\
 & \vec{k}\times\vec{v} & \\
0 & 0 & 1
\end{bmatrix}$

```python
vx, vy,vz = symbols('v_x v_y v_z')
v = Matrix([vx, vy, vz])
k = Matrix([0, 0, 1])
```

```python
k.T
```

$\displaystyle \left[\begin{matrix}0 & 0 & 1\end{matrix}\right]$

```python
v.T
```

$\displaystyle \left[\begin{matrix}v_{x} & v_{y} & v_{z}\end{matrix}\right]$

## Відновлення матриці по стрічкам

$R = \begin{bmatrix}
 & \vec{k}\times(\vec{k}\times\vec{v}) & \\
 & \vec{k}\times\vec{v} & \\
0 & 0 & 1
\end{bmatrix}$

```python
k.cross(v).T
```

$\displaystyle \left[\begin{matrix}- v_{y} & v_{x} & 0\end{matrix}\right]$

$R = \begin{bmatrix}
- & \vec{k}\times(\vec{k}\times\vec{v}) & -\\
-v_y & v_x & 0\\
0 & 0 & 1
\end{bmatrix}$

```python
k.cross( k.cross(v) ).T
```

$\displaystyle \left[\begin{matrix}- v_{x} & - v_{y} & 0\end{matrix}\right]$

$R = \begin{bmatrix}
-v_x & -v_y & 0\\
-v_y & v_x & 0\\
0 & 0 & 1
\end{bmatrix}$

```python
R = Matrix([[-vx, -vy, 0], [-vy, vx, 0], [0, 0, 1]])
R
```

$\displaystyle \left[\begin{matrix}- v_{x} & - v_{y} & 0\\- v_{y} & v_{x} & 0\\0 & 0 & 1\end{matrix}\right]$

```python
R.det()
```

$\displaystyle - v_{x}^{2} - v_{y}^{2}$

$det(R) \neq 1$, оскільки за умовою лише $v_x^2 + v_y^2 + v_z^2=1$ (ще $det(R) < 0$)

```python
R.inv() - R.T
```

$\displaystyle \left[\begin{matrix}v_{x} - \frac{v_{x}}{v_{x}^{2} + v_{y}^{2}} & v_{y} - \frac{v_{y}}{v_{x}^{2} + v_{y}^{2}} & 0\\v_{y} + \frac{v_{y}}{- v_{x}^{2} - v_{y}^{2}} & - v_{x} + \frac{v_{x}}{v_{x}^{2} + v_{y}^{2}} & 0\\0 & 0 & 0\end{matrix}\right]$

## Офіційна відповідь: матриця $R$ не є матрицею повороту (ябо є лише у випадку коли один з стовпчиків домножити на $-1$, та при умові що $v_z = 0$).

# Вывести матрицу поворота

$\begin{cases} R\vec{v} = \vec{i} \\ R^T=R^{-1} \\ det(R) = 1 \end{cases}$

Перша матриця: поворот навколо осі $Z$

```python
cos_fi = vx/sqrt(vx**2+vy**2)
sin_fi = vy/sqrt(vx**2+vy**2)

R = Matrix([[cos_fi, sin_fi, 0],
            [-sin_fi, cos_fi, 0],
            [0, 0, 1]])
R
```

$\displaystyle \left[\begin{matrix}\frac{v_{x}}{\sqrt{v_{x}^{2} + v_{y}^{2}}} & \frac{v_{y}}{\sqrt{v_{x}^{2} + v_{y}^{2}}} & 0\\- \frac{v_{y}}{\sqrt{v_{x}^{2} + v_{y}^{2}}} & \frac{v_{x}}{\sqrt{v_{x}^{2} + v_{y}^{2}}} & 0\\0 & 0 & 1\end{matrix}\right]$

Перевірка того, чи це дійсно матриця повороту

```python
R.det()
```

$\displaystyle 1$

```python
R.T - R.inv()
```

$\displaystyle \left[\begin{matrix}0 & 0 & 0\\0 & 0 & 0\\0 & 0 & 0\end{matrix}\right]$

і координата $v_y$ зануляється

```python
v2 = simplify(R*v)
v2
```

$\displaystyle \left[\begin{matrix}\sqrt{v_{x}^{2} + v_{y}^{2}}\\0\\v_{z}\end{matrix}\right]$

Друга матриця: поворот навколо осі $Y$

```python
sin_fi = vz
cos_fi = sqrt(1 - sin_fi**2)

F = Matrix([[cos_fi, 0, sin_fi],
            [0, 1, 0],
            [-sin_fi, 0, cos_fi]])
F
```

$\displaystyle \left[\begin{matrix}\sqrt{1 - v_{z}^{2}} & 0 & v_{z}\\0 & 1 & 0\\- v_{z} & 0 & \sqrt{1 - v_{z}^{2}}\end{matrix}\right]$

```python
F.det()
```

$\displaystyle 1$

```python
F.inv() - F.T
```

$\displaystyle \left[\begin{matrix}0 & 0 & 0\\0 & 0 & 0\\0 & 0 & 0\end{matrix}\right]$

і координата $v_z$ зануляється

```python
(F*v2).subs(vx**2+vy**2, 1- vz**2)
```

$\displaystyle \left[\begin{matrix}1\\0\\0\end{matrix}\right]$

## Комбінована матриця

```python
FinalR = (F*R).subs(vx**2 + vy**2, 1 - vz**2)
FinalR
```

$\displaystyle \left[\begin{matrix}v_{x} & v_{y} & v_{z}\\- \frac{v_{y}}{\sqrt{1 - v_{z}^{2}}} & \frac{v_{x}}{\sqrt{1 - v_{z}^{2}}} & 0\\- \frac{v_{x} v_{z}}{\sqrt{1 - v_{z}^{2}}} & - \frac{v_{y} v_{z}}{\sqrt{1 - v_{z}^{2}}} & \sqrt{1 - v_{z}^{2}}\end{matrix}\right]$

Перевірка множенням на $\vec{v}$

```python
simplify(FinalR*v)
```

$\displaystyle \left[\begin{matrix}v_{x}^{2} + v_{y}^{2} + v_{z}^{2}\\0\\\frac{v_{z} \left(- v_{x}^{2} - v_{y}^{2} - v_{z}^{2} + 1\right)}{\sqrt{1 - v_{z}^{2}}}\end{matrix}\right]$

Спрощення виразу і підстановка $v_x^2 + v_y^2 + v_z^2 =1$

```python
simplify(FinalR*v).subs(vx**2 + vy**2 + vz**2, 1)
```

$\displaystyle \left[\begin{matrix}1\\0\\0\end{matrix}\right]$

Поворот назад

```python
FinalR.inv()*Matrix([1,0,0])
```

$\displaystyle \left[\begin{matrix}- \frac{v_{x} v_{z}^{2} - v_{x}}{v_{x}^{2} + v_{y}^{2}}\\- \frac{v_{y} v_{z}^{2} - v_{y}}{v_{x}^{2} + v_{y}^{2}}\\v_{z}\end{matrix}\right]$

Спрощення виразу і підстановка $v_x^2 + v_y^2 = 1- v_z^2$

```python
simplify(FinalR.inv()*Matrix([1,0,0])).subs(vx**2 + vy**2, 1 - vz**2)
```

$\displaystyle \left[\begin{matrix}v_{x}\\v_{y}\\v_{z}\end{matrix}\right]$

```python

```


# Introduction and demonstration of OSTL

This tutorial starts from a short description of BPTT, followed by a discussion of its shortcomings. Next, we introduce OSTL as a biologically-inspired alternative to BPTT. We demonstrate its basic operational principle and showcase the gradient-equivalence for certain scenarios, using here an example of a simple task of learning the XOR function. In particular, this tutorial:

1. Introduces Online Spatio-Temporal Learning (OSTL) as an alternative to back-propagation through time (BPTT)
2. Illustrates the basic working principle of OSTL
3. Demonstrates gradient-equivalence for LSTM and bio-inspired SNU units

This tutorial is based on the paper
**Bohnstingl, T., Woźniak, S., Maass, W., Pantazi, A., & Eleftheriou, E. (2020). Online Spatio-Temporal Learning in Deep Neural Networks. arXiv, 2007.12723. https://arxiv.org/abs/2007.12723v2**

## Back-propagation through time (BPTT)

Almost all recurrent neural networks (RNN) used in machine learning operating on temporal input data are trained with the error back-propagation through time algorithm. This algorithm works by unrolling the RNN over time, i.e., the network is replicated for each individual discrete time step. The video below illustrates this process for a two-layer RNN which receives a phrase of French words as an input sequence: "Conseil Européen pour la Recherche Nucléaire", and is given the task to translate this three-step long sentence into its English version: "European Council for Nuclear Research".

At first, the network is unrolled in time, i.e., the network is replicated for three time steps. Then, the inputs for these time steps are fed sequentially and the output is computed for each time step separately. Once the entire input sequence has been processed, the network operation is interrupted and the gradients of the error with respect to the parameters are propagated back through time.

$$
\newcommand{\dv}[2]{\frac{\textrm{d}#1}{\textrm{d}#2}}
\newcommand{\pdv}[2]{\frac{\partial #1}{\partial #2}}
\begin{align}
\Delta {\theta}_l &= -\eta \dv{E}{{\theta}_l}\\
&= -\eta \sum_{1 \leq t \leq T} \dv{E_k^t}{{\theta}_l}
\end{align}$$

It is important to note that the error is typically only provided at the output layer *k* and has to propagate through the units in the network to reach all parameters. For example, if a network is composed of two layers with simple RNN cells:

$$
\newcommand{\dv}[2]{\frac{\textrm{d}#1}{\textrm{d}#2}}
\newcommand{\pdv}[2]{\frac{\partial #1}{\partial #2}}
\begin{align}
{s}_l^t &= g({W}_l x^t + {H}_l y^t + d \cdot (1- y_l^{t-1}) \cdot {s}_l^{t-1}),\\
{y}_l^t &= {\sigma}({s}^t + {b}_l),
\end{align}$$

with $${\theta}_l=\{{W}_l, {H}_l, {b}_l\}$$.

### Disadvantages and drawbacks
- In order to compute $\Delta {\theta}_l$, the input sequence has to be observed till its last time step and the intermediate activations need to be stored
- In consequence, memory requirements grow linearly with length of input sequence *T*
- Separate phases for the forward propagation and the gradient computation are required
- $\Delta {\theta}_l$ contains a complex combination of *spatial* and *temporal* gradients

## Online Spatio-Temporal Learning (OSTL)

A way to tackle the shortcomings of BPTT is proposed in OSTL. This algorithm also makes use of gradient computations, but the way those computations are done is much inspired by findings from biological systems.
In particular, as the following Figure illustrates, three factors are found in biology to modulate the change of synaptic weights (plasticity):
1. Pre-synaptic activity
2. Post-synaptic activity
3. Learning signals, so called-third factors

Inspired by this, we assign to each synapse a so-called eligibility trace
$$
\newcommand{\dv}[2]{\frac{\textrm{d}#1}{\textrm{d}#2}}
\newcommand{\pdv}[2]{\frac{\partial #1}{\partial #2}}
\begin{align}
\mathbf{e}^{t,{\theta}_l}_{k} &= \pdv{{y}^t_{k}}{{s}^t_{k}} {\epsilon}^{t,{\theta}_l}_{k} + \pdv{{y}^t_{k}}{{\theta}_{l}},
\end{align}$$

with $$
\newcommand{\dv}[2]{\frac{\textrm{d}#1}{\textrm{d}#2}}
\newcommand{\pdv}[2]{\frac{\partial #1}{\partial #2}}
\newcommand\myeq{\stackrel{\mbox{def}}{=}}
\begin{align}
{\epsilon}_k^{t,{\theta}_l} &\myeq \dv{{s}_k^t}{{\theta}_l}.
\end{align}$$

Importantly, this eligibility trace holds information of the pre- and post-synaptic activity and carries this information through time, hence those are the spatial gradient components. Furthermoer, we define the learning signal, which transports information from the environment to the synapses, to be
$$
\newcommand{\dv}[2]{\frac{\textrm{d}#1}{\textrm{d}#2}}
\newcommand{\pdv}[2]{\frac{\partial #1}{\partial #2}}
\begin{align}
\mathbf{L}^t_{l} &= \pdv{E^t}{{y}_k^t} \left( \prod_{(k-l+1) > m \ge 1} \pdv{{y}^t_{k-m+1}}{{s}^t_{k-m+1}} \pdv{{s}^t_{k-m+1}}{{y}^t_{k-m}} \right).
\end{align}$$

The learning signal transports information from the environment to the individual layers of the network *l*, hence those are the temporal components of the gradients.
To finally compute the gradients for the trainable parameters, the eligitbility traces and the learning signals are combined at each time step (online) according to
$$
\newcommand{\dv}[2]{\frac{\textrm{d}#1}{\textrm{d}#2}}
\newcommand{\pdv}[2]{\frac{\partial #1}{\partial #2}}
\begin{align}
\Delta {\theta}_l &= -\eta \sum_{1 \leq t \leq T} \left( \mathbf{L}_l^t \mathbf{e}_l^{t,{\theta}_l} + \mathbf{R}\right),
\end{align}
$$

which gives rise to the name "Online Spatio-Temporal learning".
The working pricinple of OSTL is illustrated in the animation below.

## Implementation

We start by importing required python packages and by defining an auxiliary function for data generation.

```python
#General imports
import collections
import argparse
import time
import pickle
import Scripts.Utils_Persistence as Utils_Persistence
import numpy as np
import tensorflow as tf
from tensorflow.python.ops import rnn_cell_impl

#This function creates data from the XOR function
def createXorData(bs, seq_len):
    x_val = np.tile(np.expand_dims([[1], [0], [1], [1], [0], [0], [1], [1], [0], [1]], 0), (bs, 1, 1))
    y_val = np.tile(np.expand_dims([[0], [1], [1], [1], [0], [0], [0], [0], [1], [0]], 0), (bs, 1, 1))

    return x_val, y_val
```

    C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\framework\dtypes.py:526: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
      _np_qint8 = np.dtype([("qint8", np.int8, 1)])
    C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\framework\dtypes.py:527: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
      _np_quint8 = np.dtype([("quint8", np.uint8, 1)])
    C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\framework\dtypes.py:528: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
      _np_qint16 = np.dtype([("qint16", np.int16, 1)])
    C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\framework\dtypes.py:529: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
      _np_quint16 = np.dtype([("quint16", np.uint16, 1)])
    C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\framework\dtypes.py:530: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
      _np_qint32 = np.dtype([("qint32", np.int32, 1)])
    C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\framework\dtypes.py:535: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
      np_resource = np.dtype([("resource", np.ubyte, 1)])

```python
#OSTL specific inputs
from Scripts.RNN_loop import dynamic_rnn
from Scripts.MultiRNNCell import MultiRNNCell
from Scripts.gradient_descent import GradientDescentOptimizer as opt
from Scripts.Neuron_models import SNUELayer
```

### Define configuration of the network

In this section, the network configuration is defined. For example, the type of unit to run may be selected. One of the options is to use soft spiking neural units (sSNU). These units have been introduced in the paper **Woźniak, S., Pantazi, A., Bohnstingl, T., & Eleftheriou, E. (2020). Deep learning incorporating biologically inspired neural dynamics and in-memory computing - Nature Machine Intelligence. Nat. Mach. Intell., 2(6), 325–336. doi: 10.1038/s42256-020-0187-0**

```python
config = {'lr': 0.01,
          'batch_size': 1,
          'cell': 'sSNU',
          'seq_len': 10,
          'save_state': True,
         }
```

As the first step, we generate the data that will later be used for training and for testing. This tutorial illustrates solving a fixed input/output relation using the sSNU. The following animation demonstrates this task:

```python
#Fix the random seed in order to be able to numerically
#compare the computed gradients
np.random.seed(1234)
tf.set_random_seed(1234)

#We generate "config['batch_size']" examples with a length of 100 for testing
val_data_x_val, val_data_y_val = createXorData(config['batch_size'], config['seq_len'])

#We generate data roughly 2000 examples with a length of 100 for traininging
train_data_x_val, train_data_y_val = createXorData(int(2000/config['batch_size']), config['seq_len'])
train_data_x_val.shape

#Generate test data
test_data_x_val, test_data_y_val = createXorData(config['batch_size'], config['seq_len'])
```

Next we can also briefly visualize the data

```python
import matplotlib
matplotlib.rcParams['text.usetex'] = True
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = [20, 10]
from matplotlib import patches
from matplotlib import cm
matplotlib.rcParams.update({'font.size': 22})
```

```python
fig, ax = plt.subplots(1, 1)
for x in np.where(train_data_x_val[10, :20, 0])[0]:
    ax.axvline(x=x, ymin=0.03, ymax=0.25, color='C0', linewidth=6.)
p = patches.Rectangle((-0.2, -0.1), 0.4+np.max(np.where(train_data_x_val[10, :20, :])[0]), 1, linewidth=1, edgecolor='none', facecolor='C0', alpha=0.2)
ax.add_patch(p)
for x in np.where(train_data_y_val[10, :20, 0])[0]:
    ax.axvline(x=x, ymin=0.7, ymax=0.9, color='C2', linewidth=6.)
p = patches.Rectangle((-0.2, 2.3), 0.4+np.max(np.where(train_data_x_val[10, :20, :])[0]), 1, linewidth=1, edgecolor='none', facecolor='C2', alpha=0.2)
ax.add_patch(p)

ax.set_xlim([-5., np.max(np.where(train_data_x_val[10, :20, :])[0])+5.])
ax.set_xticks(np.arange(0., np.max(np.where(train_data_x_val[10, :20, :])[0])+5.))
ax.set_xlabel('Timestep')
ax.set_ylim([-0.1,3.5])
ax.set_yticklabels(['Input 1', 'Input 2', 'Ouptut'])
ax.set_yticks([0.45, 1.6, 2.7])
```

### Preparation of the network architecture

<Put an illustration of the network architecture here>

Let's define some placeholders

```python
#Define the placeholders to construct the network
x = tf.placeholder(tf.float32, [None, None, 1])  #(batchsize, timesteps, number of units)
target = tf.placeholder(tf.float32, [None, None, 1])
train = tf.placeholder_with_default(tf.constant(1, tf.int32), ())
```

Let's define the units to be used

```python
#List containing the RNN units
rnn_layers_BPTT = []
rnn_layers_OSTL = []

#Create fixed random initializers for reproducibility
initW = np.random.uniform(-np.sqrt(6) / np.sqrt(1 + 1), np.sqrt(6) / np.sqrt(1 + 1), size=(1, 1))
initH = np.random.uniform(-np.sqrt(6) / np.sqrt(1 + 1), np.sqrt(6) / np.sqrt(1 + 1), size=(1, 1))

#Create the SNU layer for BPTT and for OSTL
rnn_layers_BPTT.append(SNUELayer(1, 1, activation=tf.nn.sigmoid, recurrent=True, decay=0., g=tf.identity, initW=initW, initH=initH, name='BPTT'))
rnn_layers_OSTL.append(SNUELayer(1, 1, activation=tf.nn.sigmoid, recurrent=True, decay=0., g=tf.identity, initW=initW, initH=initH, name='OSTL'))
'''
#Create the SNU layer for BPTT and for OSTL
rnn_layers_BPTT.append(SNUELayer(1, 1, activation=tf.nn.sigmoid, recurrent=True, decay=0., g=tf.identity, name='BPTT'))
rnn_layers_OSTL.append(SNUELayer(1, 1, activation=tf.nn.sigmoid, recurrent=True, decay=0., g=tf.identity, name='OSTL'))
'''
```

    "\n#Create the SNU layer for BPTT and for OSTL\nrnn_layers_BPTT.append(SNUELayer(1, 1, activation=tf.nn.sigmoid, recurrent=True, decay=0., g=tf.identity, name='BPTT'))\nrnn_layers_OSTL.append(SNUELayer(1, 1, activation=tf.nn.sigmoid, recurrent=True, decay=0., g=tf.identity, name='OSTL'))\n"

Define the optimizer to be used

```python
optimizer = opt(learning_rate=config['lr'])
```

Next, we define the loss function and the optimization step

```python
#Define the loss function to be used in OSTL
def loss_function(target_t, output_t, time, layer):
    '''
    Computes the desired loss for a single timestep
    '''
    loss = tf.reduce_sum(tf.cast(tf.squared_difference(output_t, target_t), dtype=tf.float32))
    loss = tf.reduce_sum(loss)
    return loss

#Define the gradient function to compute the learning signal
def gradient_function(loss, output_t, time, last_time, layer):
    '''
    Computes the learning signal used for OSTL
    '''
    grads = tf.gradients(loss, output_t)[0]
    return grads

#Optimization step for BPTT
multi_rnn_cell_BPTT = tf.nn.rnn_cell.MultiRNNCell(rnn_layers_BPTT)
out_BPTT, state_BPTT = tf.nn.dynamic_rnn(multi_rnn_cell_BPTT, x, dtype=tf.float32)
loss_BPTT = loss_function(target, out_BPTT, None, 0)
train_step_BPTT = optimizer.minimize(loss_BPTT)

#Optimization step for OSTL
multi_rnn_cell_OSTL = MultiRNNCell(rnn_layers_OSTL)
out_OSTL, state_OSTL = dynamic_rnn(multi_rnn_cell_OSTL, x, target, loss_function, gradient_function, train, 1, 0, -1, optimizer, dtype=tf.float32)
loss_OSTL = loss_function(target, out_OSTL, None, 0)
train_step_OSTL = tf.no_op()
```

    WARNING:tensorflow:From C:\Users\THOMAS~1\AppData\Local\Temp/ipykernel_16928/4109756845.py:19: MultiRNNCell.__init__ (from tensorflow.python.ops.rnn_cell_impl) is deprecated and will be removed in a future version.
    Instructions for updating:
    This class is equivalent as tf.keras.layers.StackedRNNCells, and will be replaced by that in Tensorflow 2.0.
    WARNING:tensorflow:From C:\Users\THOMAS~1\AppData\Local\Temp/ipykernel_16928/4109756845.py:20: dynamic_rnn (from tensorflow.python.ops.rnn) is deprecated and will be removed in a future version.
    Instructions for updating:
    Please use `keras.layers.RNN(cell)`, which is equivalent to this API
    WARNING:tensorflow:From C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\ops\tensor_array_ops.py:162: colocate_with (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
    Instructions for updating:
    Colocations handled automatically by placer.

### Train the network

```python
#Array to track the gradients of the individual weights
vals_BPTT = []
vals_OSTL = []

vals_tensor_length = int(len(tf.trainable_variables())/2)
vals_tensor_BPTT = tf.trainable_variables()[:vals_tensor_length]
vals_tensor_OSTL = tf.trainable_variables()[vals_tensor_length:]

#Define the feed_dictionary for testing
feed_dict_val = {x: val_data_x_val,
                 target: val_data_y_val,
                 train: 0.}

#Define a TF session
s = tf.Session()

#Run the initialization of the network
s.run(tf.global_variables_initializer())

#Store the initial weights of the network
init_weights_BPTT = s.run(vals_tensor_BPTT)
init_weights_OSTL = s.run(vals_tensor_OSTL)

#Iterate over the training data
iterations = int(2000/config['batch_size'])

#Try to load a checkpoint if any exists
resume = Utils_Persistence.LoadObject('./checkpoints/GradEqu_' + config['cell'])
if resume is not None:
    print('Loading checkpoint')
    itrStart = resume['itr']
    np.random.set_state(resume['rng'])
    Utils_Persistence.PersistenceLoadThis(s, 'GradEqu_' + config['cell'] + '_e' + str(itrStart), withGraph=False)
    if itrStart >= iterations:
        print("State loaded: already finished training at itr", itrStart, ". Plotting & exiting.")
        exit(0)
    else:
        print('State loaded: resuming the learning from itr', itrStart)
    not_loaded = False
else:
    print("Did not find checkpoint. Run from scratch")
    itrStart = 0
    not_loaded = True

for itr in range(itrStart, iterations):

    #Define the feed dictionary for training
    feed_dict = {x: train_data_x_val[(config['batch_size']*itr):(config['batch_size']*(itr+1)), :],
                 target: train_data_y_val[(config['batch_size']*itr):(config['batch_size']*(itr+1)), :],
                 train: 1.}

    #Run a single training step
    o_BPTT, o_OSTL, _, _ = s.run([out_BPTT, out_OSTL, train_step_BPTT, train_step_OSTL], feed_dict=feed_dict)

    #Store the intermediate values of the parameters
    vals_BPTT.append(s.run(vals_tensor_BPTT))
    vals_OSTL.append(s.run(vals_tensor_OSTL))

    #Evaluate the model on the testset every 100 iterations
    if itr % 100 == 0:
        l_BPTT, l_OSTL = s.run([loss_BPTT, loss_OSTL], feed_dict=feed_dict_val)
        print('(' + str(itr + 100) + '/' + str(iterations) + ') Loss (BPTT): ' + str(l_BPTT) + ', Loss (OSTL): ' + str(l_OSTL))

vals_BPTT.append(s.run(vals_tensor_BPTT))
vals_OSTL.append(s.run(vals_tensor_OSTL))

#Save the final checkpoint
if not_loaded and 'GradEqu_' + config['cell'] is not None and config['save_state']:
    Utils_Persistence.PersitenceSaveThis(s, 'GradEqu_' + config['cell'] + '_e' + str(itr))
    Utils_Persistence.SaveObject({'itr': itr, 'rng': np.random.get_state(),
                                  'vals_BPTT': vals_BPTT, 'vals_OSTL': vals_OSTL},
                                  './checkpoints/GradEqu_' + config['cell'])
    print('State saved at iteration ' + str(itr))
```

    Loading checkpoint
    WARNING:tensorflow:From C:\Users\ThomasBohnstingl\.conda\envs\TF\lib\site-packages\tensorflow\python\training\saver.py:1266: checkpoint_exists (from tensorflow.python.training.checkpoint_management) is deprecated and will be removed in a future version.
    Instructions for updating:
    Use standard file APIs to check for files with this prefix.
    INFO:tensorflow:Restoring parameters from ./checkpoints/GradEqu_sSNU_e1999
    State loaded: resuming the learning from itr 1999

### Evaluate the network

Load stored checkpoints

```python
#Try to load a checkpoint if any exists
resume = Utils_Persistence.LoadObject('./checkpoints/GradEqu_' + config['cell'])
if resume is not None:
    print('Loading checkpoint')
    itrStart = resume['itr']
    vals_BPTT = resume['vals_BPTT']
    vals_OSTL = resume['vals_OSTL']
    np.random.set_state(resume['rng'])
    Utils_Persistence.PersistenceLoadThis(s, 'GradEqu_' + config['cell'] + '_e' + str(itrStart), withGraph=False)
    print('State loaded: resuming the learning from itr', itrStart)
else:
    print("Did not find checkpoint. Please run model from scratch!")
```

    Loading checkpoint
    INFO:tensorflow:Restoring parameters from ./checkpoints/GradEqu_sSNU_e1999
    State loaded: resuming the learning from itr 1999

After the model has been trained for a while, the model can be tested on unseed data

```python
print('Demo XOR data')
print('Input: ' + str(test_data_x_val[0, :, 0].flatten().astype(np.int32)) + '\n')
print('Ouput: ' + str(test_data_y_val[0, :, 0].flatten()))

#Create the new feed_dict
feed_dict_test = {x: test_data_x_val,
                  target: test_data_y_val,
                  train: 0.}
o_BPTT, o_OSTL = s.run([out_BPTT, out_OSTL], feed_dict=feed_dict_test)
print('BPTT:  ' + str((o_BPTT[0] > 0.5).astype(np.int32).flatten()) + '\nOSTL:  ' + str((o_OSTL[0] > 0.5).astype(np.int32).flatten()))
```

    Demo XOR data
    Input: [1 0 1 1 0 0 1 1 0 1]

    Ouput: [0 1 1 1 0 0 0 0 1 0]
    BPTT:  [0 1 0 0 1 1 0 0 1 0]
    OSTL:  [0 1 0 0 1 1 0 0 1 0]

### Investigate the gradients

In this simple scenario of a single-layered RNN, it can be shown that the gradients of OSTL and BPTT are equivalent. This is empirically demonstrated below. First, a random element of the input weights is selected and then analyzed.

```python
#Pick randomly entries from the gradients for each trainable parameter
param_ind = []
for param in tf.trainable_variables()[:vals_tensor_length]:
    param_ind.append(np.random.randint(0, int(np.prod(param.shape))))
```

The figure below illustrates the evolution of the learning signal and of the eligibility trace. Those two quantities combined together form the gradient updates for the parameters. One can see in the third panel that the gradient updates of BPTT and of OSTL align.

```python
fig, ax = plt.subplots(1, 1)
for ind, param in enumerate(tf.trainable_variables()[:1]):
    data_BPTT = [p.flatten()[param_ind[ind]] for p in [g[ind] for g in vals_BPTT]][:100]
    data_OSTL = [p.flatten()[param_ind[ind]] for p in [g[ind] for g in vals_OSTL]][:100]
    line = ax.plot(np.arange(len(data_OSTL)), data_OSTL, label=r'$\theta_' + str(ind) + '$ OSTL')
    ax.scatter(np.arange(len(data_BPTT)), data_BPTT, marker='x', s=200., color=line[0].get_color(), label=r'$\theta_' + str(ind) + '$ BPTT')
ax.set_ylabel(r'Value of $\theta$')
ax.set_xlabel('Time')
ax.legend()
```


# Solow-Model with human capital and distorting taxation

**Importing relevant packages and modules**

```python
import numpy as np
from scipy import optimize
import sympy as sm
import matplotlib.pyplot as plt

# autoreload modules when code is run
%load_ext autoreload
%autoreload 2
#XD FOR SMART UPLOAD
# local modules
# import modelproject if code is used form another notebook or file
```

# Model description

The following solow-model incorperates human capital accumulation aswell as distorting taxation.

The model consists of the following equations:

1 $$ (1-\tau)^{\eta}L_t, \text{   } 0<\eta<1,$$
2 $$H_{t+1}={\tau}w_tN_t+(1-{\delta})H_t $$
3 $$K_{t+1}=s_kY_t+(1-{\delta})K_t $$
4 $$K^\alpha_{t}H^\beta_{t}(A_{t}N_{t})^{1-\alpha-\beta} $$
5 $$H_t=h_{t}N_t $$
6 $$L_{t+1}=(1+n)L_t $$
7 $$A_{t+1}=(1+g)A_t $$

* $K_t$ is capital in period t
* $L_t$ is labor i period t (with a constant growth rate of $n$)
* $H_t$ is human capital in period t
* $A_t$ is technology in period t (with a constant growth rate of $g$)
* $N_t$ is the total workhours supplied in the economy in period t
* $Y_t$ is GDP

The model contains the following parameters:$$(\eta,\tau,s_k,\delta,\alpha,\beta,n,g) $$

Using the equations above, we can find the two transitioncurves given by:
$$ \tilde{k}_{t+1}=\left(\frac{1}{(1+n)(1+g}\right)[s_{k}\tilde{k}^{\alpha}_{t}\tilde{h}^{\beta}_{t}(1-\tau)^{\eta(1+\alpha)}+(1+\delta)\tilde{k}_{t}] $$
and
$$ \tilde{h}_{t+1}=\left(\frac{1}{(1+n)(1+g}\right)[\tau(1-\alpha)\tilde{k}^{\alpha}_{t}\tilde{h}^{\beta}_{t}(1-\tau)^{\eta(1+\alpha)}+(1+\delta)\tilde{h}_{t}] $$
where $\tilde{k}_{t}$ is the capital per effective worker given by $\tilde{k}_{t}=\frac{Y}{AK}$
and
$\tilde{h}_{t}$ is the human capital per effective worker given by $\tilde{h}_{t}=\frac{Y}{AK}$

```python

```


# Keypoints in this Section

- slice is a view but not a copy

- using `axis` to calculate aggregate function on margins
    - using dummy axis `a[:, None]` to expand the vector

- Generating Arrays
    - `np.fromfunction()`

        - `lambda i, j: 2*i+j, (2, 3)`

        - `lambda i, j: np.where(i<j, 1, np.where(i==j, 0, -1)`

    - `np.eye(3, 4, k = -1)`, `np.diag()`

    - `np.random.rand()`, `np.random,randn()`

- Expanding Arrays
    - Use `np.c_[]` and `np.r_` to combine arrays

    - `np.stack`: see [SciPy.org](https://docs.scipy.org/doc/numpy-1.15.0/reference/generated/numpy.stack.html)

    - `np.repeat()` and `np.tile()`

- Dividng Arrays
    - `np.split()`

# Numbers

```python
%matplotlib inline
```

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

```python
import numpy as np
```

## The `ndarray`: Vectors, matrices and tenosrs

dtype, shape, strides

### Vector

```python
x = np.array([1,2,3])
x
```

    array([1, 2, 3])

```python
type(x)
```

    numpy.ndarray

```python
x.dtype
```

    dtype('int64')

```python
x.shape
```

    (3,)

```python
x.strides
```

    (8,)

### Matrix

```python
x = np.array([[1,2,3], [4,5,6]], dtype=np.int32)
x
```

    array([[1, 2, 3],
           [4, 5, 6]], dtype=int32)

```python
x.dtype
```

    dtype('int32')

```python
x.shape
```

    (2, 3)

```python
x.strides
```

    (12, 4)

### Tensor

```python
x = np.arange(24).reshape((2,3,4))
```

```python
x
```

    array([[[ 0,  1,  2,  3],
            [ 4,  5,  6,  7],
            [ 8,  9, 10, 11]],

           [[12, 13, 14, 15],
            [16, 17, 18, 19],
            [20, 21, 22, 23]]])

## Creating `ndarray`s

### From a file

```python
%%file numbers.txt
a,b,c # can also skip headers
1,2,3
4,5,6
```

    Overwriting numbers.txt

```python
np.loadtxt('numbers.txt', dtype='int', delimiter=',',
           skiprows=1, comments='#')
```

    array([[1, 2, 3],
           [4, 5, 6]])

### From Python lists or tuples

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

    array([[1, 2, 3],
           [4, 5, 6]])

### From ranges

arange, linspace, logspace

```python
np.arange(1, 7).reshape((2,3))
```

    array([[1, 2, 3],
           [4, 5, 6]])

```python
np.linspace(1, 10, 4)
```

    array([ 1.,  4.,  7., 10.])

```python
np.logspace(0, 4, 5, dtype='int')
```

    array([    1,    10,   100,  1000, 10000])

### From a function

`fromfunciton`

```python
np.fromfunction(lambda i, j: i*3 + j + 1, (2,3))
```

    array([[1., 2., 3.],
           [4., 5., 6.]])

```python
np.fromfunction(lambda i, j: (i-2)**2 + (j-2)**2, (5,5), dtype='int')
```

    array([[8, 5, 4, 5, 8],
           [5, 2, 1, 2, 5],
           [4, 1, 0, 1, 4],
           [5, 2, 1, 2, 5],
           [8, 5, 4, 5, 8]])

#### How to visualize `fromfunction`

```python
j = np.repeat([np.arange(5)], 5, axis=0)
i = j.T
```

```python
i
```

    array([[0, 0, 0, 0, 0],
           [1, 1, 1, 1, 1],
           [2, 2, 2, 2, 2],
           [3, 3, 3, 3, 3],
           [4, 4, 4, 4, 4]])

```python
j
```

    array([[0, 1, 2, 3, 4],
           [0, 1, 2, 3, 4],
           [0, 1, 2, 3, 4],
           [0, 1, 2, 3, 4],
           [0, 1, 2, 3, 4]])

```python
(i-2)**2 + (j-2)**2
```

    array([[8, 5, 4, 5, 8],
           [5, 2, 1, 2, 5],
           [4, 1, 0, 1, 4],
           [5, 2, 1, 2, 5],
           [8, 5, 4, 5, 8]])

#### Using element-wise functions in `fromfunction`

```python
np.fromfunction(lambda i, j: np.where(i==j,0, -1), (5,5))
```

    array([[ 0, -1, -1, -1, -1],
           [-1,  0, -1, -1, -1],
           [-1, -1,  0, -1, -1],
           [-1, -1, -1,  0, -1],
           [-1, -1, -1, -1,  0]])

```python
np.fromfunction(lambda i, j: np.where(i<j, 1, np.where(i==j,0, -1)), (5,5))
```

    array([[ 0,  1,  1,  1,  1],
           [-1,  0,  1,  1,  1],
           [-1, -1,  0,  1,  1],
           [-1, -1, -1,  0,  1],
           [-1, -1, -1, -1,  0]])

```python
np.fromfunction(lambda i, j: np.minimum(i,j), (5,5), dtype='int')
```

    array([[0, 0, 0, 0, 0],
           [0, 1, 1, 1, 1],
           [0, 1, 2, 2, 2],
           [0, 1, 2, 3, 3],
           [0, 1, 2, 3, 4]])

```python
np.fromfunction(lambda i, j: np.maximum(i,j), (5,5), dtype='int')
```

    array([[0, 1, 2, 3, 4],
           [1, 1, 2, 3, 4],
           [2, 2, 2, 3, 4],
           [3, 3, 3, 3, 4],
           [4, 4, 4, 4, 4]])

### From special constructors

zeros, ones, eye, diag

```python
np.zeros((2,3))
```

    array([[0., 0., 0.],
           [0., 0., 0.]])

```python
np.ones((2,3))
```

    array([[1., 1., 1.],
           [1., 1., 1.]])

```python
np.eye(3)
```

    array([[1., 0., 0.],
           [0., 1., 0.],
           [0., 0., 1.]])

```python
np.eye(3, 4)
```

    array([[1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.]])

```python
np.eye(4, k=-1)
```

    array([[0., 0., 0., 0.],
           [1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.]])

```python
np.diag([1,2,3,4])
```

    array([[1, 0, 0, 0],
           [0, 2, 0, 0],
           [0, 0, 3, 0],
           [0, 0, 0, 4]])

```python
np.diag([1,2,3,4], k=1)
```

    array([[0, 1, 0, 0, 0],
           [0, 0, 2, 0, 0],
           [0, 0, 0, 3, 0],
           [0, 0, 0, 0, 4],
           [0, 0, 0, 0, 0]])

### From random variables

#### Convenience functions

rand, randn

```python
np.random.rand(2,3)
```

    array([[0.00460297, 0.6790471 , 0.66693293],
           [0.46481665, 0.68104561, 0.47717252]])

```python
np.random.randn(2,3)
```

    array([[ 0.32728775, -0.7593871 ,  2.00243124],
           [-0.45911955, -2.29433348, -0.62233783]])

#### Distributions

uniform, normal, randint, poisson, multinomial, multivariate_ normal

```python
np.random.uniform(0, 1, (2,3))
```

    array([[0.35471781, 0.68890474, 0.69249992],
           [0.89704091, 0.84881458, 0.55436389]])

```python
np.random.normal(0, 1, (2,3))
```

    array([[ 0.38938858,  0.02046035,  1.12586858],
           [ 1.55033516,  1.42547904, -1.02902684]])

```python
np.random.randint(0, 10, (4,5))
```

    array([[3, 8, 3, 9, 6],
           [9, 7, 9, 0, 2],
           [5, 3, 0, 4, 4],
           [8, 2, 7, 3, 0]])

```python
np.random.poisson(10, (4,5))
```

    array([[12,  5, 11,  8, 11],
           [ 8, 13, 12,  9, 16],
           [10, 16, 17,  7, 14],
           [15,  5, 10, 10,  6]])

```python
np.random.multinomial(n=5, pvals=np.ones(5)/5, size=8)
```

    array([[2, 1, 0, 1, 1],
           [1, 1, 2, 0, 1],
           [1, 1, 1, 1, 1],
           [1, 2, 0, 1, 1],
           [1, 1, 1, 1, 1],
           [0, 0, 3, 0, 2],
           [0, 1, 2, 1, 1],
           [1, 3, 0, 1, 0]])

```python
np.random.multivariate_normal(mean=[10,20,30], cov=np.eye(3), size=4)
```

    array([[ 8.46959709, 19.70106618, 30.96751184],
           [ 7.750921  , 21.15980554, 30.28468523],
           [ 8.22849672, 20.22017755, 29.69429188],
           [ 9.715338  , 18.56547751, 29.56025953]])

## Indexing

```python
x = np.arange(20).reshape((4,5))
x
```

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

### Extracing a scalar

```python
x[1,1]
```

    6

### Extracting a vector

```python
x[1]
```

    array([5, 6, 7, 8, 9])

### Using slices

```python
x[1,:]
```

    array([5, 6, 7, 8, 9])

```python
x[:,1]
```

    array([ 1,  6, 11, 16])

```python
x[1:3,1:3]
```

    array([[ 6,  7],
           [11, 12]])

### Using slices with strides

```python
x[::2,::2]
```

    array([[ 0,  2,  4],
           [10, 12, 14]])

### Extrcting blocks with arbitrary row and column lists (fancy indexing)

`np.ix_`

```python
x[:, [0,3]]
```

    array([[ 0,  3],
           [ 5,  8],
           [10, 13],
           [15, 18]])

Warning: Fancy indexing can only be used for 1 dimension at a time.

In the example below, `numpy` treats the arguments as *paired* coordinates, and returns the values at (0,0) and (2,3).

```python
x[[0,2],[0,3]]
```

    array([ 0, 13])

Use the helper `np.ix_` to extract arbitrary blocks.

```python
x[np.ix_([0,2], [0,3])]
```

    array([[ 0,  3],
           [10, 13]])

### A slice is a view, not a copy

```python
x
```

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

```python
y = x[1:-1, 1:-1]
y
```

    array([[ 6,  7,  8],
           [11, 12, 13]])

```python
y *= 10
```

```python
y
```

    array([[ 60,  70,  80],
           [110, 120, 130]])

```python
x
```

    array([[  0,   1,   2,   3,   4],
           [  5,  60,  70,  80,   9],
           [ 10, 110, 120, 130,  14],
           [ 15,  16,  17,  18,  19]])

Use the copy method to convert a view to a copy

```python
z = x[1:-1, 1:-1].copy()
```

```python
z
```

    array([[ 60,  70,  80],
           [110, 120, 130]])

```python
z[:] = 0
```

```python
z
```

    array([[0, 0, 0],
           [0, 0, 0]])

```python
x
```

    array([[  0,   1,   2,   3,   4],
           [  5,  60,  70,  80,   9],
           [ 10, 110, 120, 130,  14],
           [ 15,  16,  17,  18,  19]])

### Boolean indexing

```python
x[x % 2 == 0]
```

    array([  0,   2,   4,  60,  70,  80,  10, 110, 120, 130,  14,  16,  18])

```python
x [x > 3]
```

    array([  4,   5,  60,  70,  80,   9,  10, 110, 120, 130,  14,  15,  16,
            17,  18,  19])

```python
x > 3
```

    array([[False, False, False, False,  True],
           [ True,  True,  True,  True,  True],
           [ True,  True,  True,  True,  True],
           [ True,  True,  True,  True,  True]])

### Functions that return indexes

```python
idx = np.nonzero(x)
idx
```

    (array([0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3]),
     array([1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4]))

```python
x[idx]
```

    array([  1,   2,   3,   4,   5,  60,  70,  80,   9,  10, 110, 120, 130,
            14,  15,  16,  17,  18,  19])

```python
idx = np.where(x > 3)
idx
```

    (array([0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3]),
     array([4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4]))

```python
x[idx]
```

    array([  4,   5,  60,  70,  80,   9,  10, 110, 120, 130,  14,  15,  16,
            17,  18,  19])

## Margins and the `axis` argument

```python
x
```

    array([[  0,   1,   2,   3,   4],
           [  5,  60,  70,  80,   9],
           [ 10, 110, 120, 130,  14],
           [ 15,  16,  17,  18,  19]])

The 0th axis has 4 items, the 1st axis has 5 items.

```python
x.shape
```

    (4, 5)

```python
x.mean()
```

    35.15

### Marginalizing out the 0th axis = column summaries

```python
x.mean(axis=0)
```

    array([ 7.5 , 46.75, 52.25, 57.75, 11.5 ])

### Marginalizing out the 1st axis = row summaries

```python
x.mean(axis=1)
```

    array([ 2. , 44.8, 76.8, 17. ])

Note marginalizing out the last axis is a common default.

```python
x.mean(axis=-1)
```

    array([ 2. , 44.8, 76.8, 17. ])

### Marginalization works for higher dimensions in the same way

```python
x = np.random.random((2,3,4))
x
```

    array([[[0.18658517, 0.64047713, 0.94587991, 0.61495165],
            [0.05194638, 0.25243009, 0.77169445, 0.95496594],
            [0.0551977 , 0.85674614, 0.92755212, 0.86043415]],

           [[0.84571303, 0.89371063, 0.23284013, 0.09526368],
            [0.91167377, 0.27580046, 0.93419312, 0.79156371],
            [0.45611741, 0.95214143, 0.42178673, 0.84316195]]])

```python
x.shape
```

    (2, 3, 4)

```python
x.mean(axis=0).shape
```

    (3, 4)

```python
x.mean(axis=1).shape
```

    (2, 4)

```python
x.mean(axis=2).shape
```

    (2, 3)

```python
x.mean(axis=(0,1)).shape
```

    (4,)

```python
x.mean(axis=(0,2)).shape
```

    (3,)

```python
x.mean(axis=(1,2)).shape
```

    (2,)

## Broadcasting

Broadcasting is what happens when `numpy` tries to perform binary operations on two arrays with different shapes. In general, shapes are *promoted* to make the arrays compatible using the following rule

- For each axis from highest to lowest
    - If both dimensions are the same, do nothing
    - If one of the dimensions is 1 or None and the other is $k$, promote to $k$
    - Otherwise print error message

```python
x = np.zeros((3,2))
x.shape
```

    (3, 2)

```python
x
```

    array([[0., 0.],
           [0., 0.],
           [0., 0.]])

Shapes are compatible

```python
y = np.ones(2)
y.shape
```

    (2,)

```python
x + y
```

    array([[1., 1.],
           [1., 1.],
           [1., 1.]])

Shapes are compatible

```python
y = np.ones((1,2))
y.shape
```

    (1, 2)

```python
x + y
```

    array([[1., 1.],
           [1., 1.],
           [1., 1.]])

Shapes are incompatible but can be made compaible by adding empty dimension

```python
y = np.ones(3)
y.shape
```

    (3,)

```python
try:
    x + y
except ValueError as e:
    print(e)
```

    operands could not be broadcast together with shapes (3,2) (3,)

```python
y[:, None].shape
```

    (3, 1)

```python
x + y[:, None]
```

    array([[1., 1.],
           [1., 1.],
           [1., 1.]])

Shapes are incompatible

```python
y = np.ones((2,2))
y.shape
```

    (2, 2)

```python
try:
    x + y
except ValueError as e:
    print(e)
```

    operands could not be broadcast together with shapes (3,2) (2,2)

### More examples of broadcasting

```python
x1 = np.arange(12)
```

```python
x1
```

    array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

```python
x1 * 10
```

    array([  0,  10,  20,  30,  40,  50,  60,  70,  80,  90, 100, 110])

```python
x2 = np.random.randint(0,10,(3,4))
```

```python
x2
```

    array([[3, 3, 6, 2],
           [5, 1, 6, 2],
           [8, 0, 2, 8]])

```python
x2 * 10
```

    array([[30, 30, 60, 20],
           [50, 10, 60, 20],
           [80,  0, 20, 80]])

```python
x2.shape
```

    (3, 4)

### Column-wise broadcasting

```python
mu = np.mean(x2, axis=0)
mu.shape
```

    (4,)

```python
x2 - mu
```

    array([[-2.33333333,  1.66666667,  1.33333333, -2.        ],
           [-0.33333333, -0.33333333,  1.33333333, -2.        ],
           [ 2.66666667, -1.33333333, -2.66666667,  4.        ]])

```python
(x2 - mu).mean(axis=0)
```

    array([ 2.96059473e-16,  7.40148683e-17, -2.96059473e-16,  0.00000000e+00])

### Row wise broadcasting

```python
mu = np.mean(x2, axis=1)
mu.shape
```

    (3,)

```python
try:
    x2 - mu
except ValueError as e:
    print(e)
```

    operands could not be broadcast together with shapes (3,4) (3,)

### We can add a "dummy" axis using None or `np.newaxis`

```python
mu[:, None].shape
```

    (3, 1)

```python
x2 - mu[:, None]
```

    array([[-0.5, -0.5,  2.5, -1.5],
           [ 1.5, -2.5,  2.5, -1.5],
           [ 3.5, -4.5, -2.5,  3.5]])

```python
x2 - mu[:, np.newaxis]
```

    array([[-0.5, -0.5,  2.5, -1.5],
           [ 1.5, -2.5,  2.5, -1.5],
           [ 3.5, -4.5, -2.5,  3.5]])

```python
np.mean(x2 - mu[:, None], axis=1)
```

    array([0., 0., 0.])

#### Reshaping works too

```python
x2 - mu.reshape((-1,1))
```

    array([[-0.5, -0.5,  2.5, -1.5],
           [ 1.5, -2.5,  2.5, -1.5],
           [ 3.5, -4.5, -2.5,  3.5]])

#### Exercise in broadcasting

Creating a 12 by 12 multiplication table

```python
x = np.arange(1, 13)
x[:,None] * x[None,:]
```

    array([[  1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12],
           [  2,   4,   6,   8,  10,  12,  14,  16,  18,  20,  22,  24],
           [  3,   6,   9,  12,  15,  18,  21,  24,  27,  30,  33,  36],
           [  4,   8,  12,  16,  20,  24,  28,  32,  36,  40,  44,  48],
           [  5,  10,  15,  20,  25,  30,  35,  40,  45,  50,  55,  60],
           [  6,  12,  18,  24,  30,  36,  42,  48,  54,  60,  66,  72],
           [  7,  14,  21,  28,  35,  42,  49,  56,  63,  70,  77,  84],
           [  8,  16,  24,  32,  40,  48,  56,  64,  72,  80,  88,  96],
           [  9,  18,  27,  36,  45,  54,  63,  72,  81,  90,  99, 108],
           [ 10,  20,  30,  40,  50,  60,  70,  80,  90, 100, 110, 120],
           [ 11,  22,  33,  44,  55,  66,  77,  88,  99, 110, 121, 132],
           [ 12,  24,  36,  48,  60,  72,  84,  96, 108, 120, 132, 144]])

Scaling to have zero mean and unit standard devation for each feature.

```python
x = np.random.normal(10, 5,(3,4))
x
```

    array([[10.68728899, 14.63808698, 12.13188671, 13.48607715],
           [ 6.3118309 , 11.92912759,  4.01779711,  7.28426382],
           [ 9.15695838, 20.47190356, 18.5117833 ,  3.00547023]])

Scaling column-wise

```python
(x - x.mean(axis=0))/x.std(axis=0)
```

    array([[ 1.08584868, -0.2922203 ,  0.09746085,  1.29242016],
           [-1.32758954, -1.05220344, -1.27056349, -0.14898015],
           [ 0.24174086,  1.34442375,  1.17310264, -1.14344001]])

Scaling row-wise

```python
(x - x.mean(axis=1)[:, None])/x.std(axis=1)[:, None]
```

    array([[-1.38564641,  1.28669248, -0.40851352,  0.50746745],
           [-0.37305115,  1.57824065, -1.16993435, -0.03525515],
           [-0.51242021,  1.08501579,  0.80828739, -1.38088297]])

## Combining `ndarray`s

```python
x1 = np.zeros((3,4))
x2 = np.ones((3,5))
x3 = np.eye(4)
```

```python
x1
```

    array([[0., 0., 0., 0.],
           [0., 0., 0., 0.],
           [0., 0., 0., 0.]])

```python
x2
```

    array([[1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.]])

```python
x3
```

    array([[1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.],
           [0., 0., 0., 1.]])

### Binding rows when number of columns is the same

```python
np.r_[x1, x3]
```

    array([[0., 0., 0., 0.],
           [0., 0., 0., 0.],
           [0., 0., 0., 0.],
           [1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.],
           [0., 0., 0., 1.]])

### Binding columns when number of rows is the same

```python
np.c_[x1, x2]
```

    array([[0., 0., 0., 0., 1., 1., 1., 1., 1.],
           [0., 0., 0., 0., 1., 1., 1., 1., 1.],
           [0., 0., 0., 0., 1., 1., 1., 1., 1.]])

### You can combine more than 2 at a time

```python
np.c_[x1, x2, x1]
```

    array([[0., 0., 0., 0., 1., 1., 1., 1., 1., 0., 0., 0., 0.],
           [0., 0., 0., 0., 1., 1., 1., 1., 1., 0., 0., 0., 0.],
           [0., 0., 0., 0., 1., 1., 1., 1., 1., 0., 0., 0., 0.]])

### Stacking

```python
np.vstack([x1, x3])
```

    array([[0., 0., 0., 0.],
           [0., 0., 0., 0.],
           [0., 0., 0., 0.],
           [1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.],
           [0., 0., 0., 1.]])

```python
np.hstack([x1, x2])
```

    array([[0., 0., 0., 0., 1., 1., 1., 1., 1.],
           [0., 0., 0., 0., 1., 1., 1., 1., 1.],
           [0., 0., 0., 0., 1., 1., 1., 1., 1.]])

```python
np.dstack([x2, 2*x2, 3*x2])
```

    array([[[1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.]],

           [[1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.]],

           [[1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.]]])

### Generic stack with axis argument

```python
np.stack([x2, 2*x2, 3*x2], axis=0)
```

    array([[[1., 1., 1., 1., 1.],
            [1., 1., 1., 1., 1.],
            [1., 1., 1., 1., 1.]],

           [[2., 2., 2., 2., 2.],
            [2., 2., 2., 2., 2.],
            [2., 2., 2., 2., 2.]],

           [[3., 3., 3., 3., 3.],
            [3., 3., 3., 3., 3.],
            [3., 3., 3., 3., 3.]]])

```python
np.stack([x2, 2*x2, 3*x2], axis=1)
```

    array([[[1., 1., 1., 1., 1.],
            [2., 2., 2., 2., 2.],
            [3., 3., 3., 3., 3.]],

           [[1., 1., 1., 1., 1.],
            [2., 2., 2., 2., 2.],
            [3., 3., 3., 3., 3.]],

           [[1., 1., 1., 1., 1.],
            [2., 2., 2., 2., 2.],
            [3., 3., 3., 3., 3.]]])

```python
np.stack([x2, 2*x2, 3*x2], axis=2)
```

    array([[[1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.]],

           [[1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.]],

           [[1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.],
            [1., 2., 3.]]])

### Repetition and tiling

#### For a vector

```python
x = np.array([1,2,3])
```

```python
np.repeat(x, 3)
```

    array([1, 1, 1, 2, 2, 2, 3, 3, 3])

```python
np.tile(x, 3)
```

    array([1, 2, 3, 1, 2, 3, 1, 2, 3])

#### For a matrix

```python
x = np.arange(6).reshape((2,3))
x
```

    array([[0, 1, 2],
           [3, 4, 5]])

```python
np.repeat(x, 3)
```

    array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])

```python
np.repeat(x, 3, axis=0)
```

    array([[0, 1, 2],
           [0, 1, 2],
           [0, 1, 2],
           [3, 4, 5],
           [3, 4, 5],
           [3, 4, 5]])

```python
np.repeat(x, 3, axis=1)
```

    array([[0, 0, 0, 1, 1, 1, 2, 2, 2],
           [3, 3, 3, 4, 4, 4, 5, 5, 5]])

```python
np.tile(x, (3,2))
```

    array([[0, 1, 2, 0, 1, 2],
           [3, 4, 5, 3, 4, 5],
           [0, 1, 2, 0, 1, 2],
           [3, 4, 5, 3, 4, 5],
           [0, 1, 2, 0, 1, 2],
           [3, 4, 5, 3, 4, 5]])

## Splitting `ndarray`s

```python
x = np.arange(32).reshape((4,8))
```

```python
x
```

    array([[ 0,  1,  2,  3,  4,  5,  6,  7],
           [ 8,  9, 10, 11, 12, 13, 14, 15],
           [16, 17, 18, 19, 20, 21, 22, 23],
           [24, 25, 26, 27, 28, 29, 30, 31]])

```python
np.split(x, 4)
```

    [array([[0, 1, 2, 3, 4, 5, 6, 7]]),
     array([[ 8,  9, 10, 11, 12, 13, 14, 15]]),
     array([[16, 17, 18, 19, 20, 21, 22, 23]]),
     array([[24, 25, 26, 27, 28, 29, 30, 31]])]

```python
np.split(x, 4, axis=1)
```

    [array([[ 0,  1],
            [ 8,  9],
            [16, 17],
            [24, 25]]), array([[ 2,  3],
            [10, 11],
            [18, 19],
            [26, 27]]), array([[ 4,  5],
            [12, 13],
            [20, 21],
            [28, 29]]), array([[ 6,  7],
            [14, 15],
            [22, 23],
            [30, 31]])]

## Vectorization

### Example 1

The operators and functions (ufuncs) in Python are vectorized, and will work element-wise over all entries in an `ndarray`.

```python
xs = np.zeros(10, dtype='int')
for i in range(10):
    xs[i] = i**2
xs
```

    array([ 0,  1,  4,  9, 16, 25, 36, 49, 64, 81])

```python
xs = np.arange(10)**2
xs
```

    array([ 0,  1,  4,  9, 16, 25, 36, 49, 64, 81])

Using ufuncs

```python
np.sqrt(xs)
```

    array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])

```python
np.log1p(xs)
```

    array([0.        , 0.69314718, 1.60943791, 2.30258509, 2.83321334,
           3.25809654, 3.61091791, 3.91202301, 4.17438727, 4.40671925])

### Example 2

Scalar product.

```python
n = 10

xs = np.random.rand(n)
ys = np.random.rand(n)

s = 0
for i in range(n):
    s += xs[i] * ys[i]
s
```

    2.5188405619306895

```python
np.dot(xs, ys)
```

    2.51884056193069

```python
xs @ ys
```

    2.51884056193069

### Example 3

\begin{align}
y_0 &= \alpha + \beta_1 x_1 + \beta_2 x_2 \\
y_1 &= \alpha + \beta_1 x_1 + \beta_2 x_2 \\
y_2 &= \alpha + \beta_1 x_1 + \beta_2 x_2 \\
\end{align}

```python
m = 3
n = 2

alpha = np.random.rand(1)
betas = np.random.rand(n,1)
xs = np.random.rand(m,n)
```

```python
alpha
```

    array([0.14766101])

```python
betas
```

    array([[0.73667336],
           [0.0346741 ]])

```python
xs
```

    array([[0.03376605, 0.60183824],
           [0.70389785, 0.04904536],
           [0.22335997, 0.74175809]])

### Using loops

```python
ys = np.zeros((m,1))
for i in range(m):
    ys[i] = alpha
    for j in range(n):
        ys[i] += betas[j] * xs[i,j]
ys
```

    array([[0.19340376],
           [0.6679044 ],
           [0.33792414]])

### Removing inner loop

```python
ys = np.zeros((m,1))
for i in range(m):
    ys[i] = alpha + xs[i,:].T @ betas
ys
```

    array([[0.19340376],
           [0.6679044 ],
           [0.33792414]])

### Removing all loops

```python
ys = alpha + xs @ betas
ys
```

    array([[0.19340376],
           [0.6679044 ],
           [0.33792414]])

### Alternative approach

The calculaiton with explicit intercepts and coefficients is common in deep learning, where $\alpha$ is called the bias ($b$) and $\beta$ are called the weights ($w$), and each equation is $y[i] = b + w[i]*x[i]$.

It is common in statisiics to use an augmented matrix in which the first column is all ones, so that all that is needed is a single matrix multiplicaiotn.

```python
X = np.c_[np.ones(m), xs]
X
```

    array([[1.        , 0.03376605, 0.60183824],
           [1.        , 0.70389785, 0.04904536],
           [1.        , 0.22335997, 0.74175809]])

```python
alpha
```

    array([0.14766101])

```python
betas
```

    array([[0.73667336],
           [0.0346741 ]])

```python
betas_ = np.concatenate([[alpha], betas])
betas_
```

    array([[0.14766101],
           [0.73667336],
           [0.0346741 ]])

```python
ys = X @ betas_
ys
```

    array([[0.19340376],
           [0.6679044 ],
           [0.33792414]])

### Simulating diffusion

```python
w = 100
h = 100
x = np.zeros((w+2,h+2), dtype='float')
x[(w//2-1):(w//2+2), (h//2-1):(h//2+2)] = 1

wts = np.ones(5)/5

for i in range(41):
    if i % 10 == 0:
        plt.figure()
        plt.imshow(x[1:-1, 1:-1], interpolation='nearest')

    center = x[1:-1, 1:-1]
    left = x[:-2, 1:-1]
    right = x[2:, 1:-1]
    bottom = x[1:-1, :-2]
    top = x[1:-1, 2:]
    nbrs = np.dstack([center, left, right, bottom, top])
    x = np.sum(wts * nbrs, axis=-1)
```