```python
import grouptesting
from grouptesting.model import *
from grouptesting.algorithms import *
import autograd.numpy as np
from autograd import grad
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import math
from scipy.stats import bernoulli
from scipy.optimize import minimize, rosen, rosen_der
from sympy import *

def nCr(n,k):
    return math.factorial(n) // math.factorial(k) // math.factorial(n - k)

np.random.seed(10)

```

## Plot ideas:

1. NCOMP vs. NDD for some q, or for three different qs (just to show empirical performance of NDD vs NCOMP) x
2. NCOMP with an achievability bound for some q x
4. NDD with achievability found for some q x

### 2. NCOMP with an achievability bound for some q

```python
# Define initial parameters
num_tests = 100
n = 1000 # population size
theta = 0.5
C = 2
n_theta = C * (n**theta)
k = round(n_theta) # number of infected
alpha = 0.5
p = alpha/k # Bernoulli test design probability parameter
eta = 0.001
q_results = []

############ RUN EXPERIMENT #####################
for q in [0.1]:
    NCOMP_ber_acc = []

    ############## Achievable bound 1 for T ################
    ## Choose delta according to z
    T_array = np.linspace(1, n, 25)
    z = ((1- alpha/(k) * (1-q))**(k-1) + (1/q)*(1 - alpha/(k) * (1-q))**k) / 2
    l = (1 - alpha / k * (1-q))**(k-1)
    u = (1 - alpha / k * (1-q))**(k) * (1/q)
    print("l-1, u-1:", l-1, u-1)
    delta = z - 1
    assert (l-1) < delta < (u-1)

    ########### Achievability Bound 2 Method 2 #######
    eta_1, alpha_1, theta_1, q_1 = eta, alpha, theta, q
    n_1 = n
    n_theta_1 = n_theta
    d_1 = round(n_theta_1)
    eps = 1e-12

    # Objective
    Tminus = lambda delta_1: ((1 + eta_1) * theta_1 * (1/(q_1**2)) * (d_1) * (np.log(n_1))) / (alpha_1 * (1 - np.exp(-2)) * (eps + (1+delta_1) - (1- alpha_1/(d_1) * (1-q_1))**(d_1-1))**2)
    Tplus = lambda delta_1: ((1 + eta_1) * (1/(q_1**2)) * (d_1) * (np.log(n_1))) / (alpha_1 * (1 - np.exp(-2)) * (eps + (1+delta_1) - (1/q_1) * (1- alpha_1/(d_1) * (1-q_1))**d_1)**2)
    ff = lambda delta_1: max(Tminus(delta_1), Tplus(delta_1))

    # Constraints
    l = (1 - alpha_1 / d_1 * (1-q_1))**(d_1-1)
    u = (1 - alpha_1 / d_1 * (1-q_1))**(d_1) * (1/q_1)
    bound = (l-1, u-1)

    x0 = delta
    options={'disp': None, 'maxcor': 100, 'ftol': 1e-14, 'gtol': 1e-012, 'eps': 1e-08, 'maxfun': 15000, 'maxiter': 15000, 'iprint': 1, 'maxls': 20}
    gff = grad(ff)
    res = minimize(ff, x0, tol=1e-12, bounds=[bound], jac=gff)
    achiev_bound = res.fun[0]

    ###### Main Experiment loop #####

    for T in T_array:
        NCOMP_ber_error = []
        NDD_ber_error = []
        COMP_ber_error = []
        print("T: ", int(round(T)))
        for test in range(num_tests):
            sigma = D(n, k) # Generate the vector of defectives
            X_ber = Ber(n, int(round(T)), p)
            y_ber = Y(dilution_noise(X_ber, q), sigma)

            # NCOMP - Bernoulli
            sigma_hat_ber = NCOMP(X_ber, y_ber, q=q, delta=delta)
            err = error(sigma, sigma_hat_ber)
            NCOMP_ber_error.append(error(sigma, sigma_hat_ber))

        acc = (num_tests - np.sum(np.array(NCOMP_ber_error))) / num_tests
        NCOMP_ber_acc.append(acc)

    ################ Plot ####################
    PAL= ['#f2f0f7','#cbc9e2','#9e9ac8','#756bb1','#54278f']
    plt.plot(T_array, NCOMP_ber_acc,'o-', c=PAL[-2], label="NCOMP - q = " + str(round(q, 2)))

#     use LaTeX fonts in the plot
    plt.rc('text', usetex=True)
    plt.rc('font', family='serif')
    matplotlib.rcParams['text.usetex'] = True

    plt.axvline(x=float(achiev_bound), ymin=0, ymax=1, color='black', linestyle='dotted', label="Achievability Bound", linewidth=3)
    plt.xticks(ticks=[10, round(float(achiev_bound)), round(n)])
    plt.yticks(ticks=[0.0, 1.0])
    plt.xlabel(r'Number of tests (T)', fontsize=11)
    plt.ylabel(r'Success Probability', fontsize=11)
    plt.legend()
    plt.savefig('plots/NCOMP Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
    plt.savefig('plots/NCOMP Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.eps')
    plt.savefig('plots/NCOMP Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
    plt.show()
```

```python
## Redo the plot because the latex font only comes in the second time you save it for some reason.
PAL= ['#f2f0f7','#cbc9e2','#9e9ac8','#756bb1','#54278f']
plt.plot(T_array, NCOMP_ber_acc,'o-', c=PAL[-2], label="NCOMP - q = " + str(round(q, 2)))

#     use LaTeX fonts in the plot
plt.rc('text', usetex=True)
plt.rc('font', family='serif')
matplotlib.rcParams['text.usetex'] = True

plt.axvline(x=float(achiev_bound), ymin=0, ymax=1, color='black', linestyle='dotted', label="Achievability Bound", linewidth=3)
plt.xticks(ticks=[10, round(float(achiev_bound)), round(n)])
plt.yticks(ticks=[0.0, 1.0])
plt.xlabel(r'Number of tests (T)', fontsize=11)
plt.ylabel(r'Success Probability', fontsize=11)
plt.legend()
plt.savefig('plots/NCOMP Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
plt.savefig('plots/NCOMP Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.eps')
plt.savefig('plots/NCOMP Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
plt.show()
```

### 4. NDD with Achievability bound for some q

```python
# Define initial parameters
num_tests = 100
n = 1000 # population size
theta = 0.15
C = 2
n_theta = C * (n**theta)
k = round(n_theta) # number of infected
eta = 0.001
q_results = []

############ RUN EXPERIMENT #####################
for q in [0.1]:
#     alpha = np.log(2)/(1-q) # 0.77
    alpha = 0.5 # 0.77
    p = alpha/k # Bernoulli test design probability parameter
    print("q: ", q)
    NDD_ber_acc = []
    NCOMP_ber_acc = []

    T_array = np.linspace(1, n, 25)
    pi_init = (q+0.07)

    ########### Achievability Bound 2 Method 2 #######
    eta_1, alpha_1, theta_1, q_1 = eta, alpha, theta, q
    n_1 = n
    n_theta_1 = n_theta
    d_1 = round(n_theta_1)
    eps = 1e-12

    # Objective
    D_ = lambda _eps: (_eps)*np.log(_eps) - _eps + 1
    T1 = lambda pi: (d_1 * np.log(d_1) * np.exp(alpha_1 * (1-q_1)) / ((alpha_1 * q_1)*D_(pi / (q_1 * np.exp(-alpha_1 * (1-q_1))))))
    T2 = lambda pi: ((1 - (theta_1 - eps)) * d_1 * np.log(n) * np.exp(alpha_1*(1-q_1)) / (alpha_1 * D_(pi * np.exp(alpha_1 * (1-q_1)))))
    T3 = lambda pi: (np.exp(alpha_1) * d_1 * np.log(d_1) / (alpha_1 * (1-q_1)))
    ff = lambda pi: max(T1(pi), T2(pi), T3(pi))

    bound = (q, np.exp(-alpha * (1-q))-0.01)

    x0 = pi_init
    gff = grad(ff)
    res = minimize(ff, x0, tol=1e-12, bounds=[bound], jac=gff)
    achiev_bound = res.fun[0]
    pi_NDD = res.x[0]
    assert q <= pi_NDD <= 1

    ###### Main Experiment loop #####
    for T in T_array:
        NDD_ber_error = []
        DD_ber_error = []
        NCOMP_ber_error = []
        print("T: ", int(round(T)))
        for test in range(num_tests):
            sigma = D(n, k) # Generate the vector of defectives
            X_ber = Ber(n, int(round(T)), p)
            y_ber = Y(dilution_noise(X_ber, q), sigma)

            # NCOMP - Bernoulli
            # Choose delta according to z
            z = ((1- alpha/(k) * (1-q))**(k-1) + (1/q)*(1 - alpha/(k) * (1-q))**k) / 2
            l = (1 - alpha / k * (1-q))**(k-1)
            u = (1 - alpha / k * (1-q))**(k) * (1/q)
            delta_NCOMP = z - 1
            assert (l-1) < delta_NCOMP < (u-1)
            sigma_hat_ber = NCOMP(X_ber, y_ber, q=q, delta=delta_NCOMP)
            err = error(sigma, sigma_hat_ber)
            NCOMP_ber_error.append(error(sigma, sigma_hat_ber))

            # NDD - Bernoulli
            sigma_hat_ber = NDD(X_ber, y_ber, pi=(pi_NDD), alpha=alpha, T = T, d = k)
            err = error(sigma, sigma_hat_ber)
            NDD_ber_error.append(error(sigma, sigma_hat_ber))

        acc = (num_tests - np.sum(np.array(NDD_ber_error))) / num_tests
        NDD_ber_acc.append(acc)
        acc = (num_tests - np.sum(np.array(NCOMP_ber_error))) / num_tests
        NCOMP_ber_acc.append(acc)

    ################ Plot ####################
    PAL = ['#edf8e9','#bae4b3','#74c476','#31a354','#006d2c']
    plt.plot(T_array, NDD_ber_acc,'o-', c=PAL[-2], label="NDD - q = " + str(round(q, 2)))

    # use LaTeX fonts in the plot
    plt.rc('text', usetex=True)
    plt.rc('font', family='serif')
    matplotlib.rcParams['text.usetex'] = True

    plt.axvline(x=ff(pi_NDD), ymin=0, ymax=1, color='black', linestyle='dotted', label="Achievability Bound", linewidth=3)
    plt.xticks(ticks=[round(10.0), round(ff(pi_NDD)), round(n)])
    plt.yticks(ticks=[0.0, 1.0])
    plt.xlabel(r'Number of tests (T)', fontsize=11)
    plt.ylabel(r'Success Probability', fontsize=11)
    plt.legend()
    plt.savefig('plots/NDD Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', pi = ' + str(round(pi_NDD, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
    plt.savefig('plots/NDD Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', pi = ' + str(round(pi_NDD, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.eps')
    plt.savefig('plots/NDD Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', pi = ' + str(round(pi_NDD, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
    plt.show()
```

### 1. NDD vs NCOMP for some/different qs

```python
# Define initial parameters
num_tests = 100
n = 1000 # population size
theta = 0.15
C = 2
n_theta = C * (n**theta)
k = round(n_theta) # number of infected
eta = 0.001
q_results = []

############ RUN EXPERIMENT #####################
for q in [0.00001, 0.1, 0.3, 0.5]:
    alpha = 0.5
    p = alpha/k # Bernoulli test design probability parameter
    print("q: ", q)
    NDD_ber_acc = []
    DD_ber_acc = []
    NCOMP_ber_acc = []

    T_array = np.linspace(1, n, 25)
    pi_init = (q+0.07)

    ########### Achievability Bound 2 Method 2 #######
    eta_1, alpha_1, theta_1, q_1 = eta, alpha, theta, q
    n_1 = n
    n_theta_1 = n_theta
    d_1 = round(n_theta_1)
    eps = 1e-12

    # Objective
    D_ = lambda _eps: (_eps)*np.log(_eps) - _eps + 1
    T1 = lambda pi: (d_1 * np.log(d_1) * np.exp(alpha_1 * (1-q_1)) / ((alpha_1 * q_1)*D_(pi / (q_1 * np.exp(-alpha_1 * (1-q_1))))))
    T2 = lambda pi: ((1 - (theta_1 - eps)) * d_1 * np.log(n) * np.exp(alpha_1*(1-q_1)) / (alpha_1 * D_(pi * np.exp(alpha_1 * (1-q_1)))))
    T3 = lambda pi: (np.exp(alpha_1) * d_1 * np.log(d_1) / (alpha_1 * (1-q_1)))
    ff = lambda pi: max(T1(pi), T2(pi), T3(pi))

    bound = (q, np.exp(-alpha * (1-q))-0.01)
    x0 = pi_init
    gff = grad(ff)
    res = minimize(ff, x0, tol=1e-6, bounds=[bound], jac=gff)
    achiev_bound = res.fun[0]
    pi_NDD = res.x[0]
    assert q <= pi_NDD <= 1

    ###### Main Experiment loop #####
    for T in T_array:
        NDD_ber_error = []
        DD_ber_error = []
        NCOMP_ber_error = []
        print("T: ", int(round(T)))
        for test in range(num_tests):
            sigma = D(n, k) # Generate the vector of defectives
            X_ber = Ber(n, int(round(T)), p)
            y_ber = Y(dilution_noise(X_ber, q), sigma)

            # NCOMP - Bernoulli
            # Choose delta according to z
            z = ((1- alpha/(k) * (1-q))**(k-1) + (1/q)*(1 - alpha/(k) * (1-q))**k) / 2
            l = (1 - alpha / k * (1-q))**(k-1)
            u = (1 - alpha / k * (1-q))**(k) * (1/q)
            delta_NCOMP = z - 1
            assert (l-1) < delta_NCOMP < (u-1)
            sigma_hat_ber = NCOMP(X_ber, y_ber, q=q, delta=delta_NCOMP)
            err = error(sigma, sigma_hat_ber)
            NCOMP_ber_error.append(error(sigma, sigma_hat_ber))

            # NDD - Bernoulli
            sigma_hat_ber = NDD(X_ber, y_ber, pi=(pi_NDD), alpha=alpha, T = T, d = k)
            err = error(sigma, sigma_hat_ber)
            NDD_ber_error.append(error(sigma, sigma_hat_ber))

        acc = (num_tests - np.sum(np.array(NDD_ber_error))) / num_tests
        NDD_ber_acc.append(acc)
        acc = (num_tests - np.sum(np.array(NCOMP_ber_error))) / num_tests
        NCOMP_ber_acc.append(acc)

    q_results.append([q, T_array, NCOMP_ber_acc, NDD_ber_acc])
```

    q:  1e-05
    T:  1
    T:  43
    T:  84
    T:  126
    T:  168
    T:  209
    T:  251
    T:  292
    T:  334
    T:  376
    T:  417
    T:  459
    T:  500
    T:  542
    T:  584
    T:  625
    T:  667
    T:  709
    T:  750
    T:  792
    T:  834
    T:  875
    T:  917
    T:  958
    T:  1000
    q:  0.1
    T:  1
    T:  43
    T:  84
    T:  126
    T:  168
    T:  209
    T:  251
    T:  292
    T:  334
    T:  376
    T:  417
    T:  459
    T:  500
    T:  542
    T:  584
    T:  625
    T:  667
    T:  709
    T:  750
    T:  792
    T:  834
    T:  875
    T:  917
    T:  958
    T:  1000
    q:  0.3
    T:  1
    T:  43
    T:  84
    T:  126
    T:  168
    T:  209
    T:  251
    T:  292
    T:  334
    T:  376
    T:  417
    T:  459
    T:  500
    T:  542
    T:  584
    T:  625
    T:  667
    T:  709
    T:  750
    T:  792
    T:  834
    T:  875
    T:  917
    T:  958
    T:  1000
    q:  0.5
    T:  1
    T:  43
    T:  84
    T:  126
    T:  168
    T:  209
    T:  251
    T:  292
    T:  334
    T:  376
    T:  417
    T:  459
    T:  500
    T:  542
    T:  584
    T:  625
    T:  667
    T:  709
    T:  750
    T:  792
    T:  834
    T:  875
    T:  917
    T:  958
    T:  1000

```python
PAL1 = ['#edf8e9','#bae4b3','#74c476','#31a354','#006d2c']
PAL2 = ['#f2f0f7','#cbc9e2','#9e9ac8','#756bb1','#54278f']
for i in range(len(q_results)):
    print(i)
    q, T_array, NCOMP_ber_acc, NDD_ber_acc = q_results[i]

    if (i==0):
        plt.plot(T_array, NDD_ber_acc,'o-', c=PAL1[-1-i], label="NDD - q = 0.0 to 0.5", linewidth=2)
        plt.plot(T_array, NCOMP_ber_acc,'--', c=PAL2[-1-i], label="NCOMP - q = 0.0 to 0.5", linewidth=2)
    else:
        plt.plot(T_array, NDD_ber_acc,'o-', c=PAL1[-1-i], linewidth=2)
        plt.plot(T_array, NCOMP_ber_acc,'--', c=PAL2[-1-i], linewidth=2)

    # use LaTeX fonts in the plot
    plt.rc('text', usetex=True)
    plt.rc('font', family='serif')
    matplotlib.rcParams['text.usetex'] = True

    plt.yticks(ticks=[0.0, 1.0])
    plt.xlabel(r'Number of tests (T)', fontsize=11)
    plt.ylabel(r'Success Probability', fontsize=11)

plt.legend()
plt.savefig('plots/NCOMP vs NDD Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
plt.savefig('plots/NCOMP vs NDD Report Gabriel 2' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
plt.show()
```

notebooks/.ipynb_checkpoints/Semester Project Plots Gabriel-checkpoint.ipynb


# Competencia perfecta

Clase video: https://economicasuba-my.sharepoint.com/:v:/g/personal/96bi42292886_campus_economicas_uba_ar/EX6N21PJrvtNsYrSenZL6jQB_SoKPFFLd-YooDZKlvlP3A?e=GkMzNF
Problemas de Competencia Perfecta: https://drive.google.com/file/d/1HCswIhqA_KqtP-jw9VjLsLX9-QM9afYU/view?usp=sharing
Guia: https://drive.google.com/file/d/1pmR8dMCCnapUQ-cDk-IiOvqhXYIiz35_/view?usp=sharing

### Ejercicio 6
Considere una ciudad con un mercado perfectamente competitivo de taxis, es decir, el precio no se encuentra regulado por una autoridad estatal. El costo marginal de cada viaje es constante e igual a $ 10 y cada taxi puede realizar, en promedio, 25 viajes diarios. Si la función de demanda de viajes en taxi es Q(p) = 2000 - 15p
a) ¿Cuál es el precio de equilibrio de un viaje?
b) ¿Cuál es, en equilibrio, el número de viajes diarios?
c) ¿Cuál es, en equilibrio, el número de taxis de la ciudad?

Si la curva de demanda se desplaza a Q(p) = 1220 - 20 p
e) ¿Cuál será ahora el precio de equilibrio de un viaje?
f) ¿Cuáles son ahora los beneficios de un taxista por cada viaje?

```python

```

### Ejercicio 7
7.1 Considere una economía en la que existen 100 individuos homogéneos, donde la función de utilidad[i] que los representa es para todo i = 1..100

```python
import sympy as sp
x, y, t, L, m, py, px, n  = sp.symbols('x, y, t, L, m, py, px, n')
```

```python
cantidad_de_empresas = 100
utilidad = 100*(x**2)*(y**2)
ingreso = px * x
```

Y cada uno dispone de una renta m=120$.

```python
renta = 120 - px*x - py*y
```

A su vez, existen 100 empresas con la misma tecnología. La función de costos de cada empresa es CT[j] para todo j = 1...100

```python
i_range = range(cantidad_de_empresas)
CT = (x**2) + 0.1 * x + 10
```

a) Para hallar la función de demanda agregada del bien x tenemos que partir del problema del consumidor individual: Maximizo utilidad sujeta a CT

```python
j_range = range(cantidad_de_empresas)
L = utilidad + t*renta
```

Planteo condiciones de primer orden y despejo

```python
Lx = sp.diff(L, x)
Lx = sp.Eq(Lx,0)
Lx = sp.solve(Lx, t)
Lx[0]

```

$\displaystyle \frac{200 x y^{2}}{px}$

```python
Ly = sp.diff(L, y)
Ly = sp.Eq(Ly,0)
Ly = sp.solve(Ly, t)
Ly[0]

```

$\displaystyle \frac{200 x^{2} y}{py}$

```python
ig = sp.Eq(Lx[0]-Ly[0], 0)
ig
```

$\displaystyle - \frac{200 x^{2} y}{py} + \frac{200 x y^{2}}{px} = 0$

```python
sp.solve(ig, y)[1]
```

$\displaystyle \frac{px x}{py}$

```python
igx = sp.solve(ig, x)[1]
igy = sp.solve(ig, y)[1]
```

```python
Lt = sp.diff(L, t)
Lt

```

$\displaystyle - px x - py y + 120$

Las funciones de demanda resultantes son:

```python
demanda_individual_y = Lt.subs(x, igx )
demanda_individual_y = sp.solve(demanda_individual_y, y)[0]
demanda_individual_y
```

$\displaystyle \frac{60}{py}$

```python

demanda_individual_x = Lt.subs(y, igy )
demanda_individual_x = sp.solve(demanda_individual_x, x)[0]
demanda_individual_x
```

$\displaystyle \frac{60}{px}$

La función de demanda total de mercado es la suma horizontal de las demandas individuales, que son 100. Por lo que

```python
demanda_total_x = demanda_individual_x*cantidad_de_empresas
demanda_total_x
```

$\displaystyle \frac{6000}{px}$

b) Sabemos que la curva de oferta de la empresa a corto plazo comienza a ofrecer cantidades positivas cuando el precio sea mayor o igual que el costo variable medio mínimo. Y en el largo plazo cuando el precio sea mayor o igual que el costo medio mínimo. Por lo tanto tenemos que

```python
CME = sp.simplify( CT/x )
CME
```

$\displaystyle x + 0.1 + \frac{10}{x}$

```python
CVME = sp.simplify( (CT-10)/x)
CVME
```

$\displaystyle x + 0.1$

```python
CMg = sp.diff(CT, x)
CMg
```

$\displaystyle 2 x + 0.1$

Dado que tanto el CVMe como el CMg son rectas con ordenada al origen 0,1, la curva de oferta de corto plazo será. Oferta individual a corto plazo :
- si px < 0,1 => xi = 0
- si px >= 0.1 xj será tal que px = 2x +0.1
- xj =(px-0.1)/2

```python
pl1 = sp.plot(CT, xlim=[0,25],ylim=[0,25], show=False, line_color = 'Blue')
pl2 = sp.plot(CME, (x, 0, 100), show=False, line_color = 'Green')
pl3 = sp.plot(CVME, (x, 0, 100), show=False, line_color = 'Magenta')
pl4 = sp.plot(CMg, (x, 0, 100), show=False, line_color = 'Orange')
pl1.append(pl2[0])
pl1.append(pl3[0])
pl1.append(pl4[0])
pl1.show()
```

La oferta de la industria es la suma horizontal de las ofertas individuales. En este caso, dado que hay 100 empresas, sabemos que:

mercado_x = sumatoria detodas las xj para todo j entre 0 y 100
- si px < 0,1 => mercado_x = 100 * xj =0
- px >= 0,1 => mercado_x = 100 * xj = 100 * (px - 0,1) / 2

d) Para hallar el equilibrio de mercado tenemos que igualar oferta a demanda. Entonces:

```python
oferta_individual_x = (px - 0.1) / 2
oferta_total_x = cantidad_de_empresas * oferta_individual_x
oferta_total_x = sp.simplify(oferta_total_x)
#oferta_total_x = 50 * px - 5

igualdad_oferta_demanda_total_x = demanda_total_x - oferta_total_x

sp.simplify(igualdad_oferta_demanda_total_x)

```

$\displaystyle - 50 px + 5.0 + \frac{6000}{px}$

Buscando el (o los) valor(es) de px . Como la igualdad da un resultado cuadrático, vamos a encontrar dos raíces.

```python
px_equilibrio = sp.solveset(igualdad_oferta_demanda_total_x, px)
px_equilibrio
```

$\displaystyle \left\{-10.9045652583752, 11.0045652583752\right\}$

Elegimos la raíz positiva porque no existen cantidades negativas.

```python
px_equilibrio = float(px_equilibrio.args[1])
px_equilibrio
```

    11.004565258375159

Expreso demanda total y oferta total en función de cantidades y Grafico

```python
demanda_total_x_en_funcion_de_cantidad = sp.solve(sp.Eq(x,demanda_total_x), px)[0]
oferta_total_x_en_funcion_de_cantidad = sp.solve(sp.Eq(x,oferta_total_x), px)[0]
cantidad_total_equilibrio = demanda_total_x.subs(px, px_equilibrio)
```

```python

pl5 = sp.plot(demanda_total_x_en_funcion_de_cantidad, (x, 0, 800), ylim = (0.0, 20.0), show = False)
pl6 = sp.plot(oferta_total_x_en_funcion_de_cantidad, (x, 0, 800), ylim = (0.0, 20.0), line_color = 'red', show = False)
pl7 = sp.plot(px_equilibrio, (x, 0, 800), line_color = 'gray', show = False)
# pl8 = sp.plot_implicit(sp.Eq(px, cantidad_total_equilibrio), (x, 0, 800), show = False) # vertical line
pl5.append(pl6[0])
pl5.append(pl7[0])
# pl5.append(pl8[0])
pl5.show()
```

```python
cantidad_individual_equilibrio = cantidad_total_equilibrio / cantidad_de_empresas
cantidad_individual_equilibrio
```

$\displaystyle 5.45228262918758$

Por tanto el precio de mercado de equilibrio es de 11$ y las cantidades de equilibrio son 545 unidades.
Para saber las cantidades individuales demandadas y ofrecidas reemplazamos el precio de equilibrio en cada caso y obtenemos que cada individuo demanda 5.45 unidades mientras que cada productor ofrece, a su vez, 5.45 unidades.

```python

```

e) Para saber el beneficio de cada empresa partimos del problema de productor, con los valores hallados

```python
beneficio = ingreso.subs({px: px_equilibrio, x: cantidad_individual_equilibrio }) - CT.subs(x, cantidad_individual_equilibrio)
beneficio
```

$\displaystyle 19.7273858685406$

Por lo tanto, dado que hay beneficios positivos, hay incentivos a que entren más empresas en este mercado.

7.2 Dado un shock tecnológico positivo, la función de costos de cada empresa queda redefinida como

```python
CT = 0.5 * x**2 + 0.1 * x + 10
```

Por lo tanto tenemos que

```python
CME = sp.simplify(CT / x)
CVME = sp.simplify((CT - 10) / x)
CVMg = sp.diff((CT - 10), x)
CMg = sp.diff(CT, x)
```

La curva de oferta de corto plazo es
- si px < 0.1
 - x = 0
- si px > = 0.1
 - x = px - 0.1

La oferta total de la industria será la sumatoria de todas las cantidades ofrecidas será
- si px < 0.1 =>
 - oferta_total_x = cantidad_de_empresas
 - oferta_individual_x = 0
- si px > = 0.1 => x = px-0.1
 - oferta_total_x = cantidad_de_empresas
 - oferta_individual_x = cantidad_de_empresas*(px - 0.1)

Para hallar el equilibrio de mercado, al igual que en el caso anterior, tenemos que igualar la oferta y la demanda.
Entonces

```python
oferta_total_x = cantidad_de_empresas * (px - 0.1)
oferta_total_x = sp.simplify(oferta_total_x)
#oferta_total_x = 50 * px - 5

igualdad_oferta_demanda_total_x = demanda_total_x - oferta_total_x

sp.simplify(igualdad_oferta_demanda_total_x)
```

$\displaystyle - 100 px + 10.0 + \frac{6000}{px}$

```python
px_equilibrio = sp.solveset(igualdad_oferta_demanda_total_x, px)
px_equilibrio

```

$\displaystyle \left\{-7.69612806503998, 7.79612806503998\right\}$

```python
px_equilibrio = float(px_equilibrio.args[1])
px_equilibrio
```

    7.796128065039978

Por tanto el precio de mercado de equilibrio es de 7,79$ y las cantidades de equilibrio son 770 unidades. Para saber las cantidades individuales demandadas y ofrecidas reemplazamos el precio de equilibrio en cada caso
y obtenemos que cada individuo demanda 7,7 unidades mientras que cada productor ofrece, a su vez, 7,7 unidades. Para saber el beneficio de cada empresa partimos del problema de productor, con los valores hallados

```python
beneficio = ingreso.subs({px: px_equilibrio, x: cantidad_individual_equilibrio }) - CT.subs(x, cantidad_individual_equilibrio)
beneficio
```

$\displaystyle 17.0977724267502$

Por lo tanto, dado que hay beneficios positivos, hay incentivos a que entren más empresas en este mercado.

i) Para hallar el equilibrio de largo plazo, con libre entrada y salida de empresas, tenemos que encontrar cuál es el precio para el que no hay incentivos a que cambie el número de empresas en el mercado. Lo que es decir, cuando los beneficios extraordinarios son cero. Buscamos el CMe mínimo

```python
CME
```

$\displaystyle 0.5 x + 0.1 + \frac{10}{x}$

```python
CVME
```

$\displaystyle 0.5 x + 0.1$

```python
CMg
```

$\displaystyle 1.0 x + 0.1$

```python
cantidad_minima_producida = sp.solveset(sp.Eq(CME-CMg,0)).args[1]
cantidad_minima_producida
```

$\displaystyle 4.47213595499958$

```python
CVME_minimo = CVME.subs(x,cantidad_minima_producida)
CVME_minimo

```

$\displaystyle 2.33606797749979$

```python
CME_minimo = CME.subs(x,cantidad_minima_producida)
CME_minimo
```

$\displaystyle 4.57213595499958$

```python
pl13 = sp.plot(CMg, (x, 0, 10), xlim = (0.0, 10.0),  ylim = (0.0, 10.0), line_color="green", show = False)
pl14 = sp.plot(CME, show = False)
pl15 = sp.plot(CVME, show = False)
pl16 = sp.plot(CME_minimo, line_color = "red", show = False)
pl17 = sp.plot(px_equilibrio, line_color = "gray", show = False)

pl13.append(pl14[0])

pl13.append(pl15[0])
pl13.append(pl16[0])
pl13.append(pl17[0])
pl13.show()

# to do : labels, ejes verticales
```

A pecio $4.57 sabemos que no existirán incentivos a que entren ni salgan empresas del mercado. Por tanto, las cantidades demandadas son

```python
demanda_minima = demanda_total_x.subs(px, CME_minimo)
demanda_minima
```

$\displaystyle 1312.29693496736$

Sabiendo que cada empresa, a precio $4.57(CME_minimo) ofrece 4.47 unidades(cantidad_minima_producida), entonces averiguamos la cantidad de consumidores dispuestos a comprar esa cantidad a ese precio
Donde n es el número de empresas de equilibrio.
- cantidad_de_empresas =  n * cantidad_minima_producida = demanda_minima

```python
cantidad_de_empresas =  sp.solve(sp.Eq(n * cantidad_minima_producida , demanda_minima), n)[0]
cantidad_de_empresas
```

$\displaystyle 293.438515325164$

En este mercado, por lo tanto, caben 293,71 empresas.

```python
demanda_total_x_en_funcion_de_cantidad = sp.solve(sp.Eq(x,demanda_total_x), px)[0]
oferta_total_x_en_funcion_de_cantidad = sp.solve(sp.Eq(x,oferta_total_x), px)[0]

cantidad_total_equilibrio = demanda_total_x.subs(px, px_equilibrio)

cantidad_individual_equilibrio = cantidad_total_equilibrio / cantidad_de_empresas

igualdad_oferta_demanda_totales = sp.solveset(sp.Eq(demanda_total_x_en_funcion_de_cantidad-oferta_total_x_en_funcion_de_cantidad,0)).args[1]
px_equilibrio_total = demanda_total_x_en_funcion_de_cantidad.subs(x,igualdad_oferta_demanda_totales)
```

```python
pl18 = sp.plot(demanda_total_x_en_funcion_de_cantidad, (x, 0, 800), xlim = (0.0, 800.0),  ylim = (0.0, 50.0), show = False)
pl19 = sp.plot(oferta_total_x_en_funcion_de_cantidad, (x, 0, 800), show = False)
pl20 = sp.plot(px_equilibrio_total, (x, 0, 800), line_color = "gray" , show = False)
pl18.append(pl19[0])
pl18.append(pl20[0])
pl18.show()
```

El productor tendrá incentivos a retirarse del mercado si el precio es menor al costo variable medio mínimo.

```python
if CVME_minimo < CME_minimo:
    if px_equilibrio < CVME_minimo:
        print(f"El productor tendrá incentivos a retirarse del mercado porque el precio de equilibrio ${px_equilibrio} es menor que el costo medio variable mínimo ${CVME_minimo}")
    elif (px_equilibrio < CME_minimo):
        print(f"el precio de equilibrio ${px_equilibrio} es mayor que el costo medio variable mínimo ${CVME_minimo}, pero no llega al monto del costo medio mínimo de ${CME_minimo}. Por lo tanto en corto plazo produce, pero en largo plazo se retira del mercado")
    else:
        print(f"En corto y largo plazo produce porque el precio de equilbrio ${px_equilibrio} es mayor tanto al costo variable medio mínimo  ${CVME_minimo} como al costo medio mínimo ${CME_minimo}.")
else:
    print("CVME_minimo es mayor a CME_minimo y no entiendo qué significa")

```

    En corto y largo plazo produce porque el precio de equilbrio $7.796128065039978 es mayor tanto al costo variable medio mínimo  $2.33606797749979 como al costo medio mínimo $4.57213595499958.

En el corto plazo si el precio se encuentra entre el costo variable medio mínimo y el costo medio mínimo, produce, ya que cubre los costos variables y parte de los costos fijos. En largo plazo se retira del mercado.

Si el precio es igual al costo medio mínimo, todos los factores  utilizados en la producción se ven retribuidos a sus costos de oportunidad.
Si el precio es mayor que el costo medio mínimo el productor gozará de beneficios extraordinarios.

============================================

La oferta del mercado es la suma horizontal de las ofertas individuales. Por lo tanto en la medida en que existan más empresas , la pendiente de la curva de oferta se aplana

```python
pl9 = sp.plot(CME_minimo, (x, 0, 20), xlim = (0.0, 20.0), ylim = (0.0, 10.0), show = False)
pl10 = sp.plot(oferta_individual_x, show = False)
pl11 = sp.plot(oferta_individual_x*2, show = False)
pl12 = sp.plot(oferta_individual_x*3, show = False)

pl9.append(pl10[0])
pl9.append(pl11[0])
pl9.append(pl12[0])
pl9.show()
#to do: que se muestren labels, ejes verticales y que la sumatoria de ofertas tenga rango de x mayor sin que tire error
```

Si el precio de mercado en el largo plazo es mayor que el costo medio minimo, las empresas gozarán de beneficios extraordinarios

============================================


**Brief Honor Code**. Do the homework on your own. You may discuss ideas with your classmates, but DO NOT copy the solutions from someone else or the Internet. If stuck, discuss with TA.

**Note**: The expected figures are provided so you can check your solutions.

**1**. (20 points)

Find the gradient and Hessian for the following equation

$$
f(x, y) = 1 + 2x + 3y + 4x^2 + 2xy + y^2
$$

- Plot the contours of this function using `matplotlib` in the box $-5 \le x \le 5$ and $-5 \le y \le 5$ using a $100 \times 100$ grid.
- Then plot the gradient vectors using the `quiver` function on top of the contour plot using a $10 \times 10$ grid. Are the gradients orthogonal to the contours?

Hint: Use `numpy.meshgrid`, `matplotlib.contour` and `matplotllib.quiver`.

```python
import numpy as np
import pandas as py
import scipy.linalg as la
import matplotlib.pyplot as plt
from sympy import symbols, Matrix, Function, simplify, exp, hessian, solve, init_printing
init_printing()
```

```python
x, y = symbols('x y')
f = symbols('f', cls=Function)
```

```python
zz = Matrix([x,y])
f = Matrix([1+2*x+3*y+4*x**2+2*x*y+y**2])
```

```python
gradf = simplify(f.jacobian(zz))
gradf
```

```python
hessianf = simplify(hessian(f, zz))
hessianf
```

```python
hessian(f, zz)
```

```python
hessian(f)
```

```python
import numpy as np
x=np.arange(-10,10,0.1)
y=np.arange(-10,10,0.1)

xx=np.arange(-10,10,2)
yy=np.arange(-10,10,2)

def f(x, y):
    return 1+2*x+3*y+4*x**2+2*x*y+y**2

X, Y = np.meshgrid(x, y)

XX,YY=np.meshgrid(xx, yy)
Z = f(X, Y)
zz= f(XX,YY)

u,v=np.gradient(zz)
```

```python
plt.contour(X, Y, Z,10)
plt.xlim(-10, 10)
plt.ylim(-10, 10)
plt.gca().set_aspect('equal', adjustable='box')
plt.quiver(XX, YY, u,v,angles='xy',scale=800,color='r')
```

```python
plt.contour(X, Y, Z,10)
plt.xlim(-10, 10)
plt.ylim(-10, 10)
plt.gca().set_aspect('equal', adjustable='box')
plt.quiver(XX, YY, v,u,angles='xy',scale=800,color='r')
```

**2**. (30 points)

This exercise is about using Newton's method to find the cube roots of unity - find $z$ such that $z^3 = 1$. From the fundamental theorem of algebra, we know there must be exactly 3 complex roots since this is a degree 3 polynomial.

We start with Euler's equation
$$
e^{ix} = \cos x + i \sin x
$$

Raising $e^{ix}$ to the $n$th power where $n$ is an integer, we get from Euler's formula with $nx$ substituting for $x$
$$
(e^{ix})^n = e^{i(nx)} = \cos nx + i \sin nx
$$

Whenever $nx$ is an integer multiple of $2\pi$, we have
$$
\cos nx + i \sin nx = 1
$$

So
$$
e^{2\pi i \frac{k}{n}}
$$
is a root of 1 whenever $k/n = 0, 1, 2, \ldots$.

So the cube roots of unity are $1, e^{2\pi i/3}, e^{4\pi i/3}$.

While we can do this analytically, the idea is to use Newton's method to find these roots, and in the process, discover some rather perplexing behavior of Newton's method.

Newton's method for functions of complex variables - stability and basins of attraction. (30 points)

1. Write a function with the following function signature `newton(z, f, fprime, max_iter=100, tol=1e-6)` where
    - `z` is a starting value (a complex number e.g.  ` 3 + 4j`)
    - `f` is a function of `z`
    - `fprime` is the derivative of  `f`
The function will run until either max_iter is reached or the absolute value of the Newton step is less than tol. In either case, the function should return the number of iterations taken and the final value of `z` as a tuple (`i`, `z`).

2. Define the function `f` and `fprime` that will result in Newton's method finding the cube roots of 1. Find 3 starting points that will give different roots, and print both the start and end points.

Write the following two plotting functions to see some (pretty) aspects of Newton's algorithm in the complex plane.

3. The first function `plot_newton_iters(f, fprime, n=200, extent=[-1,1,-1,1], cmap='hsv')` calculates and stores the number of iterations taken for convergence (or max_iter) for each point in a 2D array. The 2D array limits are given by `extent` - for example, when `extent = [-1,1,-1,1]` the corners of the plot are `(-i, -i), (1, -i), (1, i), (-1, i)`. There are `n` grid points in both the real and imaginary axes. The argument `cmap` specifies the color map to use - the suggested defaults are fine. Finally plot the image using `plt.imshow` - make sure the axis ticks are correctly scaled. Make a plot for the cube roots of 1.

4. The second function `plot_newton_basins(f, fprime, n=200, extent=[-1,1,-1,1], cmap='jet')` has the same arguments, but this time the grid stores the identity of the root that the starting point converged to. Make a plot for the cube roots of 1 - since there are 3 roots, there should be only 3 colors in the plot.

Write a function with the following function signature `newton(z, f, fprime, max_iter=100, tol=1e-6)` where
    - `z` is a starting value (a complex number e.g.  ` 3 + 4j`)
    - `f` is a function of `z`
    - `fprime` is the derivative of  `f`
The function will run until either max_iter is reached or the absolute value of the Newton step is less than tol. In either case, the function should return the number of iterations taken and the final value of `z` as a tuple (`i`, `z`).

```python
def f(x):
    z=x**3-1
    return(z)
```

```python
def fprime(x,dx=1e-10):
    return((f(x+dx) - f(x))/dx)
```

```python
def newton(z, f, fprime, max_iter=100, tol=1e-6):
    iters=0
    while True:
        y=z-f(z)/fprime(z)
        if abs(z-y)<tol:
            return(iters,y)
        if abs(fprime(z))<tol:
            return(iters,y)
        z=y
        if iters==max_iter:
            return(iters,y)
            print('break')
        iters=iters+1
```

```python
newton(3+4j,f,fprime)
```

    (15, (1.00000000000002-5.724192889027663e-15j))

 Define the function `f` and `fprime` that will result in Newton's method finding the cube roots of 1. Find 3 starting points that will give different roots, and print both the start and end points.

```python
for i in range(1,4):
    x=i+4j
    y=newton(x,f,fprime)[1]
    print(x,y)
```

    (1+4j) (-0.4999999999999997+0.8660254037844385j)
    (2+4j) (-0.4999999999999917-0.8660254037844287j)
    (3+4j) (1.00000000000002-5.724192889027663e-15j)

The first function `plot_newton_iters(f, fprime, n=200, extent=[-1,1,-1,1], cmap='hsv')` calculates and stores the number of iterations taken for convergence (or max_iter) for each point in a 2D array. The 2D array limits are given by `extent` - for example, when `extent = [-1,1,-1,1]` the corners of the plot are `(-i, -i), (1, -i), (1, i), (-1, i)`. There are `n` grid points in both the real and imaginary axes. The argument `cmap` specifies the color map to use - the suggested defaults are fine. Finally plot the image using `plt.imshow` - make sure the axis ticks are correctly scaled. Make a plot for the cube roots of 1

```python
def plot_newton_iters(f, fprime, n=200, extent=[-1,1,-1,1], cmap='hsv'):
    xmin, xmax, ymin, ymax = extent
    X=np.linspace(xmin, xmax, n)
    Y=np.linspace(ymin, ymax, n)
    m=np.zeros(n*n).reshape(n,n)
    for (a,x) in enumerate(X):
        for (b,y) in enumerate(Y):
            z=x+1j*y
            res=newton(z, f, fprime)
            m[a,b]=res[0]
    plt.imshow(m.T, extent=extent,cmap=cmap)
    plt.show()
```

```python
plot_newton_iters(f, fprime, n=200, extent=[-1,1,-1,1], cmap='hsv')
```

```python
def plot_newton_basins(f, fprime, n=200, extent=[-1,1,-1,1], cmap='jet'):
    """Shows basin of attraction for convergence to each root using the Newton-Raphson method."""
    root_count = 0
    roots = {}
    m=np.zeros(n*n).reshape(n,n)
    xmin, xmax, ymin, ymax = extent
    for a, x in enumerate(np.linspace(xmin, xmax, n)):
        for b, y in enumerate(np.linspace(ymin, ymax, n)):
            res = x + y*1j
            root = np.round(newton(res, f, fprime)[1],1)
            if not root in roots:
                roots[root] = root_count
                root_count += 1
            m[a, b] = roots[root]
    plt.imshow(m.T, cmap=cmap, extent=extent)
    plt.show()
```

```python
root_count = 0
roots = {}
    m=np.zeros(n*n).reshape(n,n)
    xmin, xmax, ymin, ymax = extent
    for a, x in enumerate(np.linspace(xmin, xmax, n)):
        for b, y in enumerate(np.linspace(ymin, ymax, n)):
            res = x + y*1j
            root = np.round(newton(res, f, fprime)[1],1)
            if not root in roots:
                roots[root] = root_count
                root_count += 1
            m[a, b] = roots[root]

```

    0.90000000000000002

```python
root_count = 0
roots = {}
```

```python
x=1
y=1
```

```python
res = x + y*1j
root = np.round(newton(res, f, fprime)[1],1)
if not root in roots:
    roots[root] = root_count
    root_count += 1

```

```python
roots
```

    {(0.90000000000000002+0j): 0}

```python
root = np.round(newton(res, f, fprime)[1],1)
```

```python
plot_newton_basins(f,fprime)
```

```python

```

**3**. (20 points)

Consider the following function on $\mathbb{R}^2$:

$$
f(x_1,x_2) = -x_1x_2e^{-\frac{(x_1^2+x_2^2)}{2}}
$$

- Find the minimum under the constraint
$$g(x) = x_1^2+x_2^2 \leq 10$$
and
$$h(x) = 2x_1 + 3x_2 = 5$$ using `scipy.optimize.minimize`.
- Plot the function contours using `matplotlib`, showing the constraints $g$ and $h$ and indicate the constrained minimum with an `X`.

```python
from scipy.optimize import minimize, rosen, rosen_der
```

```python
fun = lambda x: -x[0]*x[1]*np.exp(-0.5*(x[0]**2+x[1]**2))

cons = ({'type': 'ineq', 'fun': lambda x: 10- (x[0]**2 + x[1]**2 )},
         {'type': 'eq', 'fun': lambda x: 2*x[0] + 3 * x[1]-5})

bnds = ((0, None), (0, None))
```

```python
res=minimize(fun, (2, 0), method='SLSQP', bounds=bnds,
                constraints=cons)
```

```python
res.x
```

    array([ 1.00000707,  0.99999529])

```python
res
```

         fun: -0.36787944114487037
         jac: array([  5.20423055e-06,  -3.46824527e-06])
     message: 'Optimization terminated successfully.'
        nfev: 32
         nit: 8
        njev: 8
      status: 0
     success: True
           x: array([ 1.00000707,  0.99999529])

```python
x = np.linspace(-5, 5, 200)
y = np.linspace(-5, 5, 200)

X, Y = np.meshgrid(x, y)
Z=fun([X,Y])

plt.contour(X, Y, Z,10)

plt.plot(x, (5-2*x)/3, 'k:', linewidth=1)
plt.plot(x, (10.0-x**2)**0.5, 'k:', linewidth=1)
plt.plot(x, -(10.0-x**2)**0.5, 'k:', linewidth=1)
plt.fill_between(x,(10-x**2)**0.5,-(10-x**2)**0.5,alpha=0.15)

plt.text(res['x'][0], res['x'][1], 'x', va='center', ha='center', size=20, color='red')
plt.axis([-5,5,-5,5])
plt.title('Contour plot of f(x) subject to constraints g(x) and h(x)')
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()
```

**4** (30 points)

Find solutions to $x^3 + 4x^2 -3 = x$.

- Write a function to find brackets, assuming roots are always at least 1 unit apart and that the roots lie between -10 and 10
- For each bracket, find the enclosed root using
    - a bisection method
    - Newton-Raphson (no guarantee to stay within brackets)
- Use the end points of the bracket as starting points for the bisection methods and the midpoint for Newton-Raphson.
- Use the companion matrix and characteristic polynomial to find the solutions
- Plot the function and its roots (marked with a circle) in a window just large enough to contain all roots.

Use a tolerance of 1e-6.

$x^3 + 4x^2 -3 = x$.

### bisection method

```python
def samesign(a, b):
    return a * b > 0
def bisect(func, low, high):
    for i in range(54):
        midpoint = (low + high) / 2.0
        if samesign(func(low), func(midpoint)):
            low = midpoint
        else:
            high = midpoint
    return midpoint
def f(x):
        return x**3+4*x**2-3-x
```

```python
bisect(f, -3, 0)
```

```python
bisect(f, -0.5, 1)
```

    0.8919544406639164

```python
def fprime(x,dx=1e-10):
    return((f(x+dx) - f(x))/dx)
def newtonraphson(z, f, max_iter=100, tol=1e-6):
    iters=0
    while True:
        iters=iters+1
        y=z-f(z)/fprime(z)
        if abs(z-y)<tol:
            return(iters,y)
        if abs(fprime(z))<tol:
            return(iters,y)
        z=y
        if iter==max_iter:
            return(iters,y)
            print('break')
```

```python
newtonraphson(1,f)
```

```python
newtonraphson(-5,f)
```

```python
newtonraphson(-2,f)
```

```python
x=np.linspace(-5,2,100)
y=f(x)
```

```python
x0=list(roots.values())
y0=np.zeros(len(x0))
```

```python
x0
```

```python
plt.plot(x,y)
plt.axhline(0, c='red')
plt.scatter(x0,y0,c='red')
plt.show()
```

```python

```

```python

```

```python

```


Neuromorphic engineering I

## Lab 8: Silicon Synaptic Circuits

Team member 1: Maxim Gaertner

Team member 2: Jan Hohenheim

Date:

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

This week, we will see how synaptic circuits generate currents when stimulated by voltage pulses. Specifically we will measure the response of the synapse to a single pulse, and to a sequence of spikes.

The objectives of this lab are to:
- Analyze log-domain synapse circuits.
- Measure the response properties of the diff-pair integrator (DPI) synapse and of the dual diff-pair integrator (DDI) synapse.

## 1. Prelab

**A Differential Pair Integrator circuit**

**(1)** Write the equations characterizing $I_{w}, I_{thr} , I_{in}, I_{\tau}, I_{syn}, I_C$ assuming all corresponding FETs are in saturation and operate in weak-inversion.

> $I_w = e^{V_{thr}}$

$I_{thr} = e^{{V_w} - {V_?}}$

$I_{in} = I_w * \frac{e^{V_{syn}}}{e^{V_{syn}}+e^{V_{thr}}}$

$I_{\tau} = e^{{Vdd} - {\tau}}$

$I_{syn} = e^{{Vdd} - {V_{syn}}}$

$I_{C} = C * \frac{d}{dt} Vdd -V_{syn}$

**(2)** What is the time constant of the circuit?

$\tau = \frac{C}{\kappa I_t}$

**(3)** Derive the circuit's response to a step input assuming $I_{w}(t < 0) = 0, I_{w}(t > 0) \gg I_{\tau}$.

$\tau \frac{d}{dt} I_{syn}+I_{syn} = \frac{I_w I_{thr}}{I_{\tau }}$

**(4)** Derive the circuit's response to a step input assuming $I_{w}(t < 0) \gg I_{\tau}, I_{w}(t > 0) = 0$.

$\frac{d}{dt} I_{syn} = -\frac{I_{syn}}{\tau}$

**(5)** Suppose we stimulate the circuit with a regular spike train of frequency $f$ (high enough). What happens to $I_{syn}$ in steady-state (average value)?

> $I_{syn} = \frac{I_w I_{thr}}{I_{\tau}}$ it becomes a steady current.

**(6)** In what conditions (tau and thr) is the step response dependent only on $I_{w}$?

> when $I_{thr}= I_{\tau}$

# 2 Setup

## 2.1 Connect the device

```python
# import the necessary libraries
import pyplane
import time
import numpy as np
import matplotlib.pyplot as plt
from scipy import interpolate
```

```python
# create a Plane object and open the communication
if 'p' not in locals():
    p = pyplane.Plane()
    try:
        p.open('/dev/ttyACM0')
    except RuntimeError as e:
        del p
        print(e)
```

```python
p.get_firmware_version()
```

    (1, 8, 4)

```python
# Send a reset signal to the board, check if the LED blinks
p.reset(pyplane.ResetType.Soft)
time.sleep(0.5)
# NOTE: You must send this request events every time you do a reset operetion, otherwise the recieved data is noisy.
# Because the class chip need to do handshake to get the communication correct.
p.request_events(1)
```

```python
# Try to read something, make sure the chip responses
p.read_current(pyplane.AdcChannel.GO0_N)
```

    1.6113281731122697e-07

```python
 # If any of the above steps fail, delete the object, close and halt, stop the server and ask the TA to restart
# please also say your board number: ttyACMx
# del p
```

## 2.2 Chip configuration

* To measure DPI synapse:

```python
p.send_coach_events([pyplane.Coach.generate_aerc_event(
    pyplane.pyplane.Coach.CurrentOutputSelect.SelectLine5,
    pyplane.Coach.VoltageOutputSelect.SelectLine2,
    pyplane.Coach.VoltageInputSelect.NoneSelected,
    pyplane.Coach.SynapseSelect.DPI,0)])
```

## 2.3 C2F

* To set up the C2F circuit:

```python
# setup C2F
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.C2F_HYS_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 100)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.C2F_BIAS_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I240nA, 255)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.C2F_PWLK_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I240nA, 255)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.C2F_REF_L, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I240nA, 255)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.C2F_REF_H, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I240nA, 255)])

# setup output rail-to-rail buffer
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.RR_BIAS_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I240nA, 255)])
```

## 2.4 BiasGen

In a simplified form, the output of a branch of the BiasGen will be the gate voltage $V_b$ for the bias current $I_b$, and if the current mirror has a ratio of $w$ and the bias transistor operates in subthreshold-saturation:
\begin{equation}
I_b = w\frac{BG_{fine}}{256}I_{BG_{master}}
\end{equation}
Where $I_{BG_{master}}$ is the `BiasGenMasterCurrent` $\in \left\{ 60~\rm{pA}, 460~\rm{pA}, 3.8~\rm{nA}, 30~\rm{nA}, 240~\rm{nA} \right\}$, $BG_{fine}$ is the integer fine value $\in [0, 256)$

To set a bias, use the function similar to the following:

```
p.send_coach_event(pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.BIAS_NAME, \
    pyplane.Coach.BiasType.BIAS_TYPE, \
    pyplane.Coach.BiasGenMasterCurrent.MASTER_CURRENT, FINE_VALUE))
```

**You may have noticed that there are some biases that are not used to directly generate a current, but rather what matters is the voltage, e.g. $V_{gain}$, $V_{ex}$ and $V_{inh}$ in our HWTA circuit. Even though they may have a `BIAS_NAME` ending with `_N` or `_P` it only indicates that they are connected to the gate of an N- or a P-FET, but the `BIAS_TYPE` parameter can be both `_N` or `_P`. For example, setting a `_N` bias to `BIAS_TYPE = P` will only make this voltage very close to GND, which _is_ sometimes the designed use case.**

## 2.5 Pulse extender circuit

In case you didn't look into the last problem in prelab, the pulse extender circuit basically defines the pulse width, which is inversely proportional to the parameter `PEX_VTAU_N`.

# 3 DPI synapse

The **DPI synapse** receives a voltage pulse train, $V_{pulse}$, as input and
outputs a corresponding synaptic current, $I_{syn}$. Additionally, the synaptic voltage, $V_{syn}$, is provided.
Bias parameters $V_{weight}$ & $V_{tau}$ affect the amplitude and decay of the response, while $V_{thr}$ acts as an additional weight bias. $C_{syn}$ sizing was chosen for a capacitance of 2pF.

**Pin map**

**$V_{syn}$ = adc[14]**

**$I_{syn}$ = c2f[9]**

The task of this exercise it to tune the parameters and observe the behavior of the DPI synapse.

## 3.1 Basic impulse response

- **Set parameters**

```python
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTAU_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 25)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTHR_N, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 30)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VWEIGHT_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I30nA, 100)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.PEX_VTAU_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)])
```

- **Data acquisition**

```python
N_pulses = 2 # for each trial, send 2 input pulses
N_samples_per_pulse = 10 # for each input pulse, sample 10 points
N_samples = N_pulses*N_samples_per_pulse
dT = 0.02 # delta t between the samples, DO NOT CHANGE
t = np.arange(N_samples)*dT
vsyn = np.zeros(N_samples)
isyn = np.zeros(N_samples)
for k in range(N_pulses):
    p.send_coach_events([pyplane.Coach.generate_pulse_event()])
    for i in range(N_samples_per_pulse):
        vsyn[k*N_samples_per_pulse+i] += p.read_voltage(pyplane.AdcChannel.AOUT14)
        inter=p.read_c2f_output(dT)
        isyn[k*N_samples_per_pulse+i] += inter[9]
```

- **Plot the data**

```python
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams.update({'font.size': 15})

t,vsyn,isyn = np.loadtxt('data_ex_3_1.csv',delimiter=',')
plt.plot(t,vsyn,'-')

plt.xlabel('t [s]')
plt.ylabel('$V_{syn}$ [V]')
plt.legend(['$V_{syn}$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 1: Measured values of $V_{syn}$ as a function of time')

plt.grid()
plt.show()

plt.plot(t,isyn,'-')

plt.xlabel('t [s]')
plt.ylabel('C2F [Hz]')
plt.legend(['C2F$(I_{syn})$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 2: Measured C2F values of $I_{syn}$ as a function of time')

plt.grid()
plt.show()
```

- **Save the data**

```python
np.savetxt('data_ex_3_1.csv',[t,vsyn,isyn] , delimiter=',')
```

## 3.2 Different $I_{weight}$

Repeat 3.1 with a smaller and a larger  $I_{weight}$, compare the three curves in the same plot.

- **Set smaller bias**

```python
## REMINDER , RESET ALL PARAMETERS AS 3.1
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTAU_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 25)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTHR_N, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 30)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VWEIGHT_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I30nA, 50)]) #change weight

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.PEX_VTAU_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)])
```

- **Data acquisition**

```python
N_pulses = 2 # for each trial, send 2 input pulses
N_samples_per_pulse = 10 # for each input pulse, sample 10 points
N_samples = N_pulses*N_samples_per_pulse
dT = 0.02 # delta t between the samples, DO NOT CHANGE
t = np.arange(N_samples)*dT
vsyn = np.zeros(N_samples)
isyn = np.zeros(N_samples)
for k in range(N_pulses):
    p.send_coach_events([pyplane.Coach.generate_pulse_event()])
    for i in range(N_samples_per_pulse):
        vsyn[k*N_samples_per_pulse+i] += p.read_voltage(pyplane.AdcChannel.AOUT14)
        inter=p.read_c2f_output(dT)
        isyn[k*N_samples_per_pulse+i] += inter[9]
```

- **Save data**

```python
np.savetxt('data_ex_3_2_smaller.csv',[t,vsyn,isyn] , delimiter=',')
```

- **Set largerer bias**

```python
#Insert a bigger I weight
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTAU_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 25)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTHR_N, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 30)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VWEIGHT_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I240nA, 35)]) #change weight

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.PEX_VTAU_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)])
```

- **Data acquisition**

```python
N_pulses = 2 # for each trial, send 2 input pulses
N_samples_per_pulse = 10 # for each input pulse, sample 10 points
N_samples = N_pulses*N_samples_per_pulse
dT = 0.02 # delta t between the samples, DO NOT CHANGE
t = np.arange(N_samples)*dT
vsyn = np.zeros(N_samples)
isyn = np.zeros(N_samples)
for k in range(N_pulses):
    p.send_coach_events([pyplane.Coach.generate_pulse_event()])
    for i in range(N_samples_per_pulse):
        vsyn[k*N_samples_per_pulse+i] += p.read_voltage(pyplane.AdcChannel.AOUT14)
        inter=p.read_c2f_output(dT)
        isyn[k*N_samples_per_pulse+i] += inter[9]
```

- **Save data**

```python
np.savetxt('data_ex_3_2_bigger.csv',[t,vsyn,isyn] , delimiter=',')
```

- **Plot**

```python
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams.update({'font.size': 15})

t,vsyn_smaller,isyn_smaller = np.loadtxt('data_ex_3_2_smaller.csv',delimiter=',')
_,vsyn_normal,isyn_normal = np.loadtxt('data_ex_3_1.csv',delimiter=',')
_,vsyn_bigger,isyn_bigger = np.loadtxt('data_ex_3_2_bigger.csv',delimiter=',')

plt.plot(t,vsyn_smaller,t,vsyn_normal,t,vsyn_bigger)

plt.xlabel('t [s]')
plt.ylabel('$V_{syn}$ [V]')
plt.legend(['$V_{syn}$ - Smaller $I_w$','$V_{syn}$ - Normal $I_w$','$V_{syn}$ - Larger $I_w$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 3: Measured values of $V_{syn}$ as function of time for varying $I_{w}$')

plt.grid()
plt.show()

plt.plot(t[1:],isyn_smaller[1:],t,isyn_normal,t,isyn_bigger)

plt.xlabel('t [s]')
plt.ylabel('C2F [Hz]')
plt.legend(['C2F$(I_{syn})$ - Smaller $I_w$','C2F$(I_{syn})$ - Normal $I_w$','C2F$(I_{syn})$ - Larger $I_w$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 4: Measured values of $I_{syn}$ as function of time for varying $I_{w}$')

plt.grid()
plt.show()
```

## 3.3 Different $I_{tau}$

Repeat 3.1 with a smaller and a larger  $I_{tau}$, compare the three curves in the same plot.

```python
## REMINDER , RESET ALL PARAMETERS AS 3.1
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTAU_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 5)])   #change tau

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTHR_N, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 30)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VWEIGHT_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I30nA, 100)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.PEX_VTAU_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)])
```

```python
N_pulses = 2 # for each trial, send 2 input pulses
N_samples_per_pulse = 10 # for each input pulse, sample 10 points
N_samples = N_pulses*N_samples_per_pulse
dT = 0.02 # delta t between the samples, DO NOT CHANGE
t = np.arange(N_samples)*dT
vsyn = np.zeros(N_samples)
isyn = np.zeros(N_samples)
for k in range(N_pulses):
    p.send_coach_events([pyplane.Coach.generate_pulse_event()])
    for i in range(N_samples_per_pulse):
        vsyn[k*N_samples_per_pulse+i] += p.read_voltage(pyplane.AdcChannel.AOUT14)
        inter=p.read_c2f_output(dT)
        isyn[k*N_samples_per_pulse+i] += inter[9]
```

```python
np.savetxt('data_ex_3_3_smaller.csv',[t,vsyn,isyn] , delimiter=',')
```

```python
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTAU_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 50)])    #change tau

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTHR_N, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 30)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VWEIGHT_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I30nA, 100)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.PEX_VTAU_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)])
```

```python
N_pulses = 2 # for each trial, send 2 input pulses
N_samples_per_pulse = 10 # for each input pulse, sample 10 points
N_samples = N_pulses*N_samples_per_pulse
dT = 0.02 # delta t between the samples, DO NOT CHANGE
t = np.arange(N_samples)*dT
vsyn = np.zeros(N_samples)
isyn = np.zeros(N_samples)
for k in range(N_pulses):
    p.send_coach_events([pyplane.Coach.generate_pulse_event()])
    for i in range(N_samples_per_pulse):
        vsyn[k*N_samples_per_pulse+i] += p.read_voltage(pyplane.AdcChannel.AOUT14)
        inter=p.read_c2f_output(dT)
        isyn[k*N_samples_per_pulse+i] += inter[9]
```

```python
np.savetxt('data_ex_3_3_bigger.csv',[t,vsyn,isyn] , delimiter=',')
```

```python
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams.update({'font.size': 15})

t,vsyn_smaller,isyn_smaller = np.loadtxt('data_ex_3_3_smaller.csv',delimiter=',')
_,vsyn_normal,isyn_normal = np.loadtxt('data_ex_3_1.csv',delimiter=',')
_,vsyn_bigger,isyn_bigger = np.loadtxt('data_ex_3_3_bigger.csv',delimiter=',')

plt.plot(t,vsyn_smaller,t,vsyn_normal,t,vsyn_bigger)

plt.xlabel('t [s]')
plt.ylabel('$V_{syn}$ [V]')
plt.legend(['$V_{syn}$ - Smaller $I_{𝜏}$','$V_{syn}$ - Normal $I_{𝜏}$','$V_{syn}$ - Larger $I_{𝜏}$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 5: Measured values of $V_{syn}$ as function of time for varying $I_{𝜏}$')

plt.grid()
plt.show()

plt.plot(t,isyn_smaller,t,isyn_normal,t,isyn_bigger)

plt.xlabel('t [s]')
plt.ylabel('C2F [Hz]')
plt.legend(['C2F$(I_{syn})$ - Smaller $I_{𝜏}$','C2F$(I_{syn})$ - Normal $I_{𝜏}$','C2F$(I_{syn})$ - Larger $I_{𝜏}$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 6: Measured values of $I_{syn}$ as function of time for varying $I_{𝜏}$')

plt.grid()
plt.show()
```

## 3.4 Different $I_{thr}$

Repeat 3.1 with a smaller and a larger $I_{thr}$, compare the three curves in the same plot.

```python
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTAU_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 25)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTHR_N, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)]) #change threshold

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VWEIGHT_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I30nA, 100)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.PEX_VTAU_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)])
```

```python
N_pulses = 2 # for each trial, send 2 input pulses
N_samples_per_pulse = 10 # for each input pulse, sample 10 points
N_samples = N_pulses*N_samples_per_pulse
dT = 0.02 # delta t between the samples, DO NOT CHANGE
t = np.arange(N_samples)*dT
vsyn = np.zeros(N_samples)
isyn = np.zeros(N_samples)
for k in range(N_pulses):
    p.send_coach_events([pyplane.Coach.generate_pulse_event()])
    for i in range(N_samples_per_pulse):
        vsyn[k*N_samples_per_pulse+i] += p.read_voltage(pyplane.AdcChannel.AOUT14)
        inter=p.read_c2f_output(dT)
        isyn[k*N_samples_per_pulse+i] += inter[9]
```

```python
np.savetxt('data_ex_3_4_smaller.csv',[t,vsyn,isyn] , delimiter=',')
```

```python
p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTAU_P, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 25)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VTHR_N, \
    pyplane.Coach.BiasType.P, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 80)]) #change threshold

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.DPI_VWEIGHT_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I30nA, 100)])

p.send_coach_events([pyplane.Coach.generate_biasgen_event(\
    pyplane.Coach.BiasAddress.PEX_VTAU_N, \
    pyplane.Coach.BiasType.N, \
    pyplane.Coach.BiasGenMasterCurrent.I60pA, 10)])
```

```python
N_pulses = 2 # for each trial, send 2 input pulses
N_samples_per_pulse = 10 # for each input pulse, sample 10 points
N_samples = N_pulses*N_samples_per_pulse
dT = 0.02 # delta t between the samples, DO NOT CHANGE
t = np.arange(N_samples)*dT
vsyn = np.zeros(N_samples)
isyn = np.zeros(N_samples)
for k in range(N_pulses):
    p.send_coach_events([pyplane.Coach.generate_pulse_event()])
    for i in range(N_samples_per_pulse):
        vsyn[k*N_samples_per_pulse+i] += p.read_voltage(pyplane.AdcChannel.AOUT14)
        inter=p.read_c2f_output(dT)
        isyn[k*N_samples_per_pulse+i] += inter[9]
```

```python
np.savetxt('data_ex_3_4_bigger.csv',[t,vsyn,isyn] , delimiter=',')
```

```python
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams.update({'font.size': 15})

t,vsyn_smaller,isyn_smaller = np.loadtxt('data_ex_3_4_smaller.csv',delimiter=',')
_,vsyn_normal,isyn_normal = np.loadtxt('data_ex_3_1.csv',delimiter=',')
_,vsyn_bigger,isyn_bigger = np.loadtxt('data_ex_3_4_bigger.csv',delimiter=',')

plt.plot(t,vsyn_smaller,t,vsyn_normal,t,vsyn_bigger)

plt.xlabel('t [s]')
plt.ylabel('$V_{syn}$ [V]')
plt.legend(['$V_{syn}$ - Smaller $I_{thr}$','$V_{syn}$ - Normal $I_{thr}$','$V_{syn}$ - Larger $I_{thr}$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 7: Measured values of $V_{syn}$ as function of time for varying $I_{thr}$')

plt.grid()
plt.show()

plt.plot(t[1:],isyn_smaller[1:],t,isyn_normal,t,isyn_bigger)

plt.xlabel('t [s]')
plt.ylabel('C2F [Hz]')
plt.legend(['C2F$(I_{syn})$ - Smaller $I_{thr}$','C2F$(I_{syn})$ - Normal $I_{thr}$','C2F$(I_{syn})$ - Larger $I_{thr}$'],bbox_to_anchor=(1.05, 1),loc='upper left')
plt.title('Fig. 8: Measured values of $I_{syn}$ as function of time for varying $I_{thr}$')

plt.grid()
plt.show()
```

## 3.5 Different pulse width

Repeat 3.1 with a smaller and a larger pulse width, compare the three curves in the same plot.


# Boris particle pusher in electromagnetic field
\begin{align}
\textbf{x}^{n+1}=\textbf{x}^n+\Delta t\textbf{v}^{n+1/2} \\
\textbf{v}^{n+1/2}=\textbf{u}^\prime+q^\prime\textbf{E}^{n} \\
\textbf{u}^\prime=\textbf{u}+(\textbf{u}+(\textbf{u}\times\textbf{h}))\times\textbf{s} \\
\textbf{u}=\textbf{v}^{n-1/2}+q^\prime\textbf{E}^n \\
\textbf{h} = q^\prime\textbf{B}^n \\
\textbf{s}=2\textbf{h}/(1+h^2) \\
q^\prime=\Delta t\frac{q}{2m}
\end{align}

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d

# ... parameters
q = -1                   # ... charge of particle
m = 1                    # ... mass of particle
T = 10*2*np.pi           # ... simulation time
Nt = 10000               # ... number of time steps
dt = T/Nt                # ... time step
B0x0 = 0.5               # ... field at x = 0 for magnetic mirror
a = 0                    # ... inhomogeneity factor of magnetic mirror
Omega = q*B0x0/m         # ... cyclotron frequency
# ...

# ...
x = np.linspace(-60,60,8)
y = np.linspace(-60,60,8)
z = np.linspace(-60,60,8)
t = np.linspace(0,T,Nt+1)

X,Y = np.meshgrid(x,y)
# ...

# ... define magnetic field
def B(x,shape):

    if shape == 1:
        return np.array([0,0,1])
    elif shape == 2:
        return np.array([-x[0]*B0x0*a*x[2],-x[1]*B0x0*a*x[2],B0x0*(1+a*x[2]**2)])
# ...

# ... define electric field
def E(x):
    return np.array([0,0.05,0])
# ...

# ... particle positions and velocities
xp = np.zeros((Nt+1,3))
vp = np.zeros((Nt+1,3))
# ...

# ... initial velocity
vp[0] = np.array([0,1,2])
# ...

# ... lamor radius
rl = np.abs(m*vp[0,1]/(q*np.linalg.norm(B([0,0,0],2))))
# ...

# ... initial position
xp[0] = np.array([rl,0,0])
# ...

# ... Boris pusher from v^(n-1/2) to v^(n+1/2) and x^n to x^(n+1)
def push(xold,vold,dt,B,E):

    qprime = dt*q/(2*m)
    h = qprime*B(xold,2)
    s = 2*h/(1 + np.linalg.norm(h)**2)
    u = vold + qprime*E(xold)
    uprime = u + np.cross(u + np.cross(u,h),s)
    vnew = uprime + qprime*E(xold)
    xnew = xold + dt*vnew

    return xnew,vnew
# ...

# ... initialize velocity with -0.5*dt
vp[0] = push(xp[0],vp[0],-0.5*dt,B,E)[1]
# ...

# ... time integration
for n in range(0,Nt):
    xp[n+1],vp[n+1] = push(xp[n],vp[n],dt,B,E)
# ...

plt.plot(xp[:,0],xp[:,1])
plt.axis('equal')
plt.xlabel('x')
plt.ylabel('y')

fig = plt.figure()
fig.set_figheight(10)
fig.set_figwidth(10)
ax = fig.add_subplot(111,projection = '3d')
ax.plot(xp[:,0],xp[:,1],xp[:,2])
plt.xlabel('x')
plt.ylabel('y')
ax.set_zlabel('z')
```

```python

```