```python
from pathlib import Path
from functools import partial

from utils import load_polar

import pybem as pb
import numpy as np
import pandas as pd
import proplot as plot
import sympy as sy
```

Inputs

How to compute the Aspect Ratio of the blade.

$$
AR = R^2 / S
$$
where $S$ is the blade area, computed like:
$$
S = \int_{R_{min}}^{R} c(r)dr
$$

We are now going to use `sympy` to compute the area numerically for some special cases to validate our simulator. We assume the chord to vary linearly with `r`.

```python
r, R, R0 = sy.symbols("r, R, R0")
a, b = sy.symbols("a, b")
c, c0 = sy.symbols("c, c0")
```

Set up a generic linear variation

```python
expr = a + b * r
expr
```

$\displaystyle a + b r$

Create system of equations to determine the coefficients such that they match initial and final values.

```python
eq1 = sy.Eq(lhs = expr.subs(r, R), rhs = c) # Tip
eq2 = sy.Eq(lhs = expr.subs(r, R0), rhs = c0) # Hub
```

Solve the system and obtain the coefficients

```python
sol = sy.linsolve([eq1, eq2], (a, b))

a_sol , b_sol = sol.args[0]
print("a =", a_sol)
print("b =", b_sol)
```

    a = (R*c0 - R0*c)/(R - R0)
    b = (c - c0)/(R - R0)

Create chord variation equation and integrate to obtain blade area.

```python
chord = a_sol + b_sol * r

area = sy.factor(sy.integrate(chord, (r, R0, R)))
area
```

$\displaystyle \frac{\left(R - R_{0}\right) \left(c + c_{0}\right)}{2}$

```python
print(sy.pycode(area))

print(sy.pycode(chord))
```

    (1/2)*(R - R0)*(c + c0)
    r*(c - c0)/(R - R0) + (R*c0 - R0*c)/(R - R0)

```python
PATH_POLARS = Path("../polars")
file_lift = "lift.csv"
file_drag = "drag.csv"
file_propeller = PATH_POLARS / "propeller1_inches.txt"

PITCH_RATIO = 0.9
PITCH = 32.4  # inches for diameter of 36 inches
```

```python
INCH_METER = 0.0254
PITCH = INCH_METER * PITCH  # meters
```

```python
csv_reader = partial(pd.read_csv, sep=";", index_col=None)
load_polar = partial(
    load_polar,
    path=PATH_POLARS,
    file_lift=file_lift,
    file_drag=file_drag,
    reader=csv_reader,
)
```

```python
dimensions_original_df = pd.read_csv(filepath_or_buffer=file_propeller, sep=",")

# Enforce stripped columns
dimensions_original_df.columns = [col.strip() for col in dimensions_original_df.columns]
dimensions_original_df = dimensions_original_df[["radius", "chord"]]

# Convert to international units
dimensions_original_df = dimensions_original_df.mul(INCH_METER)

# Name
dimensions_original_df.index = ["Hub", "S1", "S2", "S3", "S4", "Tip"]
```

Create the blade by defining the sections at each stage.

```python
def generate_sections(coefficient, dimensions_original):

    dimensions_df = dimensions_original.copy()

    # Scale the propeller size and compute theta
    dimensions_df["chord"] *= coefficient

    R = dimensions_df["radius"].max()

    # Compute pitch angle distribution
    for idx, row in dimensions_df.iterrows():

        r = row["radius"]

        # Constant pitch law
        tan_theta = (2.0 * PITCH_RATIO * R) / (2.0 * np.pi * r)
        theta = np.arctan(tan_theta)  # radians
        theta = np.rad2deg(theta)  # degrees

        dimensions_df.loc[idx, "theta"] = theta

    # Define sections
    sections = [
        pb.Section(
            name="Hub",
            r=dimensions_df.loc["Hub", "radius"],
            beta=dimensions_df.loc["Hub", "theta"],
            chord=dimensions_df.loc["Hub", "chord"],
            airfoil=pb.Airfoil(
                polar_cl=load_polar(airfoil_type="765", which="cl"),
                polar_cd=load_polar(airfoil_type="765", which="cd"),
            ),
        ),
        pb.Section(
            name="S1",
            r=dimensions_df.loc["S1", "radius"],
            beta=dimensions_df.loc["S1", "theta"],
            chord=dimensions_df.loc["S1", "chord"],
            airfoil=pb.Airfoil(
                polar_cl=load_polar(airfoil_type="765", which="cl"),
                polar_cd=load_polar(airfoil_type="765", which="cd"),
            ),
        ),
        pb.Section(
            name="S2",
            r=dimensions_df.loc["S2", "radius"],
            beta=dimensions_df.loc["S2", "theta"],
            chord=dimensions_df.loc["S2", "chord"],
            airfoil=pb.Airfoil(
                polar_cl=load_polar(airfoil_type="764", which="cl"),
                polar_cd=load_polar(airfoil_type="764", which="cd"),
            ),
        ),
        pb.Section(
            name="S3",
            r=dimensions_df.loc["S3", "radius"],
            beta=dimensions_df.loc["S3", "theta"],
            chord=dimensions_df.loc["S3", "chord"],
            airfoil=pb.Airfoil(
                polar_cl=load_polar(airfoil_type="763", which="cl"),
                polar_cd=load_polar(airfoil_type="763", which="cd"),
            ),
        ),
        pb.Section(
            name="S4",
            r=dimensions_df.loc["S4", "radius"],
            beta=dimensions_df.loc["S4", "theta"],
            chord=dimensions_df.loc["S4", "chord"],
            airfoil=pb.Airfoil(
                polar_cl=load_polar(airfoil_type="762", which="cl"),
                polar_cd=load_polar(airfoil_type="762", which="cd"),
            ),
        ),
        pb.Section(
            name="Tip",
            r=dimensions_df.loc["Tip", "radius"],
            beta=dimensions_df.loc["Tip", "theta"],
            chord=dimensions_df.loc["Tip", "chord"],
            airfoil=pb.Airfoil(
                polar_cl=load_polar(airfoil_type="761", which="cl"),
                polar_cd=load_polar(airfoil_type="761", which="cd"),
            ),
        ),
    ]

    return sections
```

```python
# Define propeller
COEFF_0 = 0.5
COEFF_N = 3
N = 5

results = []
for coeff in np.linspace(start=COEFF_0, stop=COEFF_N, num=N):

    sections = generate_sections(
        coefficient=coeff, dimensions_original=dimensions_original_df
    )

    propeller = pb.Propeller(B=2, sections=sections)

    # Define flow conditions and BEM method

    for J in np.linspace(1e-1, 1.5):

        pi = np.pi

        _lambda = J / pi

        bem = pb.BladeElementMethod(
            _lambda=_lambda, propeller=propeller, tip_loss=True, hub_loss=False
        )

        try:
            # Solve
            bem.solve()
            CT, CQ = bem.integrate_forces()
        except:
            CT = np.nan
            CQ = np.nan

        AR = propeller.aspect_ratio

        # Create experiments non-dimensionless coefficients
        results.append(
            (
                AR,
                J,
                CT * (pi ** 3.0) / 4 / J ** 2.0,
                CQ * (pi ** 4.0) / 4 / J ** 2.0,
                _lambda * CT / CQ,  # efficiency
            )
        )

        del bem

    del propeller

results_df = pd.DataFrame(results, columns=["AR", "J", "CT", "CP", "eta"]).dropna()
```

```python
results_df
```

      AR
      J
      CT
      CP
      eta

      12
      17.314629
      0.442857
      0.282554
      0.204950
      0.610545

      13
      17.314629
      0.471429
      0.241452
      0.179703
      0.633418

      14
      17.314629
      0.500000
      0.207440
      0.158395
      0.654817

      15
      17.314629
      0.528571
      0.178951
      0.140261
      0.674372

      16
      17.314629
      0.557143
      0.155154
      0.124850
      0.692374

      ...
      ...
      ...
      ...
      ...
      ...

      225
      2.885771
      0.814286
      0.166354
      0.206728
      0.655255

      226
      2.885771
      0.842857
      0.143503
      0.185501
      0.652031

      227
      2.885771
      0.871429
      0.122904
      0.166048
      0.645008

      228
      2.885771
      0.900000
      0.104567
      0.148475
      0.633843

      229
      2.885771
      0.928571
      0.088222
      0.132814
      0.616802

118 rows × 5 columns

```python
_array = [[1, 2], [3, 3]]
fig, axes = plot.subplots(array=_array, share=0)

Rs = results_df["AR"].unique()
for R in Rs:

    mask = results_df["AR"] == AR
    _results_df = results_df.loc[mask].set_index("J").drop("AR", axis=1)

    name = np.round(AR, 2)
    name = str(name)
    ############
    # Thrust
    ############
    ax = axes[0]
    thrust = _results_df["CT"]
    thrust.name = name
    lines = ax.plot(
        thrust,
        cycle="plum",
    )
    ax.format(
        title="Thrust",
        ylabel="$C_T$",
    )

    ############
    # Torque
    ############
    ax = axes[1]
    torque = _results_df["CP"]
    torque.name = name

    ax.plot(
        torque,
        cycle="plum",
    )
    ax.format(
        title="Torque / Power",
        ylabel="$C_P$",
    )

    ############
    # Efficiency
    ############
    ax = axes[-1]
    eta = _results_df["eta"]
    eta.name = name
    ax.plot(eta, cycle="plum")
    ax.format(title="Efficiency", ylabel="$\eta$")

axes[-1].legend(title="Aspect Ratio")

fig.save("Aspect_Ratio.pdf")
```

```python

```

notebooks/Geometry influence - Aspect Ratio.ipynb


## Poisson equation 1D
\begin{equation}
    \begin{cases}
	-u'' = f(x) &\text{ in } \Omega = [0,1] \\
	 u(0) = u(1) = 0 &
	\end{cases}
\end{equation}

```python
import numpy as np
```

\begin{align*}
u(x) &= sin(\pi x)\\
f(x) &= \pi^2\sin(\pi x)
\end{align*}

```python
u_ex = lambda x: np.sin(np.pi * x)
f = lambda x: np.pi ** 2 * np.sin(np.pi * x)
u_D = lambda x: np.zeros_like(x)
```

```python
from mozart.mesh.rectangle import interval
nrElems = 7
degree = 1
c4n, n4e, n4db, ind4e = interval(0, 1, nrElems, degree)
```

```python
c4n
```

    array([ 0.        ,  0.14285714,  0.28571429,  0.42857143,  0.57142857,
            0.71428571,  0.85714286,  1.        ])

```python
from mozart.poisson.fem.interval import solve
u = solve(c4n, n4e, n4db, ind4e, f, u_D, degree)
```

```python
u
```

    array([ 0.        ,  0.42667492,  0.76884164,  0.95872984,  0.95872984,
            0.76884164,  0.42667492,  0.        ])

```python
%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(c4n, u_ex(c4n), 'bo--', label='exact')
plt.plot(c4n, u, 'rx-', label='approx')
plt.grid(True)
plt.legend(loc=2)
```

### Error Convergence

```python
f = lambda x: np.pi ** 2 * np.sin(np.pi * x)
u_D = lambda x: np.zeros_like(x)
exact_u = lambda x: np.sin(np.pi * x)
exact_ux = lambda x: np.pi * np.cos(np.pi * x)
```

```python
from mozart.poisson.fem.interval import computeError
```

```python
for degree in range(1, 6):
    print("Degree of Polynomial = {0}".format(degree))
    nrElems = 3
    for it in range(0, 4):
        nrElems *= 2
        c4n, n4e, n4db, ind4e = interval(0, 1, nrElems, degree)
        u = solve(c4n, n4e, n4db, ind4e, f, u_D, degree)
        L2, sH1 = computeError(c4n, n4e, ind4e, exact_u, exact_ux, u, degree, degree + 3)
        print("L2 = {0:12.10f}, H1 = {1:12.10f}".format(L2, sH1))
    print("=====================================")
```

    Degree of Polynomial = 1
    L2 = 0.0323728529, H1 = 0.3378838486
    L2 = 0.0082306511, H1 = 0.1681670719
    L2 = 0.0020663224, H1 = 0.0839784610
    L2 = 0.0005171229, H1 = 0.0419758340
    =====================================
    Degree of Polynomial = 2
    L2 = 0.0005907883, H1 = 0.0226088307
    L2 = 0.0000731873, H1 = 0.0056686738
    L2 = 0.0000091274, H1 = 0.0014182070
    L2 = 0.0000011403, H1 = 0.0003546168
    =====================================
    Degree of Polynomial = 3
    L2 = 0.0000193160, H1 = 0.0010014343
    L2 = 0.0000012139, H1 = 0.0001254561
    L2 = 0.0000000760, H1 = 0.0000156906
    L2 = 0.0000000047, H1 = 0.0000019616
    =====================================
    Degree of Polynomial = 4
    L2 = 0.0000004448, H1 = 0.0000330369
    L2 = 0.0000000139, H1 = 0.0000020691
    L2 = 0.0000000004, H1 = 0.0000001294
    L2 = 0.0000000000, H1 = 0.0000000081
    =====================================
    Degree of Polynomial = 5
    L2 = 0.0000000103, H1 = 0.0000008697
    L2 = 0.0000000002, H1 = 0.0000000272
    L2 = 0.0000000000, H1 = 0.0000000009
    L2 = 0.0000000000, H1 = 0.0000000000
    =====================================

```python

```


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from sympy.stats import E
import warnings
warnings.filterwarnings('ignore')
np.random.seed(0)
```

### 1. Population and Sample

```python
items = ['t-shirt', 'sleeveless', 'sweat', 'pull-over', 'pants',
'trousers', 'dress', 'jacket', 'nightdress', 'underwear']
pop = np.random.choice(items, size=10000, replace=True)
samp = np.random.choice(pop, 1000)

labels, pop_counts = np.unique(pop, return_counts=True)
_, samp_counts = np.unique(samp, return_counts=True)
```

```python
fig, axs = plt.subplots(nrows = 1, ncols = 2, figsize=(10,3))
ax1, ax2 = axs[0], axs[1]
fontdict = {'fontsize':17}
ax1.bar(labels, pop_counts, align='center')
ax1.set_xticklabels(labels, rotation=80)
ax1.set_xlabel('Population', fontdict=fontdict)
ax2.bar(labels, samp_counts, align='center')
ax2.set_xticklabels(labels, rotation=80)
ax2.set_xlabel('Sample', fontdict=fontdict)
plt.show()
```

### 2. Probability distributions

```python
probs = pop_counts/np.sum(pop_counts)
plt.bar(labels, probs)
plt.xticks(rotation=75)
plt.show()
```

### 3. Normal distribution

```python
def norm(x, mean, std):
    f = np.exp(-0.5 * ((x - mean)/std**2)**2) /(std * (np.sqrt(2*np.pi)))
    return f

x = np.linspace(-4, 4, 1000)
y = [norm(i, 0, 1) for i in x]
plt.plot(x,y)
plt.grid()
plt.show()
```

### 4. Measures of central tendency

```python
print(f'mean = {np.mean(pop_counts)}')
print(f'median = {np.median(pop_counts)}')
```

    mean = 1000.0
    median = 997.0

### 5. Measures of dispersion

```python
print(f'std = {np.std(pop_counts):.2f}')
print(f'IQR = {stats.iqr(pop_counts)}')
print(f'range = {np.max(pop_counts) - np.min(pop_counts)}')
```

    std = 32.39
    IQR = 60.75
    range = 86

### 6. Expected value of a random variable

```python
print(f'E = {np.sum(pop_counts*probs):.2f}')
```

    E = 1001.05

### 7. Linear regression
#### 7.2. Example
We will create a randomly distributed dataset $x$ for which we will find the best linear model $y$ describing $x$, that is find the values of the **slope** $\beta_1$ and the **intercept** $\beta_0$ such that $y = \beta_0 + \beta_1x$.

```python
x = np.random.randn(100)
y = 1.5*x + np.random.default_rng().random(100)
res = stats.linregress(x, y)
y_hat = res.intercept + res.slope*x
plt.plot(x, y_hat, label='Data', color='red')
plt.scatter(x, y, label='Best fit')
plt.grid()
plt.legend()
plt.show()
```

### 8. Over sampling and under sampling

### 9. Probability
Let's consider the random experiment **Rolling a dice** and calculate the probabilities of few possible outcomes. Whatever the outcome, it is an integer that falls between 1 and 6, included.

In this case, $\Omega = \{1,2,3,4,5,6\}$.

Consider the following events:
- A : "Roll a 3"
- B : "Roll an even digit"
- C : "Roll a digit les than or equal to 4"
- D : "Roll an odd digit greater tha 2"
- E : "Roll 7"

```python
omega = {1,2,3,4,5,6}
A = {3}
B = {2,4,6}
C = {1,2,3,4}
D = {3,5}
E = {}
print(f'P(A) = {len(A)}/{len(omega)}\nP(B) = {len(B)}/{len(omega)}\n\
P(C) = {len(C)}/{len(omega)}\nP(D) = {len(D)}/{len(omega)}\nP(E) = {len(E)/len(omega)}\n')
```

    P(A) = 1/6
    P(B) = 3/6
    P(C) = 4/6
    P(D) = 2/6
    P(E) = 0.0

### 10. Combinations and Permutations
### Examples
- a) Permutation : How many ways (noted X) can first, second and third place be awarded to 10 people?
- b) Combination : In how many different ways (noted Y) a team of 3 people can be formed from a group of 10?

```python
# a)
X = np.math.factorial(10)/np.math.factorial(10-3)
Y = np.math.factorial(10)/(np.math.factorial(10-3)*np.math.factorial(3))
print(f"X = {int(X)}\nY = {int(Y)}")
```

    X = 720
    Y = 120

## Conclusion
Data Science is a generic field of knowledge that leverages the power of advanced mathematics to draw useful insights from data. The tenants of mathematics for data science are statistics and probability theory. The concepts presented throughout this post are just few of a large set of concepts and tools statistics provide us with, to help data scientists succeed in our data story telling.

You can find read more about this notebook [here](https://www.datainsightonline.com/post/10-must-know-statistical-concepts-for-data-science).


Godfrey Beddard 'Applying Mathis in the Chemical & Biomolecular Sciences' Chapter 9

```python
# import all python add-ons etc that will be needed later on
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from sympy import *
from scipy.integrate import quad
init_printing()                      # allows printing of SymPy results in typeset maths format
plt.rcParams.update({'font.size': 14})  # set font size for plots
```

# Answers 16-21

**Q16 answer** Using the ideas presented in the 'strategy' expanding the integral forms,

$$\displaystyle G^1(u)=2+\frac{2\int E(t)E(u+t)dt}{\int E(t)^2dt}$$

where the limits are $\pm \infty$. As a check that $\int E(t)dt=\int E(u+t)dt$. SymPy can be used to perform the integrals. It is easier to help by converting the cosines to exponentials first:

```python
omega,a,t,u = symbols('omega a t u',positive=True)

f01= cos(omega*t/a)*exp(-(t/a)**2/2)         # define electric fields
f02= cos(omega*(t+u)/a)*exp(-((t+u)/a)**2/2)
ef01   = expand(f01.rewrite(exp))
intf01 = integrate(ef01,(t,-oo,oo),conds='none')
simplify(intf01)
```

```python
# and for the t+u function
ef02   = expand(f02.rewrite(exp))
intf02 = integrate(ef02,(t,-oo,oo),conds='none')
simplify(intf02)
```

which shows that $\int E(t)dt=\int E(u+t)dt$. The same is true for the powers of the integrals. The cross term is $2\int E(t)E(u+t)dt$ and is calculated as

```python
# calculate the cross term E(t)E(t+u)
fcross = simplify(2*expand(ef01*ef02))
intfcross= integrate(fcross, (t,-oo,oo),conds='none')
simplify(intfcross)
```

This can be simplified by converting the $e^{-i\omega u/a }$ terms to  a cosine and the result is

$$\displaystyle  \int E(t)E(u+t)dt = \frac{\sqrt{\pi}}{a}e^{-u^2/(4a^2)}\left(\cos\left(\frac{\omega u}{a}\right) +e^{-\omega^2} \right)$$

The normalisation is found when $u=0$ and is $\displaystyle \int E(t)^2dt = \frac{\sqrt{\pi}}{a}(e^{-\omega^2}+1)$ and this produces the fringe resolved autocorrelation shown in figure 35. The final equation, divided by two so that the signal is 1 in the wings, is

$$\displaystyle  1+ \frac{\int E(t)E(u+t)dt}{\int E(t)^2dt}  = 1 + \frac{e^{-u^2/(4a^2)}\left(\cos\left(\frac{\omega u}{a}\right) +e^{-\omega^2} \right)}{1+e^{-\omega^2}} $$

This function is calculated and plotted as $G^1(u)$ below,

```python
G1 = lambda u, a, omega: 1+(np.exp(-u**2/(4*a**2)) *(np.cos(omega*u/a)+np.exp(-omega**2)) )/(1+np.exp(-omega**2))
a = 5
omega = 10
u = np.linspace(-20,20,500)

plt.plot(u,G1(u,a,omega))
plt.axhline(1,linewidth=1,color='grey')
plt.title(r'$G^1(u)$')
plt.xlabel('time')
plt.show()
```

**(b)** The second-order correlation is calculated in a similar way; expanding the terms before integrating gives

$$\displaystyle G^2(u)=2+4\frac{\int E(t)E(u+t)^3 + E(t)^3E(u+t)dt}{\int E(t)^4dt}+6\frac{\int E(t)^2E(u+t)^2 dt}{\int E(t)^4dt}$$

Immediately it can be seen that at long positive and negative delays, the correlation is going to be constant. This is because when the pulses are not overlapped, each pulse produces frequency doubled light which the detector measures.

The denominator integrates to a simple expression, but not the numerator, and therefore python/SymPy is used to calculate and plot $G^2$ directly.

The normalization term has a value of 1/8, but it is conventional to have the signal with a value of 1 in the wings of the pulse, because here frequency doubling from only one arm of the interferometer is measured, and the autocorrelation is multiplied by 8 to achieve this.

The algebraic solution of each integral is given below calculated using SymPy. Unless you are interested in how to do this calculation skip this part and go straight to the figure.

```python
# to calculate G^2(u)
omega,a,t,u = symbols('omega a t u',positive=True)

f01= cos(omega*t/a)*exp(-(t/a)**2/2)          # define electric fields
f02= cos(omega*(t+u)/a)*exp(-((t+u)/a)**2/2)
ef01   = expand(f01.rewrite(exp))             # change into exponential form to ease integration
ef02   = expand(f02.rewrite(exp))

# normalisation # int E(t)^4
ef01_4 = expand(ef01**4)
norml4 = integrate(ef01_4,(t,-oo,oo),conds='none')
factor(norml4)
```

```python
# integration E(t) x E(t+u)^3 and vice versa
ef13 = expand(ef01*ef02**3)
term13 = integrate(ef13,(t,-oo,oo),conds='none')
simplify(term13)
```

```python
# integration E(t)^2 x E(t+u)^2
ef02= expand(f02.rewrite(exp))
ef22= expand(ef01**2*ef02**2)
term22 = integrate(ef22,(t,-oo,oo),conds='none')
factor(term22)
```

The integrals can be used as they are or converted back into cosine form using $\displaystyle 2\cos(x)=e^{ix}+e^{-ix}$; thus

$$\begin{align}
\int E(t)^4dt &= \frac{a\sqrt{2\pi}}{16}\left(3+e^{-2\omega^2}+4e^{-\omega^2/2}\right) \\
\int E(t)E(u+t)^3dt &= \frac{a\sqrt{2\pi}}{32}\left[6\cos\left(\frac{\omega u}{a}\right)
+ e^{-2\omega^2} + 2e^{-\omega^2/2}\left(\cos\left(\frac{3\omega u}{2a}\right) +6\cos\left(\frac{\omega u}{2a}\right)\right) \right]e^{-3u^2/(8a^2)} \\
\int E(t)^2E(u+t)^2dt &= \frac{a\sqrt{2\pi}}{32}\left[4+2\cos\left(\frac{2\omega u}{a}\right) +2e^{-2\omega^2} +8\cos\left(\frac{\omega u}{a}\right)e^{-\omega^2/2} \right]e^{-u^2/(2a^2}
\end{align}$$

```python
a = 5
omega = 10
anorm = lambda a, omega: np.sqrt(2*np.pi)*a/16*\
                   ( 3 + np.exp(-2*omega**2) + 4*np.exp(-0.5*omega**2) )

f13   = lambda u,a,omega: np.sqrt(2*np.pi)*a/32*(3*2*np.cos(omega*u/a) + np.exp(-2*omega**2)\
                      + 2*np.exp(-omega**2/2)*( np.cos(3*omega*u/(2*a))\
                    + 3*2*np.cos(omega*u/(2*a) )))*np.exp(-3*u**2/(8*a**2))

f22   = lambda u,a,omega :np.sqrt(2*np.pi)*a/32*\
                    (4+ 2*np.cos(2*omega*u/a)+2*np.exp(-2*omega**2) \
                     +4*2*np.cos(omega*u/a)*np.exp(-omega**2/2) ) *np.exp(-u**2/(2*a**2))

n = anorm(a,omega)
G2 = lambda u, a, omega: 2   + 6*f22(u,a,omega)/n + 2*4*f13(u,a,omega)/n

fmax = lambda u,a: (1+3*np.exp(-u**2/(2*a**2))+4*np.exp(-3*u**2/(8*a**2))) # max and min values
fmin = lambda u,a: (1+3*np.exp(-u**2/(2*a**2))-4*np.exp(-3*u**2/(8*a**2)))

u = np.linspace(-20,20,500)     # define time values

plt.title(r'$G^2(u)$')
plt.plot( u, G2(u,a,omega)/2  )
plt.plot(u,fmax(u,a),color='grey',linewidth=1)
plt.plot(u,fmin(u,a),color='grey',linewidth=1)
plt.axhline(1,linewidth=1,color='grey')
plt.axhline(0,linewidth=1,color='grey')
plt.xlabel('time')
plt.show()
```

Figure 57. Normalized fringe resolved autocorrelation $G^2(u)$ with the upper and lower bounds shown as solid lines. The frequency of the pulse is constant throughout its duration so this is a _transform-limited_ pulse with zero chirp. The constants are $a$ = 5 and $\omega$ = 10.

**(b**) The outline pulse shape is found by ignoring the cosine term and integrating. The results is shown below. THe lower profile is found by subtracting the two exponentials, and has the effect only of changing the +4 term to -4.

```python
# outline pulse , ****   ignore cosine ****
t ,a,u =symbols('t a u',positive=True)
f011= ( exp(-(t/a)**2/2)  + exp(-((t+u)/a)**2/2) )**4     # define electric fields
s = integrate(f011,(t,-oo,oo),conds='none')
expand(s)
```

This equation can be normalised to one at long times and then simplified to give $\displaystyle \sqrt{2\pi}a(1+3e^{-u^2/(2a^2)}\pm 4e^{-3u^2/(8a^2)})$ where the positive sign corresponds to the upper curve. The fwhm of the curve $2\tau$, taking 1 as the baseline, is found by solving $\displaystyle (3e^{-\tau^2/(2a^2)}+ 4e^{-3\tau^2/(8a^2)})=7/2$. This equation is simplified by substituting $\displaystyle x=e^{-\tau^2/2a^2}$ giving $(3x+ 4x^3/4)=7/2$ which has one real solution which is 0.4428, and therefore $\displaystyle \tau = a\sqrt{-2\ln(0.4428)}=1.276a$. The original pulse has a fwhm of $\displaystyle a\sqrt{2\ln(2)}$, therefore the outline of the autocorrelation is $\approx$ 1.08 times wider than the pulse.

**(c)** The Fourier transform of the pulse is

$$\displaystyle g(k)= \frac{1}{\sqrt{2\pi}}\int\limits_{-\infty}^\infty \cos\left(\frac{\omega t}{a}\right)e^{-(t/a)^2/2}e^{-ikt}dt$$

and this is of a standard type of integral if the cosine is converted to its exponential form. Then $\displaystyle g(k)= \frac{1}{2\sqrt{2\pi}}\int\limits_{-\infty}^\infty (e^{i\omega t}+e^{-i\omega t})e^{-(t/a)^2/2-ikt}dt $.

The transform is thus $\displaystyle g(k)=\frac{a}{2}(e^{2ak\omega} +1)e^{-(ak+\omega)^2/2}$ and is shown in the next figure. The shape is dominated by the Gaussian shape of the final exponential.

```python
gk = lambda k,a, omega : 0.5*a*(np.exp(2.0*a*k*omega) +1 )*np.exp(-(a*k + omega )**2/2.0)
a = 5
omega = 10
k=np.linspace(0,4,500)
plt.plot(k,gk(k,a,omega),color='blue')
plt.xlabel('k')
plt.title('Fourier Transform of pulse')
plt.plot()
plt.show()
```

**Q17 answer**  **(a)** The integration for the $\displaystyle e^{-|x|/a}$ pulse is $\displaystyle \int_{-\infty}^\infty e^{-|x|/a}e^{-|x+u|/a}dx$ which is simplified, as the autocorrelation is symmetric about zero, by doubling the integral from 0 to $\infty$, in which case the absolute values are not needed. The result is $\displaystyle 2\int_0^\infty e^{-2x/a}e^{-u/a}dx = ae^{-u/a}$. However, when plotting the absolute values must be replaced as $u$ can be negative. Thus $\displaystyle A(u) = ae^{-|u|/a}$.

At the fwhm let $u=\tau$ and $\displaystyle A(u)=1/2= ae^{-\tau/a}$ thus $\tau=2a\ln(2a)$. The laser pulse fwhm is $2a\ln(2)$ the autocorrelation is thus $\ln(2a)/\ln(2)$ wider.

**(b)** The $\mathrm{sech}^2$ pulse can be simplified first by converting to its exponential form then integrated, and after inserting limits of $\pm\infty$, the normalised result is

$$\displaystyle A(u) =16\frac{e^{2u} [ u(e^{2u}+1) - (e^{2u}-1) ]}{(e^{2u}-1)^3}$$

The fwhm can be found by solving the autocorrelation for the value $u$ when $A(u)$=0.5. This is easily done to sufficient accuracy using the Newton - Raphson method outlined below and in chapter 3.1. The method is recursive, if x is the required solution then $x=x-f(x)/f'(x)$ is repeatedly calculated where $f(x)$ is the function and $f'(x)$ the first derivative wrt. $x$. To find the half width $A(u)$-1/2 is the function to find the root.

```python
x = symbols('x')
u = symbols('u', positive=True)  # this is necessary to get solution
eqn= (2/(exp(x)+exp(-x) ) )**2*(2/(exp(x+u)+exp(-x-u) ) )**2
Au = simplify(integrate(eqn,(x,-oo,oo )  )  )
Au
```

```python
df = simplify(diff(Au,u) )

factor(df)
```

```python
# Newton-Raphson
# derivative of A(u) is df
# simplified function and derivative used
df = lambda u: -16*np.exp(2*u)*(2*u*np.exp(4*u)+8*u*np.exp(2*u)+2*u-3*np.exp(4*u) +3)\
/(np.exp(2*u) -1 )**4

# function A(u)-1/2  and find value when zero (root).

f = lambda u:16*np.exp(2*u)*(u*np.exp(2*u)+u-np.exp(2*u)+1)/(np.exp(2*u)-1)**3 - 1/2

x = -0.5                 # initial guess
for i in range(6):       # 6 iterations are enough
    x = x - f(x)/df(x)
    print(x)
```

    -1.992610035168469
    -1.5791589526105754
    -1.6377830584176059
    -1.6389083234043145
    -1.6389087505542517
    -1.6389087505543132

which shows that the half width at half maximum (fwhm)  is 2$\cdot$ 1.64. In comparison the same value to the sech2 pulse is 2$\cdot$ 0.88 thus the autocorrelation is $\approx$ 1.86 times wider.

**Q18 answer** The acoustic pulse, $S(t)$ with $a$ = 0 compared with $a$ = 60,shows that the frequency is higher at short times than at longer ones; a down chirp. see fig 58. The calculated autocorrelation of the chirped and un - chirped pulse, are shown in Fig. 59. The calculation is essentially the same as in section 5.

Figure 58. Left. An acoustic pulse $S(t)$ but without a chirp, $a$ = 0. A chirped  pulse (right) mimicking that emitted by a bat when close to its prey.

_____

The un-chirped pulse $a$ = 0 produces a linearly decreasing autocorrelation but which is so long that poor range discrimination would be achieved and the bat would hardly ever succeed in catching its prey. The integral of this autocorrelation also increases slowly with time and hence distance, again indicating that the bat would find it difficult to discriminate the prey from something else. The chirped pulse has a small autocorrelation amplitude at long times, therefore, its summation is large and constant when the bat is far from the prey, but it decreases rapidly as it approaches within 3 cm of its target allowing sharp range discrimination. Figure 59 shows the summed autocorrelations together with the experimental data given in the question.

Figure 59. Left: Autocorrelations of bat pulses $S(t)$ with no chirp (grey dotted line) and with down chirp (solid green line). Right: The sum of the autocorrelation with time, converted into distance, for the same two pulses together with the experimental data which is the percentage correct response vs difference in distance to the target. (Data was measured from Simmons 1971, Fig. 2.)

____

The summation in figure 59 was calculated as follows where $A_0$ is the autocorrelation.

S0 = [ 0.0 for i in range( numt) ]

for i in range( numt ): S0[i] = sum( [ abs( A0[ k ] ) for k in range( i ) ] )

**Q 19 answer** The method to use similar to that used in the example. However, when the raw data is plotted the signal is buried in the noise, Fourier transforming produces an ambiguous result where it is not clear where to set the filter to extract the data. Apodising, by multiplying the data by an exponential decreases the noise in the longer part of the data and helps to identify the frequencies present. The initial FID and its Fourier transform is shown in the figure.

fig 60 Left: ideal FID of two spins. Right: close-up of the spectrum (imag part of transform) showing two lines, one at each frequency.

_____

The code with which to calculate the FID and FFT is shown below

```python
#.. make FID add noise and make FFT

n = 2**12
maxx = n/2
x =  np.linspace(0,maxx,n)    # make n points and of length maxx
tp = maxx/n
freq = [ i/(tp*n) for i in range(n)]
nu1 = 1/4.8
nu2 = 1/4.5
sig = 2.0  # magnitude of noise use randn() to use normally distributed noise

fid0 = [ np.exp(-x[i]/300.0)*(np.sin(2*np.pi*nu1*x[i]) + np.sin(2*np.pi*nu2*x[i]) ) \
         + sig*np.random.randn() for i in range(n)]

fft0 = np.fft.rfft(fid0).imag    # as FID contais sine waves FFT is imaginary.
#.. plots shwon in the next figures
```

Figure 61. Noise added to the FID and then transformed to give a noisy spectrum of which only the imag part is shown. It is clear from the FFT that it is hard to determine which peaks are due to the data and which due to noise.

__

The figure shows that data is clearly swamped by noise. The next step is to apodise by multiplying by an exponential. This decreases the noise relative to the signal as this is largest at earlier times. The decay time can be chosen by trial and error  to best illustrate the features wanted.

#.. define a new list to hold the apodised FID

fid1 = [0 for i in range( n )]          # make new list to hold new FID
for i in range(n):
 >   fid1[ i ] = fid0[i]*np.exp(-i/1000)

fft1 = np.fft.rfft( fid1 ).imag    # calculate FFT

#.... plot data as above

Figure 62. The apodised FID (left) is transformed into the spectrum thereby retrieving the two frequencies. The original frequencies, although not perfectly isolated,  are at approx 0.2 and can clearly be identified from the noise.

____

**Q 20 answer** Plotting the data with and without noise, shows that the pulse lasts for about 2 ps, and examining this close to the maximum time, indicates that the smallest period is about 60 fs. Therefore, $n$ = 212 points will be more than adequate for the Fourier transforms.

The pulse has the form $\displaystyle \sin(x^2/200^2 )\exp(- (x-800.0)^2/200.0^2)$ and the noise, with an amplitude from $\pm$1, is added by including randn(). The plot shows the chirped pulse (pure pulse), and with the noise added, this forms the 'experimental' data. The pure pulse and the recovered data are shown in the second set of figures below.

```python
n = 2**12
maxt = 1500
t  = np.linspace(0,maxt,n)
tp = maxt/n
# make data with and without noise
pnoise = [ np.sin((t[i]/200)**2 )*np.exp(-((t[i]-800.0)/200.0)**2)+np.random.randn() for i in range(n) ]
pulse =  [ np.sin((t[i]/200)**2 )*np.exp(-((t[i]-800.0)/200.0)**2) for i in range(n) ]

freq= [ i/(tp*n) for i in range(n)]
fig1= plt.figure(figsize=(10.0,5.0))
ax0 = fig1.add_subplot(1,2,1)
ax1 = fig1.add_subplot(1,2,2)

ax0.plot(t,pnoise,color='gray')
ax0.plot(t,pulse,color='red',linewidth=2)
ax0.set_xlabel('time /fs')
ax0.set_title('noisy & pure signal')

fft0= np.fft.rfft(pnoise).real

ax1.plot(freq[0:n//2],fft0[0:n//2],marker='o',color='red')
ax1.set_xlim([0,0.04])
ax1.set_title('FFT')
ax1.set_xlabel('frequency')
plt.tight_layout()
plt.show()
```

```python
# from FFT plot choose frequencies to include in reverse transform
filter = [ fft0[i]  if (i/n >0.0 and i/n <0.02)   else 0 for i in range(n//2)]

fft1 = np.fft.irfft(filter)

plt.plot(t,pulse,color='red',linewidth=3,linestyle='dotted')
plt.plot(t[0:n-2],fft1[0:n],color='gray',linewidth=2)
plt.xlabel('time /fs')
plt.title('pure (green dotted) and recovered signal')
plt.show()
```

**Q21 answer** The recursive algorithm below is based on the equation in the text. The data is assumed to have been calculated elsewhere and put into array called data, the smoothed data is called sdata.

```python
# make some noisy data. The window has width m=3. This code generates the data shown in the text.
fig1= plt.figure(figsize=(5.0,5.0))
n = 2**9
noise = [ 0 for i in range(n)]

for i in [100,170,210,305,355,390,410]:
    noise[i] = np.random.rand() + 0.5

data = [ noise[i]*0.75 + np.exp(-(i-250)**2/1e4)*0.5 + 0.3*np.random.rand()-0.3/2 for i in range(n)]

x    = [i   for i in range(n)]
sdata= [0.0 for i in range(n)]
m = 3
sdata[m]= sum(data[i] for i in range(0,2*m+1))/(2*m+1)
for i in range(1,n-m):
    sdata[i] = sdata[i-1] + (data[i+m]-data[i-m-1])/(2*m+1)
plt.plot(x,data,color='blue')
plt.plot(x,sdata,color='red',linewidth=3)
plt.show()
```

```python

```


# Variational Monte Carlo

The main ingredient is a trial wave function $|\psi _{T}(\overline{\eta }
)\rangle $, that depends on a set of parameters $\overline{\eta }$. This
wave function is represented in terms of a basis of orthogonal states $
|x\rangle$
$$
|\psi _{T}(\overline{\eta })\rangle =\sum_{x}\langle x|\psi _{T}(\overline{
\eta })\rangle |x\rangle =\sum_{x}C_{x}(\overline{\eta })|x\rangle ,
$$
where the coefficients of the parametrization are known functions of $%
\overline{\eta }$. We would like the wave function to be a good
representation of the actual ground state of a model. Finding the best wave
function means finding the right set of parameters $\overline{\eta }$ that
maximize the overlap with the actual ground state. In practice this is
impossible since we do not know the groud state \textit{a priori}, and
some physical insight is needed to derive a good analytical
approximation. Then we apply the variational principle, that states
that the variational energy of the trial state is always greater or equal to
the exact energy of the ground state:
\begin{equation}
\langle E\rangle _{T}=\frac{\langle \psi _{T}\mid H\mid \psi _{T}\rangle }{%
\langle \psi _{T}\mid \psi _{T}\rangle }\geq E_{0},
\label{energia variacional}
\end{equation}
and we use the criterium of minimizing the variational energy. In order to
do that we require to calculate this quantity for different sets of
parameters $\overline{\eta }$, and once we found a proper wave function, we
can calculate the physical quantities of interest.

The expectation value of an arbitrary operator $O$ is
\begin{eqnarray}
\langle O\rangle _{Var} &=&\frac{\langle \psi _{T}\mid O\mid \psi
_{T}\rangle }{\langle \psi _{T}\mid \psi _{T}\rangle }  \nonumber \\
&=&\frac{\sum_{x}\langle \psi _{T}\mid x\rangle \langle x\mid O\mid \psi
_{T}\rangle }{\sum_{x}\langle \psi _{T}\mid x\rangle \langle x\mid \psi
_{T}\rangle } \\
&=&\frac{\sum_{x}\left| \langle \psi _{T}\mid x\rangle \right|
^{2}\sum_{x^{\prime }}\langle x\mid O\mid \psi _{T}\rangle /\langle x\mid
\psi _{T}\rangle }{\sum_{x}\left| \langle \psi _{T}\mid x\rangle \right| ^{2}%
}  \nonumber \\
&=&\sum_{x}P_{x}O_{x},  \label{valor medio}
\end{eqnarray}
with
\begin{equation}
P_{x}=\frac{\left| \langle \psi _{T}\mid x\rangle \right| ^{2}}{%
\sum_{x}\left| \langle \psi _{T}\mid x\rangle \right| ^{2}},  \label{P}
\end{equation}
and
\begin{equation}
O_{x}=\frac{\langle x\mid O\mid \psi _{T}\rangle }{\langle x\mid \psi
_{T}\rangle }.  \label{o}
\end{equation}
The equation (\ref{valor medio}) has precisely the form of a mean value in
statistical mechanics, with $P_{x}$ as the Boltzmann factor:
$$
P_{x}\geq 0;\,\sum_{x}P_{x}=1.
$$
The first step in order to calculate it consists of generating a collection
of configurations distributed according to this probability. For that
purpose we employ the Metropolis algorithm: starting from a configuration $
|x\rangle $, we accept a new configuration $|x^{\prime }\rangle $ with
probability $R=\left| \langle \psi _{T}\mid x^{\prime }\rangle \right|
^{2}/\left| \langle \psi _{T}\mid x\rangle \right| ^{2}$, or $1/(1+R)$ if we
use the heat bath approach.

The variational simulations are simple to perform and very stable.
Since the probabilities do not deppend on the statistics of the particles
involved, they do not suffer from the sign problem. However,
the results deppend decisively of the
quality of the variational wave function, because they are completely
pre-determined by it, and the physical arguments that define it. In the
particular situation in which the trial function coincides with the exact
ground state, the matrix elements (\ref{o}) for $O=H$ are all equal to $%
E_{0}:$%
$$
E_{x}=\frac{\langle x\mid H\mid \psi _{T}\rangle }{\langle x\mid \psi
_{T}\rangle }=E_{0}.
$$
This is the property called "zero variance": the more the wave function
resembles the actual ground state, the more rapidly the variational enery
converges with the number of iterations.

In general, the computation results more complicated, or numerically more
expensive, when the number of \ variational parameters $\overline{\eta }$
that define the trial state is large. Thus, we always try to keep the form
of the wave function simple enough and with only a few variational degrees
of freedom.

## Quantum Spin Chain

We seek a good representaiton of the ground state wave function for $L$ spins interacting via a Heisenberg Hamiltonian:
$${H}=\sum_{i=1}^{N-1}
{S}^z_i {S}^z_{i+1} -
\frac{1}{2}\left[
{S}^+_i {S}^-_{i+1} +
{S}^-_i {S}^+_{i+1}
\right]
$$
Notice the minus sign in the off diagonal term. This results from a unitary transformation called "Marshall rotation". As a consequence, all coefficients in the wave-function are positive and real, and we don't have to worry about complex phases nor signs.
As we learned in previous lectures, we can only solve the problem extraclty for a small number of spins (in reality, there is an exact analytical solution called "Bethe Ansatz"). We will now learn how to solve the problem using a variational approximation to the ground state. Leaving aside the justification for our choice of wave function, we introduce the so-called Jastrow wave-function:
$$
|\Psi\rangle = \sum_{\bar{\sigma}} \exp\left({\alpha\sum_{i>j}\frac{S_i^zS_j^z}{|i-j|}}\right)|\bar{\sigma}\rangle,
$$

where $\sigma=\pm 1/2$ represents the orientation of the spins along the $z$ axis. This expression enjoys an elegant simplicity, and depends on only one free variational parameter $\alpha$. We start our simulation by chosing a pair of spins; if anti-parallel, we atempt a spin flip. The acceptance probability is determined by the ratio $|\Psi(\bar{\sigma}')|^2/|\Psi(\bar{\sigma})|^2$, where $\Psi(\bar{\sigma})=\exp\left({\alpha\sum_{i>j}\frac{\sigma_i \sigma_j}{|i-j|}}\right)$. Notice that this probability reminds us of a Boltzmann factor with inverse temperature $\alpha$ and classical Ising-like Hamiltonian $H'=\sum_{i>j}\frac{S_i^zS_j^z}{|i-j|}$. Therefore, in order to implement the code, we only have to introduce minor modifications to our previous Monte Carlo code for the Ising model. There are some important changes: (i) Since we are only inetrested in the sector with zero total spin $S^z=0$, we pick spins in pairs and we attempt spin flips that preserve the total magnetization; (ii) The interactions in the effective classical Hamiltonian are long-range, (iii) more importantly, the expression for the energy involves the actual quantum Hamiltonian and we need to account for all possible spin flips to evaluate the expression for the "local energy":
$$E_\bar{\sigma} = \frac{\langle \bar{\sigma}|H|\Psi\rangle}{\langle \bar{\sigma}|\Psi\rangle}=\sum_i\frac{\langle \bar{\sigma}|S^z_iS^z_{i+1}|\Psi\rangle}{\langle \bar{\sigma}|\Psi\rangle}-\frac{1}{2}\sum_i\frac{\langle \bar{\sigma}|S^+_iS^-_{i+1}+S^-_iS^+_{i+1}|\Psi\rangle}{\langle \bar{\sigma}|\Psi\rangle}$$
The last term involves all the allowed spin flips between nearest neighbors, and has at most $L$ non-zero contributions.

```python
%matplotlib inline
import numpy as np
from matplotlib import pyplot
import matplotlib.animation as animation

class Direction:
    RIGHT, LEFT = range(2)

def distance(r,L):
    return abs(abs(r)-L*int(2*abs(r)/L))

class VMC(object):

    def __init__ (self, L, alpha):

        self.L = L
        self.alpha = alpha
        # Table for probabilities
        self.psi2 = np.zeros(shape=(3,int(L/2+1)))

        # Initialize neighbors table for boundary conditions
        self.nn = np.zeros(shape=(self.L,2), dtype=np.int16)

        # Periodic boundary conditions
        for ix in range(L):
            self.nn[ix,Direction.LEFT] = ix-1
            self.nn[ix,Direction.RIGHT] = ix+1

        self.nn[0,Direction.LEFT] = L-1
        self.nn[L-1,Direction.RIGHT] = 0

        # Initialize spins
        self.spin = np.ones(self.L, dtype=np.int16)
        pos = np.random.choice(range(L), int(L/2), replace=False) # we pick L/2 unique random positions and flip
        for ix in pos:
            self.spin[pos] = -1

        self.set_alpha(alpha)

    def set_alpha(self, alpha):
        self.alpha = alpha
        # Lookup tables for transition probabilities
        # Pair-wise contribution to the argument in the exponential
        for i in range(-1,2,2):
            for l in range(1,int(self.L/2)+1):
                self.psi2[i+1,l] = self.alpha*i*0.25/l

    def metropolis(self):
        nchanges = 0

        # trial spin change
        # pick a random pair of spins
        i = 0
        j = 0
        while(self.spin[i] == self.spin[j]):
            i = int(np.random.random()*self.L)
            j = int(np.random.random()*self.L)

        si = self.spin[i]
        sj = self.spin[j]

        arg = 0.
        for k in range(self.L):
            sk = self.spin[k]
            if(i != k and k != j):
                arg += self.psi2[si*sk+1,distance(i-k,self.L)]
                arg += self.psi2[sj*sk+1,distance(j-k,self.L)]
        p = np.exp(-4*arg)
        if(p >= 1 or np.random.random() < p):
            self.spin[i] *= -1
            self.spin[j] *= -1
            nchanges += 1
        return nchanges

    def Psi(self, spins):
        arg = 0.
        for i in range(self.L):
            for j in range(i+1,self.L):
                si = spins[i]
                sj = spins[j]
                arg += self.psi2[si*sj+1,distance(i-j,self.L)]
        return np.exp(arg)

    def Eloc(self):
        Ediag = 0.
        Eoff = 0.
        for i in range(self.L):
            j = self.nn[i,Direction.RIGHT]
            si = self.spin[i]
            sj = self.spin[j]

            # Diagonal contribution
            Ediag += si*sj*0.25
            # Off diagonal contribution
            arg_flip = 0.
            if(si != sj):
                for k in range(self.L):
                    sk = self.spin[k]
                    if(i != k and k != j):
                        arg_flip += self.psi2[si*sk+1,distance(i-k,self.L)]
                        arg_flip += self.psi2[sj*sk+1,distance(k-j,self.L)]
            Eoff -= 0.5*np.exp(-2*arg_flip)
        return Ediag+Eoff

    def Eloc2(self):
        Ediag = 0.
        Eoff = 0.
        for i in range(self.L):
            j = self.nn[i,Direction.RIGHT]
            si = self.spin[i]
            sj = self.spin[j]

            # Diagonal contribution
            Ediag += si*sj*0.25
            # Off diagonal contribution
            arg_flip = 0.
            if(si != sj):
                aux_spin = self.spin
                aux_spin[i] *= -1
                aux_spin[j] *= -1
                Eoff -= 0.5*self.Psi(aux_spin)/self.Psi(self.spin)
        return Ediag+Eoff

```

```python
system = VMC(8,-2)
for i in range(10):
    system.metropolis()
print(system.spin)
```

    [ 1 -1 -1  1  1 -1 -1  1]

```python
print(system.Eloc(),system.Eloc2())
```

    -5.297442541400256 -3.0

```python
%matplotlib inline
from matplotlib import pyplot

alphas=np.linspace(-6,6,20)
e0 = []
for a in alphas:
    Etot = 0.
    Nmeas = 0
    system.set_alpha(a)
    for i in range(50000):
        system.metropolis()
        if(i%10 == 0):
            Etot += system.Eloc2()
            Nmeas += 1
    print(a,Etot/Nmeas)
    e0.append(Etot/Nmeas)

```

    -6.0 -2.8216
    -5.368421052631579 -2.827
    -4.7368421052631575 -2.8336
    -4.105263157894737 -2.821
    -3.473684210526316 -2.8076
    -2.8421052631578947 -2.8042
    -2.210526315789474 -2.7828
    -1.578947368421053 -2.7382
    -0.9473684210526319 -2.688
    -0.3157894736842106 -2.614
    0.3157894736842106 -2.5174
    0.947368421052631 -2.4174
    1.5789473684210522 -2.3366
    2.2105263157894726 -2.293
    2.842105263157894 -2.2764
    3.473684210526315 -2.2778
    4.105263157894736 -2.2846
    4.7368421052631575 -2.2688
    5.368421052631579 -2.267
    6.0 -2.2746

```python
pyplot.xlabel("alpha")
pyplot.ylabel("Energy")
pyplot.plot(alphas,e0,'bo-',lw=3);

```

```python

```


 # $$\LARGE\text{Model Project}$$

## The Cournot model of oligopoly

In a Cournot model of oligopoly firms compete in quantity of homogenous products in a market with no collusion. The model describes the rational behaviour of the individual firm in the decision of output given the rivals decision of output.

This project examines the case with two competing symmetric firms with complete information. Both firms choose output simultaneously.

### 1. Firms

Firm $i$ choose to produce quantity $q_i$ of a homogeneous product in a market of two firms, with both firms having the same constant marginal cost at $c$. The inverse demand function for firm $i$ is assumed to be $p_i(q_i,q_j)=a-q_i-q_j$, where $c<a$. The profit function becomes

$$\pi_i(q_i,q_j) = p_i(q_i,q_j)q_i-q_i  c$$

The firms' best response functions $(BR_i)$ are calculated by maximizing the profit function.

$$\underset{q_i}{\textbf{max}} \hspace{0.2cm} \pi_i(qi,qj) $$

This results in
$$BR_i(q_j) = \frac{a-c-q_j}{2}$$

Each firms best response is optimal given the best response chosen by the rival - with no incentive to deviate.
If the quantity chosen by each firm is to be a Nash equilibrium it must satisfy the bestresponse functions. Given symmetry the Nash equilibrium output is

$$q_i^* = \frac{a-c}{3}$$

```python
#1.a import modules
import numpy as np
import sympy as sm
from numpy import array
from scipy import linalg
from scipy import optimize
import matplotlib.pyplot as plt
```

### 2. Define model

```python
#2.a Define symbols
a = sm.symbols('a')
c = sm.symbols('c')
q1 = sm.symbols('q_1')
q2 = sm.symbols('q_2')
pi_1 = sm.symbols('\pi_1')
pi_2 = sm.symbols('\pi_2')

# 2.b Inverse demand function
def inverse_demand(q1,q2,a):
    inverse = a - q1 - q2
    return inverse

# 2.c cost functions
def cost(q,c):
    cost_1 = q * c
    return cost_1

# 2.d Profit functions
def profit_firm1(q1,q2,a,c):
    pi_1 = (inverse_demand(q1,q2,a)) * q1 - cost(q1,c)
    return pi_1
def profit_firm2(q1,q2,a,c):
    pi_2 = inverse_demand(q1,q2,a) * q2 - cost(q2,c)
    return pi_2
```

### 3. How to solve the model

$\underline{\text{Solve in three steps:}}$

1. **maximize** profit function wrt. $q_i$ (find FOCs) and set to zero
2. **isolate** $q_i$ from the first order conditions (find BR functions)
3. **substitute** $q_j$ into $q_i$ (find Nash equilibrium)

#### 3.1 First order conditions
The first order conditions is listed below, where FOC1 denotes the solution for firm 1, and FOC2 for firm 2.

```python
FOC_1 = sm.diff(profit_firm1(q1,q2,a,c),q1) #derivative wrt. q1
FOC_2 = sm.diff(profit_firm2(q1,q2,a,c),q2)
print(f'FOC1: {FOC_1} = 0 \nFOC2: {FOC_2} = 0\n')
```

    FOC1: a - c - 2*q_1 - q_2 = 0
    FOC2: a - c - q_1 - 2*q_2 = 0

#### 3.2 Best response functions

The best response function for firm 1 is derived by isolating $q_1$ in FOC1 and
same for firm 2 in FOC2.

```python
BR_1_sympy = sm.solve(FOC_1,q1) #solve for q1
BR_2_sympy = sm.solve(FOC_2,q2)
print(f'BR_1(q2) = q1 = {BR_1_sympy} \nBR_2(q1) = q2 = {BR_2_sympy}\n')
```

    BR_1(q2) = q1 = [a/2 - c/2 - q_2/2]
    BR_2(q1) = q2 = [a/2 - c/2 - q_1/2]

#### 3.3 Nash Equilibrium

The Nash Equilibrium can be solved for by substituting $q_1$ into $q_2$ which yields

```python
BR1_sympy = a/2-c/2-q2/2
BR2_sympy = a/2-c/2-q1/2 - q2
SUB = BR2_sympy.subs(q1,BR1_sympy)
NE_q = sm.solve(SUB,q2)
quan = (a-c)/3
SUB2 = inverse_demand(q1,q2,a).subs(q1,quan)
NE_p = SUB2.subs(q2,quan)
print(f'q^*_i = {NE_q}')
print(f'p^*  = {NE_p}')
```

    q^*_i = [a/3 - c/3]
    p^*  = a/3 + 2*c/3

Which shows the Nash Equilibrium quantity, $q_i^*$, and the equilibrium price $p^*$, where $c1=c2$.

### 4. Example of how to solve the problem

In this section I derive the best response function for each firm by maximizing their respective profit function. The best response functions are then used to find the Nash Equilibrium, i.e. the optimal choice of quantity given the other firms' choice of quantity. This procedure is described in section 3.

#### 4.1 Model analysis

```python
#4.a the optimization setup does not always work without running the code below.
#del a, c1, c2, q1, q2, pi_1, pi_2

#4.b Define best response functions
q0=0
def BR_1_analysis(q2,a,c):
    opt_q1 = optimize.minimize(lambda q0: -profit_firm1(q0,q2,a,c), q2).x[0]
    return opt_q1

def BR_2_analysis(q1,a,c):
    opt_q2 = optimize.minimize(lambda q0: -profit_firm2(q1,q0,a,c), q1).x[0]
    return opt_q2

def conditions_analysis(q,param):
    u = q[0] - BR_1_analysis(q[1],param[0],param[1])
    y = q[1] - BR_2_analysis(q[0],param[0],param[1])
    return [u,y]

#4.c Define parametervalues [a,c]
param = [11,2]

#4.d Solve optimization problem
initial_values = [1,1]
opt_q_i = optimize.fsolve(conditions_analysis,initial_values, args = (param))
opt_p = param[0]/3 + 2*param[1]/3
profit_i = opt_p * opt_q_i - param[1] * opt_q_i

print(f'With parameter values of [a,c] = {param}, the Nash equilibrium quantities is (q1,q2) = {opt_q_i}.\nThis gives an equilibrium price of p = {opt_p}.')
print(f'Both companies achieve same profit of (pi_1, pi_2) = {profit_i}.\n')
```

    With parameter values of [a,c] = [11, 2], the Nash equilibrium quantities is (q1,q2) = [2.99999999 2.99999999].
    This gives an equilibrium price of p = 5.0.
    Both companies achieve same profit of (pi_1, pi_2) = [8.99999998 8.99999998].

#### 4.2 Graphical analysis of best response functions

```python
#4.e Define reactions functions
def q1_graphic(q2,param): #parameters can be changed in section 4, code 4.c
    return (param[0]-param[1]-q2)/2

def q2_graphic(q1,param):
    return (param[0]-param[1]-q1)/2

#4.f Create best response to multiple q values
range_q1 = np.linspace(-10,20,100)
range_q2 = np.linspace(-10,20,100)
q1_plot = q1_graphic(range_q2,param)
q2_plot = q2_graphic(range_q1,param)

#4.g Plot best response functions
fig = plt.figure(figsize=(12,5))
plt.style.use('fivethirtyeight')
ax_fig1 = fig.add_subplot(1,2,1)
plt.plot(q1_plot,range_q2, label = '$BR_1(q_2)$')
plt.plot(range_q1,q2_plot, label = '$BR_2(q_1)$')
plt.title('Figure 1 - Bestresponse functions', fontsize=15)
plt.plot(opt_q_i,opt_q_i,'ko')
plt.xlabel('Quantity produced by firm 1')
plt.ylabel('Quantity produced by firm 2')

ax_fig1.set_xlim(0,10)
ax_fig1.set_ylim(0,10)

ax_fig1.grid(True)
plt.legend(loc="best")
print("Note: the Figure is misleading with the negative quantities on the axis'; negative quantities are not feasible.\nI have not find a way to solve this problem yet.")
```

The two best response functions in Figure 1 intersect at the black dot and represents the Nash Equilibrium quantities found in section 4.1. Both functions decreases in the rival firm's quantity; the intuition is that whenever one firm increase output it is optimal for the rival to decrease quantity.

#### 4.3 How does increasing marginal cost affect the equilibrium?

```python
#4.h Calculate equilibrium at different costs
c_vec = np.linspace(1,10,10)

q_vec = []
for c in c_vec:
    q = (param[0]-c)/3

    q_vec.append(q)

    pi = (param[0]-q-q-c)*q
    p = param[0]/3 + 2*c/3
    print(f'a = {param[0]}, c = {c} --> q = {q:.2f}, p = {p:.2f} --> Profit = {pi:.2f}')

#4.i Code does not work (want to plot marginal cost against equilibrium quantity)
#plt.plot(c_vec,q)
#plt.title('Figure 2 - Equilibrium quantity given c', fontsize=15)
#plt.xlabel('Marginal cost')
#plt.ylabel('Equilibrium quantity')
#ax_fig1.grid(True)
```

    a = 11, c = 1.0 --> q = 3.33, p = 4.33 --> Profit = 11.11
    a = 11, c = 2.0 --> q = 3.00, p = 5.00 --> Profit = 9.00
    a = 11, c = 3.0 --> q = 2.67, p = 5.67 --> Profit = 7.11
    a = 11, c = 4.0 --> q = 2.33, p = 6.33 --> Profit = 5.44
    a = 11, c = 5.0 --> q = 2.00, p = 7.00 --> Profit = 4.00
    a = 11, c = 6.0 --> q = 1.67, p = 7.67 --> Profit = 2.78
    a = 11, c = 7.0 --> q = 1.33, p = 8.33 --> Profit = 1.78
    a = 11, c = 8.0 --> q = 1.00, p = 9.00 --> Profit = 1.00
    a = 11, c = 9.0 --> q = 0.67, p = 9.67 --> Profit = 0.44
    a = 11, c = 10.0 --> q = 0.33, p = 10.33 --> Profit = 0.11

```python
#q = [(param[0]-c)/3 for c in c_vec]
q_vec = (param[0]-c_vec)/3
plt.plot(c_vec,q_vec)

plt.title('Figure 2 - Equilibrium quantity given c', fontsize=15)
plt.xlabel('Marginal cost')
plt.ylabel('Equilibrium quantity')
ax_fig1.grid(True)
```

This output shows that increasing marginal costs decreases amount of quantity produced in equilibrium per firm. The intuition is that increasing marginal costs makes it more expensive for the firms to produce goods, which leads them to increase prices by lowering amount of goods produced.

### 5. What happens if more firms enters the market?

I will now introduce a third firm. Each firms profit is now
$$\pi_i(q_i,q_j,q_k) = p_i(q_i,q_j,q_k)q_i-q_i  c, \hspace{0.2cm}\\ p_i=a-q_1-q_2-q_3$$

The firms' best response functions $(BR_i)$ are calculated by maximizing the profit function.

$$\underset{q_i}{\textbf{max}} \hspace{0.2cm} \pi_i(qi,qj,q_k) $$

This results in
$$BR_i(q_j,q_k) = \frac{a-c-q_j-q_k}{2}$$

The Nash equilibrium quantity becomes
$$q_i^* = \frac{a-c}{4}$$

The parametervalues for $a$ and $c$ are defined in codeline 4.c. Results will be compared with results from sections 4.1 in order to conclude what affect a third firm has on the market.

$\underline{\text{Solve in three steps:}}$
1. **maximize** profit function wrt. $q_i$ (find FOCs)
2. **isolate** $q_i$ from the first order conditions (find BR functions)
3. **substitute** $q_j$ and $q_k$ into $q_i$ (find Nash equilibrium)

```python
#5.a define inverse demand function
def inverse_morefirms(q1,q2,q3,a):
    demand = a - q1 - q2 - q3
    return demand

#5.b define profit functions
def profit_firm1_morefirms(q1,q2,q3,a,c):
    return inverse_morefirms(q1,q2,q3,a) * q1 - cost(q1,c)
def profit_firm2_morefirms(q1,q2,q3,a,c):
    return inverse_morefirms(q1,q2,q3,a) * q2 - cost(q2,c)
def profit_firm3_morefirms(q1,q2,q3,a,c):
    return inverse_morefirms(q1,q2,q3,a) * q3 - cost(q3,c)

#5.c define bestresponse functions by maximizing profits
def BR1_morefirms(q2,q3,a,c):
    optimal_q1 = optimize.minimize(lambda q0: -profit_firm1_morefirms(q0,q2,q3,a,c), q0).x[0]

    return optimal_q1
def BR2_morefirms(q1,q3,a,c):
    optimal_q2 = optimize.minimize(lambda q0: -profit_firm2_morefirms(q1,q0,q3,a,c), q0).x[0]
    return optimal_q2
def BR3_morefirms(q1,q2,a,c):
    optimal_q3 = optimize.minimize(lambda q0: -profit_firm3_morefirms(q1,q2,q0,a,c), q0).x[0]
    return optimal_q3

#5.d conditions
def conditions_morefirms(q,param):
    u = q[0] - BR1_morefirms(q[1],q[2],param[0],param[1])
    y = q[1] - BR2_morefirms(q[0],q[2],param[0],param[1])
    z = q[2] - BR3_morefirms(q[0],q[1],param[0],param[1])
    return [u,y,z]

param  = [20,3]
#5.e solution and print
inital_values = [1,1,1]
opt_qi_morefirms = optimize.fsolve(conditions_morefirms,inital_values, args = (param))
opt_p_morefirms = param[0] - 3/4 * (param[0]-param[1])
profit_i_morefirms = opt_p_morefirms * opt_qi_morefirms - param[1] * opt_qi_morefirms

print(f'With parameter values of [a,c] = {param}, the Nash equilibrium quantities is (q1,q2,q3) = {opt_qi_morefirms}.\nThis gives an equilibrium price of p = {opt_p_morefirms}.')
print(f'Both companies achieve same profit of (pi_1, pi_2) = {profit_i_morefirms}.\n')
```

    With parameter values of [a,c] = [20, 3], the Nash equilibrium quantities is (q1,q2,q3) = [4.24999929 4.25000079 4.24999975].
    This gives an equilibrium price of p = 7.25.
    Both companies achieve same profit of (pi_1, pi_2) = [18.062497   18.06250335 18.06249895].

```python
def inverse_nfirms(a,q_vec):
    demand = a - np.sum(q_vec)
    return demand

def profit_firm_i(a,c,q_vec,i,q_i):
    # update q_i
    q_vec[i]=q_i

    return inverse_nfirms(a,q_vec) * q_i - cost(q_i,c)

def BR_nfirms(a,c,q_vec,q_i,i):
    optimal_q_i = optimize.minimize(lambda q0: -profit_firm_i(a,c,q_vec,i,q0),x0=np.mean(q_vec)).x[0]

    return optimal_q_i

def conditions_nfirms(q_vec,param):
    conds = [q_i-BR_nfirms(param[0],param[1],q_vec,q_i,i) for i,q_i in enumerate(q_vec)]

    return conds

def solve_model(param,n):
    q0_vec = [1 for i in range(n)] # n firms
    res = optimize.root(conditions_nfirms,q0_vec, args = (param))
    opt_qi_nfirms = res.x

    opt_p_nfirms = param[0]-np.sum(opt_qi_nfirms)

    return opt_qi_nfirms,opt_p_nfirms
```

```python
n_vec = range(2,20)
p_vec = [solve_model(param,n)[1] for n in n_vec]
```

```python
plt.plot(n_vec,p_vec)

plt.title('Equilibrium price given n', fontsize=15)
plt.xlabel('price given n firms')
plt.ylabel('Equilibrium price')
ax_fig1.grid(True)
```

```python
def conditions_nfirms_fast(q_i,param,n):

    # Assume all firms react the same
    q_vec = [q_i[0] for i in range(n)]
    cond = q_i[0]-BR_nfirms(param[0],param[1],q_vec,q_i,0)

    return cond

def solve_model_fast(param,n):
    #q0_vec = [1 for i in range(n)] # n firms
    q0 = [1]
    res = optimize.root(conditions_nfirms_fast,q0, args = (param,n))
    opt_qi_nfirms = res.x[0]

    opt_p_nfirms = param[0]-opt_qi_nfirms*n

    return opt_qi_nfirms,opt_p_nfirms
```

```python
n_vec = range(2,50)
p_vec = [solve_model(param,n)[1] for n in n_vec]
```

```python
plt.plot(n_vec,p_vec)

plt.title('Equilibrium price given n', fontsize=15)
plt.xlabel('price given n firms')
plt.ylabel('Equilibrium price')
ax_fig1.grid(True)
```

These results indicates a decreasing price and quantity produced by each firm when a third firm enter the market compared to the case with two firms. The intuition is that as firms increase in the market, power of each firm becomes smaller and equilibrium values come closer to the perfect competition outcome; market price equals marginal cost of production and zero profits.

### 6. Conslusion

In a Cournot model of duopoly firms choice of production depends on the rivals production as illustrated in Figure 1. I found that the equilibrium market price is higher than the marginal cost of production, which means that firms do not price and produce as in perfect competition situation. In section 4.3 I found that increasing marginal cost of production leads the market into af perfect competition situation since profit converges towards zero as the marginal cost increases.
Adding a third firm to the market resulted in decreasing price and quantity, i.e. the existing firms were worse off.


\begin{equation}
x_{n+1} = r(x_n)(1-x_n)
\end{equation}

```python
%matplotlib inline
# see https://youtu.be/ovJcsL7vyrk?t=322 for more context

# The following program carries out the iterative calculation stated above,
# sweeping across different values of r.
# n == number of iterations (x-axis)
from logistics_bifurcation import *
for r in reversed_logspace(2.9,3.84,num=200):
    fig, ax = plt.subplots()
    scat = ax.scatter(np.arange(NUM_ITER), get_series(r), marker="+")
    ax.set_title(f"{r=}")
    ax.set_xlabel("number of iterations")
    if np.isclose(r, 3.83, atol=1E-2):
        ax.set_title(rf"{r=},$\approx$ period 3!")
    plt.show(block=False)
    plt.close(fig)
```


```python
import sympy as sp
```

```python
val = 5
x,y = sp.symbols('x y')
```

```python
eq1 = sp.Eq(5*x + 5*y -7, 0)
eq2 = sp.Eq(6*x - 6*y - 9, 0)
display(sp.solve(eq1,x))
display(sp.solve(eq2,y))
```

    [7/5 - y]

    [x - 3/2]

```python
sp.solve((eq1,eq2),(x,y))
```

    {x: 29/20, y: -1/20}

```python
sp.solve((eq1,eq2),(x,y))[x]
sp.solve((eq1,eq2),(x,y))[y]
```

$\displaystyle - \frac{1}{20}$

```python
D,V,rho = sp.symbols('D,V_m, rho')
V
```

$\displaystyle V_{m}$

```python
f = sp.Function('f')
ode = D * f(x).diff(x,2) - V * rho
sp.Eq(ode,0)
```

$\displaystyle D \frac{d^{2}}{d x^{2}} f{\left(x \right)} - V_{m} \rho = 0$

```python
sp.dsolve(sp.Eq(ode,0))
```

$\displaystyle f{\left(x \right)} = C_{1} + C_{2} x + \frac{V_{m} \rho x^{2}}{2 D}$

# https://www.youtube.com/watch?v=MXUMJMrX2Gw

```python
import scipy as scp
import scipy.integrate as si
import numpy as np
import matplotlib.pyplot as plt

def odes(x,t):
    a1 = 3e5
    a2 = 0.2
    a3 = 4e-7
    a4 = 0.6
    a5 = 8
    a6 = 90

    A = x[0]
    B = x[1]
    C = x[2]

    dAdt = a1 - a2*A - a3*A*C
    dBdt = a3*A*C - a4*B
    dCdt = -a3*A*C - a5*C + a6*B

    return [dAdt, dBdt, dCdt]

x0 = [2e6, 0, 90]

t = np.linspace(0,15,1000)
x = si.odeint(odes,x0,t)

A = x[:,0]
B = x[:,1]
C = x[:,2]

plt.semilogy(t,A)
plt.semilogy(t,B)
plt.semilogy(t,C)
plt.show()
```

```python

```

```python

```