# Quadratic forms

A *quadratic form* is a polynomial in $n$ variables with all terms of degree two. Therefore, a quadratic form is a mapping $q:\mathbb{R}^n \rightarrow \mathbb{R}$. The term "form" is another name for "homogeneous polynomial", which is a polynomial whose all nonzero terms have the same degree.

Here are some examples of quadratic forms in one, two and three variables.

$$ q(x) = a x^2 $$

$$
\begin{split}
q(x,y) &= a x^2 + 2b xy + c y^2 \\
       &= x(ax + by) + y(bx + cy)
\end{split}
$$

$$
\begin{split}
q(x,y,z) &= a x^2 + d y^2 + f z^2  + 2b xy + 2c xz + 2e yz \\
         &= x(ax + by + cz) + y (bx + dy + ez) + z (cx + ey + fz)
\end{split}
$$

As the number of variables increases, more and more terms arise in the polynomial. Linear algebra allows to express quadratic forms with a more compact notation which is invariant from the number of variables.

The examples above can then be expressed as follows.

$$
q(x) =
\begin{bmatrix}
x
\end{bmatrix}
\begin{bmatrix}
a
\end{bmatrix}
\begin{bmatrix}
x
\end{bmatrix}
= \boldsymbol{x}^\intercal A \boldsymbol{x}
$$

$$
q(x,y) =
\begin{bmatrix}
x & y
\end{bmatrix}
\begin{bmatrix}
a & b \\
b & c
\end{bmatrix}
\begin{bmatrix}
x \\
y
\end{bmatrix}
= \boldsymbol{x}^\intercal A \boldsymbol{x}
$$

$$
q(x,y,z) =
\begin{bmatrix}
x & y & z
\end{bmatrix}
\begin{bmatrix}
a & b & c \\
b & d & e \\
c & e & f
\end{bmatrix}
\begin{bmatrix}
x \\
y \\
z
\end{bmatrix}
= \boldsymbol{x}^\intercal A \boldsymbol{x}
$$

Notice that the matrices $A$s are all symmetric.

Let's verify the correctness of the vectorized expressions above using the *sympy* module.

```python
from sympy import *
x, y, z = symbols('x y z')
a, b, c, d, e, f = symbols('a b c d e f')
```

### q(x,y)

```python
x_2d = Matrix([x, y])
A_2x2 = Matrix([[a, b], [b, c]])

x_2d.T * A_2x2 * x_2d
```

$\displaystyle \left[\begin{matrix}x \left(a x + b y\right) + y \left(b x + c y\right)\end{matrix}\right]$

### q(x,y,z)

```python
x_3d = Matrix([x, y, z])
A_3x3 = Matrix([[a, b, c], [b, d, e], [c, e, f]])

x_3d.T * A_3x3 * x_3d
```

$\displaystyle \left[\begin{matrix}x \left(a x + b y + c z\right) + y \left(b x + d y + e z\right) + z \left(c x + e y + f z\right)\end{matrix}\right]$

In the general case of an arbitrary number of variables $n$, the quadratic form can still be written using the same compact notation.

$$
\begin{split}
q(x_1,\dots,x_n) &= &\, x_{1} (a_{11} {x_1} + \, &\dots + a_{1n} {x_n}) \, + \\
& &\, x_{2} (a_{21} {x_1} + \, &\dots + a_{2n} {x_n}) \, + \\
& & &\,\, \vdots \\
& &\, x_{n} (a_{n1} {x_1} + \, &\dots + a_{nn} {x_n}) \\
& = &\, \boldsymbol{x}^\intercal A \boldsymbol{x} &
\end{split}
$$

Where

$$
A =
\begin{split}
\begin{bmatrix}
    a_{11} & \dots  & a_{1n} \\
    \vdots & \ddots & \vdots \\
    a_{n1} & \dots  & a_{nn}
    \end{bmatrix}
\end{split}
$$

and

$$
\boldsymbol{x} =
\begin{split}
\begin{bmatrix}
    x_1  \\
    \vdots \\
    x_n
    \end{bmatrix}
\end{split}
$$

```python

```


# Calculators

**CS1302 Introduction to Computer Programming**
___

```python
import math
from math import cos, exp, log, pi, sin, tan

import matplotlib.pyplot as plt
import numpy as np
from ipywidgets import interact

# interactive plot with ipympl
%matplotlib widget
```

The following code is a Python one-liner that creates a calculator.

- Evaluate the cell with `Ctrl+Enter`.
- Enter `1+1` and see the result.

```python
print(eval(input()))
```

---

**Tip**

Try some calculations below using this calculator:

1. $2^3$ by entering `2**3`;
1. $\frac23$ by entering `2/3`;
1. $\left\lceil\frac32\right\rceil$ by entering `3//2`;
1. $3\mod 2$ by entering `3%2`;
1. $\sqrt{2}$ by entering `2**0.5`; and
1. $\sin(\pi/6)$ by entering `sin(pi/6)`;

---

For this lab, you will create more powerful and dedicated calculators.
We will first show you a demo. Then, it will be your turn to create the calculators.

## Hypotenuse Calculator

---

**Proposition**

By the Pythagoras theorem, given a right-angled triangle,

the length of the hypotenuse is

$$
c = \sqrt{a^2 + b^2}
$$ (hypotenuse)

where $a$ and $b$ are the lengths of the other sides of the triangle.

---

We can define the following function to calculate the length `c` of the hypotenuse when given the lengths `a` and `b` of the other sides:

```python
def length_of_hypotenuse(a, b):
    c = (a**2 + b**2)**(0.5)  # Pythagoras
    return c
```

---

**Important**

You need not understand how a function is defined, but

- you should know how to *write the formula {eq}`hypotenuse` as a Python expression* using the exponentiation operator `**`, and
- *assign the variable* `c` the value of the expression (Line 2) using the assignment operator `=`.

---

For example, you may be asked to write Line 2, while Line 1 and 3 are given to you:

**Exercise** Complete the function below to return the length `c` of the hypotenuse given the lengths `a` and `b`.

```python
def length_of_hypotenuse(a, b):
    # YOUR CODE HERE
    raise NotImplementedError()
    return c
```

---

**Caution**

- Complete the above exercise to get the credit even though the answer was already revealed as a demo. Instead of copy-and-paste the answer, type it yourself.
- Note that indentation affects the execution of Python code. In particular, the assignment statement must be indented to indicate that it is part of the *body* of the function.

---

We will use `ipywidgets` to let user interact with the calculator more easily:

- After running the cell, move the sliders to change the values of `a` and `b`.
- Observer that the value of `c` is updated immediately.

```python
# hypotenuse calculator
@interact(a=(0, 10, 1), b=(0, 10, 1))
def calculate_hypotenuse(a=3, b=4):
    print("c: {:.2f}".format(length_of_hypotenuse(a, b)))
```

---

**Important**

You need not know how to write widgets, but you should know how to *format a floating point number* (Line 3).

---

You can check your code with a few cases listed in the test cell below.

```python
# tests
def test_length_of_hypotenuse(a, b, c):
    c_ = length_of_hypotenuse(a, b)
    correct = math.isclose(c, c_)
    if not correct:
        print(f"For a={a} and b={b}, c should be {c}, not {c_}.")
    assert correct

test_length_of_hypotenuse(3, 4, 5)
test_length_of_hypotenuse(0, 0, 0)
test_length_of_hypotenuse(4, 7, 8.06225774829855)
```

## Quadratic equation

### Graphical calculator for parabola

---

**Definition** Parabola

The collection of points $(x,y)$ satisfying the following equation forms a *parabola*:

$$
y=ax^2+bx+c
$$ (parabola)

where $a$, $b$, and $c$ are real numbers called the *coefficients*.

---

**Exercise** Given the variables `x`, `a`, `b`, and `c` store the $x$-coordinate and the coefficients $a$, $b$, and $c$ respectively, assign `y` the corresponding $y$-coordinate of the parabola {eq}`parabola`.

```python
def get_y(x, a, b, c):
    # YOUR CODE HERE
    raise NotImplementedError()
    return y
```

To test your code:

```python
# tests
def test_get_y(y, x, a, b, c):
    y_ = get_y(x, a, b, c)
    correct = math.isclose(y, y_)
    if not correct:
        print(f"With (x, a, b, c)={x,a,b,c}, y should be {y} not {y_}.")
    assert correct

test_get_y(0, 0, 0, 0, 0)
test_get_y(1, 0, 1, 2, 1)
test_get_y(2, 0, 2, 1, 2)
```

```python
# hidden tests
```

To run the graphical calculator:

```python
# graphical calculator for parabola
fig, ax = plt.subplots()
xmin, xmax, ymin, ymax, resolution = -10, 10, -10, 10, 50
x = np.linspace(xmin, xmax, resolution)
ax.set_title(r'$y=ax^2+bx+c$')
ax.set_xlabel(r'$x$')
ax.set_ylabel(r'$y$')
ax.set_xlim([xmin, xmax])
ax.set_ylim([ymin, ymax])
ax.grid()
p, = ax.plot(x, get_y(x, 0, 0, 0))

@interact(a=(-10, 10, 1), b=(-10, 10, 1), c=(-10, 10, 1))
def plot_parabola(a, b, c):
    p.set_ydata(get_y(x, a, b, c))
```

### Quadratic equation solver

---

**Proposition**

For the quadratic equation

$$
ax^2+bx+c=0,
$$ (quadratic)
the *roots* (solutions for $x$) are give by

$$
\frac{-b-\sqrt{b^2-4ac}}{2a},\frac{-b+\sqrt{b^2-4ac}}{2a}.
$$ (quadratic_roots)

---

**Exercise** Assign to `root1` and `root2` the values of the first and second roots above respectively.

```python
def get_roots(a, b, c):
    # YOUR CODE HERE
    raise NotImplementedError()
    return root1, root2
```

To test your code:

```python
# tests
def test_get_roots(roots, a, b, c):
    def mysort(c):
        return c.real, c.imag
    roots_ = get_roots(a, b, c)
    assert np.isclose(sorted(roots, key=mysort),
                      sorted(roots_, key=mysort)).all()

test_get_roots((-1.0, 0.0), 1, 1, 0)
test_get_roots((-1.0, -1.0), 1, 2, 1)
test_get_roots((-2.0, -1.0), 1, 3, 2)
test_get_roots([(-0.5-0.5j), (-0.5+0.5j)], 2, 2, 1)
```

```python
# hidden tests
```

To run the calculator:

```python
# quadratic equations solver
@interact(a=(-10,10,1),b=(-10,10,1),c=(-10,10,1))
def quadratic_equation_solver(a=1,b=2,c=1):
    print('Roots: {}, {}'.format(*get_roots(a,b,c)))
```

## Number conversion

### Byte-to-Decimal calculator

Denote a binary number stored in a byte ($8$ bits) as

$$
b_7\circ b_6\circ b_5\circ b_4\circ b_3\circ b_2\circ b_1\circ b_0,
$$
where $\circ$ concatenates $b_i$'s together into a binary string.

The binary string can be converted to a decimal number by the formula

$$
b_7\cdot 2^7 + b_6\cdot 2^6 + b_5\cdot 2^5 + b_4\cdot 2^4 + b_3\cdot 2^3 + b_2\cdot 2^2 + b_1\cdot 2^1 + b_0\cdot 2^0.
$$

E.g., the binary string `'11111111'` is the largest integer represented by a byte:

$$
2^7+2^6+2^5+2^4+2^3+2^2+2^1+2^0=255=2^8-1.
$$

**Exercise** Assign to `decimal` the *integer* value represented by the binary sequence `b7,b6,b5,b4,b3,b2,b1,b0` of *characters* `'0'` or `'1'`.

```python
def byte_to_decimal(b7, b6, b5, b4, b3, b2, b1, b0):
    """
    Parameters:
    -----------
    b7, ..., b0 are single characters either '0' or '1'.
    """
    # YOUR CODE HERE
    raise NotImplementedError()
    return decimal
```

To test your code:

```python
# tests
def test_byte_to_decimal(decimal, b7, b6, b5, b4, b3, b2, b1, b0):
    decimal_ = byte_to_decimal(b7, b6, b5, b4, b3, b2, b1, b0)
    assert decimal == decimal_ and isinstance(decimal_, int)

test_byte_to_decimal(38, '0', '0', '1', '0', '0', '1', '1', '0')
test_byte_to_decimal(20, '0', '0', '0', '1', '0', '1', '0', '0')
test_byte_to_decimal(22, '0', '0', '0', '1', '0', '1', '1', '0')
```

```python
# hidden tests
```

To run the calculator:

```python
# byte-to-decimal calculator
bit = ['0', '1']

@interact(b7=bit, b6=bit, b5=bit, b4=bit, b3=bit, b2=bit, b1=bit, b0=bit)
def convert_byte_to_decimal(b7, b6, b5, b4, b3, b2, b1, b0):
    print('decimal:', byte_to_decimal(b7, b6, b5, b4, b3, b2, b1, b0))
```

### Decimal-to-Byte calculator

**Exercise** Assign to `byte` a *string of 8 bits* that represents the value of `decimal`, a non-negative decimal integer from $0$ to $2^8-1=255$.
*Hint: Use `//` and `%`.*

```python
def decimal_to_byte(decimal):
    # YOUR CODE HERE
    raise NotImplementedError()
    return byte
```

To test your code:

```python
# tests
def test_decimal_to_byte(byte,decimal):
    byte_ = decimal_to_byte(decimal)
    assert byte == byte_ and isinstance(byte, str) and len(byte) == 8

test_decimal_to_byte('01100111', 103)
test_decimal_to_byte('00000011', 3)
test_decimal_to_byte('00011100', 28)
```

```python
# hidden tests
```

To run the calculator:

```python
# decimal-to-byte calculator
@interact(decimal=(0,255,1))
def convert_decimal_to_byte(decimal=0):
    print('byte:', decimal_to_byte(decimal))
```

## Symbolic calculator (optional)

Can we do complicated arithmetics with Python. What about Calculus?

$$
\int \tan(x)\, dx = \color{red}{?}
$$

Solution: <https://gamma.sympy.org/input/?i=integrate(tan(x))>

---

**Tip**

- Take a look at the different panels to learn about the solution: `Steps`, `Plot`, and `Derivative`.
- Try different [random examples](https://gamma.sympy.org/).

---

**How does SymPy Gamma work?**

[SymPy Gamma](https://gamma.sympy.org/) is a web application running [SymPy](https://docs.sympy.org/latest/index.html), which is a python library for symbolic computation.

**How to use SymPy?**

To import the library:

```python
import sympy as sp
```

We need to define a symbolic variable and assign it to a python variable.

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

The SymPy expression for $\tan(x)$ is:

```python
f = sp.tan(x)
f
```

To compute the integration:

```python
int_f = sp.integrate(f)
int_f
```

To compute the derivative:

```python
diff_int_f = sp.diff(int_f)
diff_int_f
```

The answer can be simplified as expected:

```python
diff_int_f.simplify()
```

To plot:

```python
p = sp.plot(f, int_f, (x, -sp.pi/4, sp.pi/4))
```

**Exercise**

Try to compute the following in SymPy and in jupyter notebook:

- $\frac{d}{dx} x^x$
- $\frac{d}{dx} \frac{1}{\sqrt{1 - x^2}}$.

---

**Hint**

Use `sp.sqrt` or `**(sp.S(1)/2)` for square root instead of `**0.5`. See [SymPy gotchas](https://docs.sympy.org/latest/gotchas.html).

---


Problem: Monte Carlo Estimate of $\int_0^1 \frac{1}{1+x} \mathrm{d}x$

\begin{equation}
\begin{split}
\int_0^1 \frac{1}{1+x} \mathrm{d}x & = \ln(x+1)+C |_0^1 \\
 & = \ln(2) - \ln(1) \\
 & = 0.693147 \\
\end{split}
\end{equation}

Source: https://en.wikipedia.org/wiki/Antithetic_variates

```python
# Calculate the truth
import math
truth = math.log(2)-math.log(1)
```

```python
# Classical Estimate
import numpy as np
from sklearn.metrics import mean_squared_error

def f(x): return 1.0/(1+x)
def sampling(n): return np.random.uniform(0,1,n)

n = 1500
rand = sampling(2*n)
samples = map(f, rand)

print "==== Classical Estimate ===="
print "Estimate: ", np.mean(samples)
print "Variance: ", np.var(samples)
```

    ==== Classical Estimate ====
    Estimate:  0.6935116995264258
    Variance:  0.019320749240938653

```python
# Antithetic Variates
import numpy as np
from sklearn.metrics import mean_squared_error

def f(x): return 1.0/(1+x)
def sampling(n): return np.random.uniform(0,1,n)

n = 1500
rand = sampling(n)
rand_anti = 1-rand
samples = np.add(map(f, rand), map(f, rand_anti))/2.0

print "==== Antithetic Variates ===="
print "Estimate: ", np.mean(samples)
print "Variance: ", np.var(samples)
```

    ==== Antithetic Variates ====
    Estimate:  0.6925277753075945
    Variance:  0.0005805944133518291


# Lagrange interpolating polynomials
Given equation:
\begin{align}
f(x) = \frac{1}{1 + 25x^2}
\end{align}

```python
# dependency import
from matplotlib import pyplot as plt
import numpy as np
%matplotlib notebook
```

```python
# functions
def f(x):
    return 1/(1 + 25*(x**2))

def lagrange(x, x_given, i):
    res = 1
    for x_j in x_given[:i] + x_given[i + 1:]:
        res *= (x - x_j) / (x_given[i] - x_j)
    return res

def poly_interpolate(x, x_given, y_given, n, i=0):
    if i + 1 in [len(x_given), n]: return lagrange(x, x_given, i) * y_given[i]
    return lagrange(x, x_given, i) * y_given[i] + poly_interpolate(x, x_given, y_given, n, i + 1)
```

```python
x_given = [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]
y_given = [f(x) for x in x_given]
```

```python
plt.plot(np.linspace(-1, 1, num=100), [f(x) for x in np.linspace(-1, 1, num=100)], '--', label='Actual f(x)')
plt.plot(x_given, y_given, 'x', color='red', label='Known f(x)')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
plt.grid()
plt.show()
```

    <IPython.core.display.Javascript object>

## Lagrange Interpolating polynomial

```python
plt.subplots_adjust(hspace=1)
for i in range(1, 9):
    ax = plt.subplot(420 + i)
    ax.plot(np.linspace(-1, 1, num=100), [f(x) for x in np.linspace(-1, 1, num=100)], '--')
    ax.plot(np.linspace(-1, 1, num=100), [poly_interpolate(x, x_given, y_given, i) for x in np.linspace(-1, 1, num=100)])
    ax.plot(x_given, y_given, 'x', color='red', label='Known f(x)')
    plt.title('Polynomial degree is: ' + str(i))
    ax.grid()
plt.show()
```

    <IPython.core.display.Javascript object>


## Deriving the transfer function of the Simper SVF filter structure

HTML output built with: jupyter nbconvert --to html svf_z_domain_tf.ipynb

Source:
https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf

We will follow the second form of the algorithm, found on page 6.

Sympy can't (very easily) be bent to display transfer functions in terms of $z^{-1}, z^{-2}, ...$ which is the convention. Plain $z$ will be used here instead - keep in mind it actually means $z^{-1}$.

```python
from sympy import *
init_printing()

z = symbols("z")
```

Start with the parameters.

```
g = Tan[π * cutoff / samplerate];
k = 1/Q = 2 - 2*res;
a1 = 1/(1 + g*(g + k));
a2 = g*a1;
```

The other coefficients defining the shape of the filter (`m0, m1, m2`) will be ignored for now, as they are only used to "mix" the output.

```python
g, k = symbols("g k")
a1 = 1/(1 + g*(g + k))
a2 = g*a1

(a1, a2)
```

Then the computation.

The variable `v0` represents the input signal - we will consider it to represent the z-transform of the input over time. `v1` and `v2` represent two other nodes in the block diagram.

The state variables `ic1eq` and `ic2eq` will be defined as unknowns first, and then we will solve them using their equations.

The relevant lines of the algorithm are:

```
v1 = a1 * ic1eq + a2 * (v0 - ic2eq);
v2 = ic2eq + g * v1;
```

Notice that the `ic1eq` and `ic2eq` actually refer to the _previous_ values of these samples. This corresponds to multiplying by $z$ (contrary to convetion!) in the z-domain.

```python
v0, ic1eq, ic2eq = symbols("v0 ic_1 ic_2")

v1 = a1 * ic1eq * z + a2 * (v0 - ic2eq * z)
v2 = ic2eq * z + g * v1

(v1, v2)
```

The "new" values for `ic1eq, ic2eq` are computed as follows:

```
ic1eq = 2*v1 - ic1eq;
ic2eq = 2*v2 - ic2eq;
```

depending on the current values of `v1, v2`, and the previous values of `ic1eq, ic2eq`.

Consider this as a system of equations, and solve it:

```python
equations = [
    2*v1 - ic1eq * z - ic1eq, # = 0
    2*v2 - ic2eq * z - ic2eq, # = 0
]
solution = solve(equations, (ic1eq, ic2eq))

solution
```

We may now subsitute the solution into `v1` and `v2` to obtain the transfer functions

$$
\begin{aligned}
H_0(z) &= \frac {v_0(z)} {v_0(z)} = 1  \\
H_1(z) &= \frac {v_1(z)} {v_0(z)} \\
H_2(z) &= \frac {v_2(z)} {v_0(z)}
\end{aligned}
$$

```python
H0 = 1
H1 = v1.subs(solution) / v0
H2 = v2.subs(solution) / v0

H1 = collect(simplify(H1), z)
H2 = collect(simplify(H2), z)

(H1, H2)
```

How convenient, the denominators seem to be the same! That is to be expected of course,
since taking linear combinations of $H_1, H_2$ cannot result in anything that has more than two poles, because the order of the system is 2.

```python
(H1_num, H1_denom) = fraction(H1)
(H2_num, H2_denom) = fraction(H2)

assert H1_denom == H2_denom
denom = H1_denom
denom
```

We can now assemble the complete transfer function, taking into account the mix coefficients `m0, m1, m2`.

$$
H(z) = m_0 H_0(z) + m_1 H_1(z) + m_2 H_2(z)
$$

```python
m0, m1, m2 = symbols("m0 m1 m2")

H = m0 * H0 + (m1 * H1_num + m2 * H2_num) / denom

print(H)
H
```

## Sanity check: High pass filter

```python
from sympy.functions import tan, exp

samplerate = 40_000
cutoff = sqrt(samplerate/2)
Q = 0.9

f = symbols("f")

H_hp_f = H.subs({
    g: tan(pi * cutoff / samplerate),
    k: 1/Q,
    m0: 1,
    m1: -1/Q, # = -k
    m2: -1,
    z: exp(2*I*pi * f / samplerate)**-1,
})

plot(abs(H_hp_f), (f, 1, samplerate/2), xscale='log', yscale='log')
```


# Contravariant & Covariant indices in Tensors (Symbolic)

```python
from einsteinpy.symbolic import ChristoffelSymbols, RiemannCurvatureTensor
from einsteinpy.symbolic.predefined import Schwarzschild
import sympy
sympy.init_printing()
```

### Analysing the schwarzschild metric along with performing various operations

```python
sch = Schwarzschild()
sch.tensor()
```

```python
sch_inv = sch.inv()
sch_inv.tensor()
```

```python
sch.order
```

```python
sch.config
```

    'll'

### Obtaining Christoffel Symbols from Metric Tensor

```python
chr = ChristoffelSymbols.from_metric(sch_inv) # can be initialized from sch also
chr.tensor()
```

```python
chr.config
```

    'ull'

### Changing the first index to covariant

```python
new_chr = chr.change_config('lll') # changing the configuration to (covariant, covariant, covariant)
new_chr.tensor()
```

```python
new_chr.config
```

    'lll'

### Any arbitary index configuration would also work!

```python
new_chr2 = new_chr.change_config('lul')
new_chr2.tensor()
```

### Obtaining Riemann Tensor from Christoffel Symbols and manipulating it's indices

```python
rm = RiemannCurvatureTensor.from_christoffels(new_chr2)
rm[0,0,:,:]
```

```python
rm.config
```

    'ulll'

```python
rm2 = rm.change_config("uuuu")
rm2[0,0,:,:]
```

```python
rm3 = rm2.change_config("lulu")
rm3[0,0,:,:]
```

```python
rm4 = rm3.change_config("ulll")
rm4.simplify()
rm4[0,0,:,:]
```

#### It is seen that `rm` and `rm4` are same as they have the same configuration

docs/source/examples/Playing with Contravariant and Covariant Indices in Tensors(Symbolic).ipynb


**Rational numbers**

```python
# source: https://scipy-lectures.org/packages/sympy.html

import sympy as sym
from sympy import pprint
print("SymPy defines three numerical types: Real, Rational and Integer.")
a=sym.Rational(1,2)
b=sym.Rational(1,3)
print("a:",a)
print("b:",b)
print("Rational arithmetic can be performed")
print("a+b:",a+b)
print("2a:",2*a)
```

    SymPy defines three numerical types: Real, Rational and Integer.
    a: 1/2
    b: 1/3
    Rational arithmetic can be performed
    a+b: 5/6
    2a: 1

```python
print("special constants, like e, pi, oo (Infinity), are treated as symbols and can be evaluated with arbitrary precision")
pprint(2*sym.pi)
pprint(sym.pi**2)
pprint(sym.exp(1))
print("\nEvaluating the value using evalf function")
print("2pi:",(2*sym.pi).evalf())
print("pi^2:",(sym.pi**2).evalf())
print("e^1:",(sym.exp(1)).evalf())
print("Infinity + 1:",sym.oo+1)
print("\nOne can adjust the precision required using the evalf")
print("2pi:",(2*sym.pi).evalf(3))
print("100 decimal precision of Square root of 2")
print((sym.sqrt(2)).evalf(100))
```

    special constants, like e, pi, oo (Infinity), are treated as symbols and can be evaluated with arbitrary precision
    2⋅π
     2
    π
    ℯ

    Evaluating the value using evalf function
    2pi: 6.28318530717959
    pi^2: 9.86960440108936
    e^1: 2.71828182845905
    Infinity + 1: oo

    One can adjust the precision required using the evalf
    2pi: 6.28
    100 decimal precision of Square root of 2
    1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573

**Expand and simplify**

```python
x=sym.Symbol('x')
y=sym.Symbol('y')
print(x+y+x-y)
print((x+y)*(x-y))
print(sym.expand((x+y)**4))
print("Adding additional arguments to expand command")
pprint(sym.expand(x+y,complex=True))
print(sym.expand(sym.cos(x+y),trig=True))
pprint(sym.expand((x+y)*(x-y)))
print(sym.expand((x+y)**6))

print("Using simplify command")
print(sym.simplify((x + x * y) / x))
print(sym.simplify(sym.sin(x)/sym.cos(x),trig=True))
```

    2*x
    (x - y)*(x + y)
    x**4 + 4*x**3*y + 6*x**2*y**2 + 4*x*y**3 + y**4
    Adding additional arguments to expand command
    re(x) + re(y) + ⅈ⋅im(x) + ⅈ⋅im(y)
    -sin(x)*sin(y) + cos(x)*cos(y)
     2    2
    x  - y
    x**6 + 6*x**5*y + 15*x**4*y**2 + 20*x**3*y**3 + 15*x**2*y**4 + 6*x*y**5 + y**6
    Using simplify command
    y + 1
    tan(x)

**Calculus and pretty printing**

```python
print("syntax for limits limit(function, variable, point)")
print(sym.limit(sym.sin(x)/x,x,0))
print(sym.limit(1/x,x,sym.oo))
print("\nsyntax for differentiation diff(function, variable, order)")
print(sym.diff(sym.log(x),x))
pprint(sym.diff(sym.tan(x),x))
print("\ncheck")
pprint(sym.limit((sym.tan(x+y)-sym.tan(x))/y,y,0))
print("\nCalculating higher derivatives using diff command")
pprint(sym.diff(x**9,x,6))
print("\ncheck")
for i in range(1,10):
    pprint(sym.diff(x**9,x,i))
```

    syntax for limits limit(function, variable, point)
    1
    0

    syntax for differentiation diff(function, variable, order)
    1/x
       2
    tan (x) + 1

    check
       2
    tan (x) + 1

    Calculating higher derivatives using diff command
           3
    60480⋅x

    check
       8
    9⋅x
        7
    72⋅x
         6
    504⋅x
          5
    3024⋅x
           4
    15120⋅x
           3
    60480⋅x
            2
    181440⋅x
    362880⋅x
    362880

**Taylor Series expansion**

```python
print("Syntax series(expression, variable)")
pprint(sym.series(sym.cos(x),x))
pprint(sym.series((1/sym.cos(x)),x))
```

    Syntax series(expression, variable)
         2    4
        x    x     ⎛ 6⎞
    1 - ── + ── + O⎝x ⎠
        2    24
         2      4
        x    5⋅x     ⎛ 6⎞
    1 + ── + ──── + O⎝x ⎠
        2     24

**Integration**

```python
print("syntax: integrate(function,(variable,lowlimit,highlimit))")
pprint(sym.integrate(6*x**5+y,x))
pprint(sym.integrate(sym.log(x),x))

print("\nDefinite integrals")
print(sym.integrate(x**3,(x,-1,1)))
print(sym.integrate(sym.sin(x),(x,0,sym.pi/2)))
print(sym.integrate((sym.cos(x)),(x,-sym.pi/2,sym.pi/2)))
print(sym.integrate(sym.exp(-x),(x,0,sym.oo)))
print("integrating the following function from from -infinity to infinity")
pprint(sym.exp(-x ** 2))
print("gives")
pprint(sym.integrate(sym.exp(-x**2),(x,-sym.oo,sym.oo)))
```

    syntax: integrate(function,(variable,lowlimit,highlimit))
     6
    x  + x⋅y
    x⋅log(x) - x

    Definite integrals
    0
    1
    2
    1
    integrating the following function from from -infinity to infinity
       2
     -x
    ℯ
    gives
    √π

**Solving equations**

```python
print("for solving algebraic equations use solveset(function,variable), for solving SLE use solve((eq1,eq2),(var1,var2))")
pprint(sym.solveset(x**4-1,x))
pprint(sym.solveset(sym.exp(x)+1,x))

sol=sym.solve((x + 5 * y - 2, -3 * x + 6 * y - 15),(x,y))
print(sol)
print(sol[x])
print(sym.solve((x+y-2,2*x+y),(x,y)))

print("\nfactorization of a polynomial")
f=x**4-3*x**2+1
pprint(f)
pprint(sym.factor(f))

print("\nSolving boolean expressions")
pprint(sym.satisfiable((~x|y)&(~y|x)))
```

    for solving algebraic equations use solveset(function,variable), for solving SLE use solve((eq1,eq2),(var1,var2))
    {-1, 1, -ⅈ, ⅈ}
    {ⅈ⋅(2⋅n⋅π + π) | n ∊ ℤ}
    {x: -3, y: 1}
    -3
    {x: -2, y: 4}

    factorization of a polynomial
     4      2
    x  - 3⋅x  + 1
    ⎛ 2        ⎞ ⎛ 2        ⎞
    ⎝x  - x - 1⎠⋅⎝x  + x - 1⎠

    Solving boolean expressions
    {x: False, y: False}

**Finding the value of a function at various points**

```python
def f(x):
    return sym.exp(x)**4-3*x**2+1
for i in range(1,4):
    pprint(f(i))
    print(f(i).evalf())
```

          4
    -2 + ℯ
    52.5981500331442
           8
    -11 + ℯ
    2969.95798704173
           12
    -26 + ℯ
    162728.791419004

**Linear algebra**

```python
A=sym.Matrix([[1,2,3],[3,4,5]])
pprint(A)
print("unlike a NumPy array, you can also put Symbols in it")
x,y=sym.symbols('x,y')
A=sym.Matrix([[1,x],[y,1]])
pprint(A)
```

    ⎡1  2  3⎤
    ⎢       ⎥
    ⎣3  4  5⎦
    unlike a NumPy array, you can also put Symbols in it
    ⎡1  x⎤
    ⎢    ⎥
    ⎣y  1⎦

**Differential equations**

```python
f, g = sym.symbols('f g', cls=sym.Function) #create an undefined function by passing cls=Function
#  f(x), will represent an unknown function
pprint(f(x).diff(x)+f(x))
pprint(f(x).diff(x,x)+f(x))
print("solving differential equations")
pprint(sym.dsolve(f(x).diff(x,x)+f(x),f(x)))
# pprint(sym.dsolve(sym.sin(x) * sym.cos(f(x)) + sym.cos(x) * sym.sin(f(x)) * f(x).diff(x), f(x), hint='separable')) #solve as seperable equation
pprint((f(x).diff(x)*x+f(x)-f(x)**2))
pprint(sym.dsolve(f(x).diff(x)*x+f(x)-f(x)**2,f(x)))
```

           d
    f(x) + ──(f(x))
           dx
             2
            d
    f(x) + ───(f(x))
             2
           dx
    solving differential equations
    f(x) = C₁⋅sin(x) + C₂⋅cos(x)
      d           2
    x⋅──(f(x)) - f (x) + f(x)
      dx
             -C₁
    f(x) = ───────
           -C₁ + x


# Python - Symbolic Mathematics (`sympy`)

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

```python
sp.init_printing()
```

### `sympy` treats stuff fundementally different than `numpy`

```python
np.sqrt(8)
```

```python
type(np.sqrt(8))
```

```python
sp.sqrt(8)
```

```python
type(sp.sqrt(8))
```

```python
np.sqrt(8) == sp.sqrt(8)
```

```python
np.pi
```

```python
np.pi + 4
```

```python
sp.pi
```

```python
sp.pi + 4
```

#### You can use `np.float64()` to turn a `sympy` float to a `numpy` float

```python
my_sympy_to_numpy_pi = np.float64(sp.pi)
```

```python
type(my_sympy_to_numpy_pi)
```

```python
my_sympy_to_numpy_pi + 4
```

### sympy has its own way to handle rational numbers

```python
sp.Rational(3,5)
```

```python
sp.Rational(3,5) + sp.Rational(1,3)
```

#### Adding `.n()` to the end of a sympy expression will `evaluate` expression

```python
sp.Rational(3,5).n()
```

```python
sp.pi.n()
```

#### You can add a value to `.n(value)` to set the number of significant figures

```python
sp.Rational(222,7).n()
```

```python
sp.Rational(222,7).n(1)
```

```python
sp.Rational(222,7).n(3)
```

#### `nsimplify()` will sort-of do the reverse of `.n()`

```python
sp.nsimplify(0.6)
```

```python
sp.nsimplify(4.242640687119286)
```

```python
sp.nsimplify(sp.pi, tolerance=1e-2)
```

```python
sp.nsimplify(sp.pi, tolerance=1e-5)
```

```python
sp.nsimplify(sp.pi, tolerance=1e-6)
```

### ... to $\infty$ and beyond

```python
sp.oo
```

```python
sp.oo + 3
```

```python
1e199 < sp.oo
```

---
## Matrix

* There are a couple of different ways to create a Matrix
* `Matrix(rows of lists)`
* `Matrix(shape, list)`

#### `rows of lists` is pretty straightforward

```python
sp.Matrix([[1,2],[3,4]])
```

#### `shape, list` gives you more flexibility

```python
sp.Matrix(2,2,[1,2,3,4])
```

```python
sp.Matrix(1,4,[1,2,3,4])
```

```python
sp.Matrix(4,1,[1,2,3,4])
```

```python
my_matrix = sp.Matrix(2,2,[1,2,3,4])

my_matrix
```

```python
my_matrix.det()
```

```python
my_matrix ** -1
```

```python
(my_matrix ** -1) * my_matrix
```

---
# Symbolic

### You have to explicitly tell `SymPy` what symbols you want to use.

* Once you declare symbols to use in `sympy` they are unavaliable for other packages

```python
x, y = sp.symbols('x y')
a, b, c = sp.symbols('a b c')
```

### Expressions are then able use these symbols

```python
my_equation = (a * x**2 * y)  + (b * x * y) + (c * x * y**2) + 6

my_equation
```

```python
my_equation + 3
```

```python
my_equation / x
```

```python
sp.simplify(my_equation / x)
```

```python
sp.collect(my_equation,x)
```

```python
sp.collect(my_equation,y)
```

### You can evaluate equations for specific values

```python
my_equation_x = sp.collect(my_equation,x)

my_equation_x
```

```python
my_equation_x.subs({y:sp.Rational(1,2), a:4, b:2, c:8})
```

### You can evaluate equations sybolically

```python
my_equation_x
```

```python
my_y = (2*x + 3)

my_y
```

#### Replace $y$ with $2x + 3$ using `.subs()`

```python
my_equation_x.subs(y, my_y)
```

#### Multiply everything through using `.expand()`

```python
sp.expand(my_equation_x.subs(y, my_y))
```

#### Collect the terms of $x$ using `.collect()`

```python
sp.collect(sp.expand(my_equation_x.subs(y, my_y)),x)
```

#### Evaluate the expression for some values of $a, b, c$ using `.subs()`

```python
sp.collect(sp.expand(my_equation_x.subs(y, my_y)),x).subs({a:4, b:2, c:8})
```

---
## Calculus

```python
my_equation
```

```python
sp.diff(my_equation,x)
```

```python
sp.diff(my_equation,x,2)
```

```python
sp.integrate(my_equation,x)
```

```python
sp.integrate(my_equation,(x,0,5))   # limits x = 0 to 5
```

```python
sp.integrate(my_equation,(x,0,5)).n()
```

```python
sp.integrate(my_equation,(x,0,5)).subs({y:sp.Rational(1,2), a:4, b:2, c:8})
```

```python
sp.integrate(my_equation,(x,0,5)).subs({y:sp.Rational(1,2), a:4, b:2, c:8}).n()
```

---
# Solving equations - `solve`

* Need to rearrange equation so that it is set to equal zero

### One equation

$$ \large
3x - 3 =  30 \hspace{1cm} \rightarrow \hspace{1cm} 3x - 33 = 0
$$

```python
equation_in_x = 3 * x - 33

equation_in_x
```

```python
sp.solve([equation_in_x], [x])
```

### System of equations

$$ \large
\begin{array}{c}
9x - 2y = 5\\
-2x + 6y = 10\\
\end{array}
$$

```python
equation_a = 9*x - 2*y - 5
equation_b = -2*x + 6*y - 10
```

```python
sp.solve([equation_a, equation_b], [x,y])
```

---

### We can also do it the Matrix way

$$ \large
\begin{bmatrix}
9 & -2 \\
-2 & 6 \\
\end{bmatrix}
\begin{bmatrix}
x \\
y
\end{bmatrix}
=
\begin{bmatrix}
5 \\
10
\end{bmatrix}
$$

$$ \large
A \vec x = b
$$

```python
my_A = sp.Matrix(2,2,[9, -2,-2, 6])

my_A
```

```python
my_b = sp.Matrix(2,1,[5,10])

my_b
```

$$ \large
\vec x = A^{-1}b
$$

```python
my_A ** -1 * my_b
```

---
## Let's do some graphing stuff ...

### In the following examples - notice the difference between `numpy` variables and `sympy` variables!

$$
\large f(x) = 2\,\cos(5x) \ e^{-x}
$$

### Need to create a `numpy` arrays to do the graphing

```python
# 200 points between -pi and pi

my_np_x = np.linspace(-np.pi, np.pi, 200)
```

```python
my_np_fx = 2 * np.cos(5 * my_np_x) * np.exp(-my_np_x)
```

```python
plt.style.use('ggplot')
```

```python
fig,ax = plt.subplots(1,1)
fig.set_size_inches(10,6)

fig.tight_layout()

ax.set_ylim(-11,11)
ax.set_xlim(-np.pi,np.pi)

ax.set_xlabel("This is X", fontsize = 14)
ax.set_ylabel("This is Y", fontsize = 14)

ax.plot(my_np_x, my_np_fx,
        color = (1.0, 0.0, 0.0, 0.5),
        marker='None',
        linestyle='-',
        linewidth = 6,
        label = "f(x)")

ax.legend(loc = 0, fontsize = 24, shadow = True);
```

---
## Taylor Series

* Taylor series are polynomial approximations at a specific point

```python
my_sp_fx = 2 * sp.cos(5 * x) * sp.exp(-x)
my_sp_fx
```

### Taylor series of f(x) at f(x) = 0

```python
my_taylor = sp.series(my_sp_fx, x, x0 = 0)

my_taylor
```

### If you want differnt number of terms

* n = magnitude of the highest term
* n = 4 means all terms up to x$^{4}$ or $\mathcal{O}(4)$

```python
my_taylor = sp.series(my_sp_fx, x, x0 = 0, n=4)

my_taylor
```

```python
my_taylor.removeO()
```

```python
my_taylor.removeO().n(3)
```

```python
# Make NumPy versions of the term to plot

my_np_1term = -2.0 * my_np_x + 2.0
my_np_2term = -24.0 * my_np_x**2 - 2.0 * my_np_x + 2.0
my_np_3term = 24.7 * my_np_x**3 - 24.0 * my_np_x**2 - 2.0 * my_np_x + 2.0
```

```python
fig,ax = plt.subplots(1,1)
fig.set_size_inches(10,8)

fig.tight_layout()

ax.set_ylim(-4,4)
ax.set_xlim(-1,1)

ax.set_xlabel("This is X", fontsize = 14)
ax.set_ylabel("This is Y", fontsize = 14)

ax.plot(my_np_x, my_np_fx,
        color = (1.0, 0.0, 0.0, 0.5),
        marker='None',
        linestyle='-',
        linewidth = 10,
        label = "f(x)")

ax.plot(my_np_x, my_np_1term, color='b', marker='None', linestyle='--', label="1-term")
ax.plot(my_np_x, my_np_2term, color='g', marker='None', linestyle='--', label="2-term")
ax.plot(my_np_x, my_np_3term, color='k', marker='None', linestyle='--', label="3-term")

ax.legend(loc = 0, fontsize = 18);
```

---
## General Equation Solving - `nsolve`

$$
\Large f(x) = 2\,\cos(5x) \ e^{-x} \\[10pt]
\Large g(x) = \frac{3}{2} \left [\frac{x^3}{\pi} - \pi x \right]\\
$$

### Make a `numpy` version of g(x)

```python
my_np_gx = 3/2 * (my_np_x ** 3 / np.pi - np.pi * my_np_x)
```

### Where do they cross? - The graph

```python
fig,ax = plt.subplots(1,1)
fig.set_size_inches(10,6)

fig.tight_layout()

ax.set_ylim(-7,7)
ax.set_xlim(-np.pi,np.pi)

ax.set_xlabel("This is X", fontsize = 14)
ax.set_ylabel("This is Y", fontsize = 14)

ax.plot(my_np_x, my_np_fx,
        color = (1.0, 0.0, 0.0, 0.5),
        marker='None',
        linestyle='-',
        linewidth = 6,
        label = "f(x)")

ax.plot(my_np_x, my_np_gx,
        color = (0.25, 0.0, 0.75, 0.5),
        marker='None',
        linestyle='-',
        linewidth = 6,
        label = "g(x)")

ax.legend(loc = 0, fontsize = 24);
```

### Where do they cross? - The `sympy` solution

### Make a `sympy` version of g(x)

```python
my_sp_gx = sp.Rational(3,2) * (x ** 3 / sp.pi - sp.pi * x)

my_sp_gx
```

```python
my_sp_fx, my_sp_gx
```

### Need to provide an initial guess

```python
my_guess = 3.0

sp.nsolve(my_sp_fx - my_sp_gx, x, my_guess)
```

```python
all_guesses = (3.0, 0, -1.0)

for val in all_guesses:
    result = sp.nsolve(my_sp_fx - my_sp_gx, x, val)
    print(result)
```

### Your guess has to be (somewhat) close or the solution will not converge:

```python
my_guess = -40

sp.nsolve(my_sp_fx - my_sp_gx, x, my_guess)
```

---
## Primes

```python
# List of primes in the range 0 -> 100

list(sp.primerange(0,100))
```

```python
# The 100th prime number

sp.prime(100)
```

```python
# The next prime after 2020

sp.nextprime(2020)
```

```python
# The prime factors of this year

sp.factorint(2022)
```

# `SymPy` can do *so* much more. It really is magic.

## Complete documentation can be found [here](http://docs.sympy.org/latest/index.html)


# Monte Carlo methods - A Sandbox/Demo

Monte Carlo broadly refers to random sampling methods. Here we will focus mainly on Monte Carlo sampling in the context of molecular simulations, but it's worth briefly considering an example of Monte Carlo integration. Particularly, we can use Monte Carlo integration to calculate $\pi$ with just a few lines of code.

## Calculating $\pi$ via MC integration

So, let's calculate $\pi$ by doing MC integration. Particularly, let's consider a drawing random numbers between -1 and 1, and then imagine a circle centered at (0, 0). Consider the area of that circle (of radius 1) to that of the full square spanned by our random numbers (2 units wide). Particularly, if $R$ is the radius of the circle, then the ratio of the areas is:
\begin{equation}
\frac{A_{sq}}{A_{cir}} = \frac{(2R)^2}{\pi R^2} = \frac{4}{\pi}
\end{equation}

so we find
\begin{equation}
\pi = \frac{4 A_{cir}}{A_{sq}}
\end{equation}

So, if we randomly place points in an interval -1 to 1, and then check to see how many fall within a square versus within a circle, we can use the ratio of counts (related to the ratio of the areas) to determine $\pi$.

```python
#Import modules we need
import numpy
import numpy.random

#Number of data points to sample
Ntrials = 1000

#Randomly generate array of XY positions - spanning -1 to 1
XY = 2.*numpy.random.rand( Ntrials, 2)-1.

#Compute distance from each point to center of the circle
distances = numpy.sqrt( numpy.sum( XY*XY, axis=1))

#Find indices of data points which are within the unit circle
#Note that you could code this in a more straightforward - but slower - way by setting up a
#'for' loop over data points and checking to see where the distance is less than 1.
indices_inside = numpy.where( distances < 1)

#Find how many points are here
num_inside = len( indices_inside[0] )

#Calculate estimate of pi
pi_estimate = 4.*num_inside/float(Ntrials)
print(pi_estimate)
```

    3.228

### Let's take a look at what we've done here, graphically.
We can plot the points inside and outside in two different colors to see.

```python
%pylab inline

#After the code above, indices_inside has the numbers of points which are inside our unit circle.
#We would also like indices of points which are outside the unit circle
indices_outside = numpy.where( distances > 1)

#Make our plot of points inside and outside using circles for the data points
#blue for inside, red for outside
plot(XY[indices_inside[0],0], XY[indices_inside[0],1], 'bo')
plot(XY[indices_outside[0],0], XY[indices_outside[0],1], 'ro')

#Adjust plot settings to look nicer
axis('equal') #Set to have equal axes
F = pylab.gcf() #Get handle of current figure
F.set_size_inches(5,5) #Set size to be square (default view is rectangular)

#Bonus: Drawing a unit circle on the graph is up to you
```

# Let's revisit the LJ particles we saw in the MD sandbox

In our last sandbox, we looked at molecular dynamics on a pair of Lennard-Jones particles. Now let's revisit that, but within the Metropolis Monte Carlo framework. Optionally, you could make things a little more interesting here by considering an extra particle. But for now let's just start with the same two particles as last time to make visualization easy..

Here, we'll apply the Metropolis MC framework as discussed in lecture, where every step we:
* Randomly pick a particle
* Change each of x, y, and z a small amount between $-\Delta r_{max}$ and $\Delta r_{max}$
* Compute the energy change $\Delta U$
* Apply the Metropolis criterion to decide whether to accept the move or reject it
  * If $\Delta U < 0$, accept the move
  * If $\Delta U > 0$, accept with the probability $P_{acc} = e^{-\Delta U/T}$
* If accepted, keep the new configuration
* Regardless of whether we accept it or not, update any running averages with the current state and energy

## Let's run some MC

(Before doing the below, you need to compile the `mc_sandbox` Fortran library as you've done for other libraries previously; typically it will look something like `f2py -c -m mc_sandbox mc_sandbox.f90` on the command-line.)

### First, we set up our system:

```python
import mc_sandbox
import numpy as np
import numpy.random

#Let's define the variables we'll need
Cut = 2.5
L = 3.0 #Let's put these in a small box so they don't lose each other
max_displacement = 0.1 #Maximum move size
T = 1. #You should play with this and see how the results change. Don't make it an integer - use a floating point value (i.e. 1., not 1)

#Choose N for number of particles; you could adjust this later.
N = 2

#Allocate position array - initially just zeros
Pos = np.zeros((N,3), float)

#Let's place the first two particles just as we did in MD Sandbox:
Pos[0,:] = np.array([0,0,0])
#We'll place the second one fairly nearby - at this point using the same starting location
#as in the MD sandbox
Pos[1,:] = np.array([1.5,0,0])

#If you have any other particles, let's just place them randomly
for i in range(2,N):
    Pos[i,:] = L*np.random.random( 3 )
```

### Now let's run a step of MC!

```python
#Set maximum number of steps to run
max_steps = 10000

#Set up storage for position vs step
Pos_t = np.zeros(( N,3,max_steps), float)
#Store initial positions
Pos_t[:,:,0] = Pos

#Evaluate initial energy
U = mc_sandbox.calcenergy(Pos, L, Cut)

#Pick a random particle
num = np.random.randint(N) #Random integer from 0 up to but not including N

#Store old position in case we need to revert
#Note that it's necessary here to make a copy, otherwise both still point to the same
#coordinates (try OldPos = Pos to see).
OldPos = Pos.copy()

#Pick a move - adjusting to make it between -DeltaX and +DeltaX
move = max_displacement * (np.random.random( 3)*2.-1.)

#Update position
Pos[num, :] += move

#Evaluate new energy
Unew = mc_sandbox.calcenergy( Pos, L, Cut)
DeltaU = Unew - U
print("U, Unew, DeltaU: ", U, Unew, DeltaU) #Just for debugging purposes so we can see what's happening.

#Print acceptance probability
Pacc = np.exp(-DeltaU/T)
print("Acceptance probability Pacc=", Pacc) #Just for debugging purposes so we can see what's happening.

#We can handle the uphill and downhill cases with a single 'if' statement
if np.random.rand() < Pacc:
    print("Accepted")  #Just for debugging purposes so we can see what's happening.
    U = Unew
else: #Revert
    Pos = OldPos
    print("Rejected") #Just for debugging purposes so we can see what's happening.

#Remember, at the end, if we are tracking energy, we update running averages/tracking data
#with the current position and energy
Pos_t[:, :, 1] = Pos
```

    U, Unew, DeltaU:  -0.3366534854145746 -0.4458429221834246 -0.10918943676884996
    Acceptance probability Pacc= 1.1153736231
    Accepted

## Now let's again define that get_r function so we can look at the separation between our particles over time
Last time, I was lazy and didn't handle the minimum image convention (in part because I knew we weren't giving the particles enough energy initially that they wouldn't fly apart). Here, because T (effectively, the kinetic energy) is an adjustable parameter we might end up with them flying apart, so we need to use the minimum image convention to properly measure the distance between particles. In other words, if a particle crosses the box edge and then finds the other particle and interacts with it, we want our distance measurement to notice that they are interacting rather than reporting that they are very distant. (The Fortran code we have is using this convention for its energy calculations).

So, we define a new get_r function which handles this:

```python
def get_r(Pos, L):
    """Calculate r, the distance between particles, for a position array containing just two particles. Return it.
    Unlike in MD sandbox, here we also implement the minimum image convention"""

    #Get displacement
    disp = Pos[1,:] - Pos[0,:]
    #Apply minimum image convention
    disp = disp - L*np.round(disp/L)
    #Calculate distance
    d = np.sqrt( np.dot( disp, disp))
    return d
```

## Now, write your own code - adapting the code from above for a single step - to run an MC simulation of your pair of LJ particles

Because a little more code is required than for the MD assignment, I provide some comments guiding you through the steps you'll need to do. And I also provide code to generate a plot at the end.

Be sure to track the acceptance probability over all suggested moves. (This could be used to adjust the size of the moves you suggest).

```python
# Put your code here

```

```python
##GET READY TO PLOT
#Find x axis (MC steps rather than time, as it was in the MD sandbox)
t = np.arange(0,max_steps)
#Find y axis (r values)
r_vs_t = []
for i in range(max_steps):
    r=get_r(Pos_t[:,:,i], L)
    r_vs_t.append(r)

r_vs_t = np.array(r_vs_t)

#Plot
figure()
plot(t, r_vs_t)
```

## Other things to be sure to try
* See what happens if you adjust the temperature. Particularly, check what happens in the limit of T becoming very small (approaching zero). Are uphill moves ever accepted? What does an MC search end up doing?
* Try making the box size reasonably big and see what happens at a moderate temperature
* Adjust the move size (`max_displacement`) to make the acceptance probability 30-50%. How big can you make it?

## Other things to try if you have extra time
* Check the probability distribution of separations and see how it varies with temperature.


```python
# setup SymPy
from sympy import *
init_printing()
x, y, z, t = symbols('x y z t')
alpha, beta = symbols('alpha beta')

```

# Linearity

```python
b, m = symbols('b m')

def f(x):
    return m*x
```

```python
f(1)
```

```python
f(2)
```

```python
f(1+2)
```

```python
f(1) + f(2)
```

```python
expand(f(x+y)) ==  f(x) + f(y)
```

    True

## What about vector inputs?

```python
m_1, m_2 = symbols('m_1 m_2')

def T(vec):
    """A function that takes a 2D vector and returns a number."""
    return m_1*vec[0] + m_2*vec[1]
```

```python
u_1, u_2 = symbols('u_1 u_2')
u = Matrix([u_1,u_2])
v_1, v_2 = symbols('v_1 v_2')
v = Matrix([v_1,v_2])
```

```python
T(u)
```

```python
T(v)
```

```python
T(u) + T(v)
```

```python
expand( T(u+v) )
```

```python
simplify( T(alpha*u + beta*v) - alpha*T(u) - beta*T(v) )
```

# Linear transformations

A linear transformation is function that takes vectors as inputs, and produces vectors as outputs:

$$
   T: \mathbb{R}^n \to \mathbb{R}^m.
$$

see page 116 in book

```python
m_11, m_12, m_21, m_22 = symbols('m_11 m_12 m_21 m_22')

def T(vec):
    """A linear transformations R^2 --> R^2."""
    out_1 = m_11*vec[0] + m_12*vec[1]
    out_2 = m_21*vec[0] + m_22*vec[1]
    return Matrix([out_1, out_2])
```

```python
T(u)
```

```python
T(v)
```

```python
T(u+v)
```

## Linear transformations as matrix-vector products

see page 113

```python
def T_impl(vec):
    """A linear transformations implemented as matrix-vector product."""
    M_T = Matrix([[m_11, m_12],
                  [m_21, m_22]])
    return M_T*vec
```

```python
T_impl(u)
```

```python

```


# Modeling and Simulation in Python

Chapter 9

Copyright 2017 Allen Downey

License: [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0)

```python
# import everything from SymPy.
from sympy import *

# Set up Jupyter notebook to display math.
init_printing()
```

The following displays SymPy expressions and provides the option of showing results in LaTeX format.

```python
from sympy.printing import latex

def show(expr, show_latex=False):
    """Display a SymPy expression.

    expr: SymPy expression
    show_latex: boolean
    """
    if show_latex:
        print(latex(expr))
    return expr
```

### Analysis with SymPy

Create a symbol for time.

```python
t = symbols('t')
t
```

If you combine symbols and numbers, you get symbolic expressions.

```python
expr = t + 1
expr
```

The result is an `Add` object, which just represents the sum without trying to compute it.

```python
type(expr)
```

    sympy.core.add.Add

`subs` can be used to replace a symbol with a number, which allows the addition to proceed.

```python
expr.subs(t, 2)
```

`f` is a special class of symbol that represents a function.

```python
f = Function('f')
f
```

    f

The type of `f` is `UndefinedFunction`

```python
type(f)
```

    sympy.core.function.UndefinedFunction

SymPy understands that `f(t)` means `f` evaluated at `t`, but it doesn't try to evaluate it yet.

```python
f(t)
```

`diff` returns a `Derivative` object that represents the time derivative of `f`

```python
dfdt = diff(f(t), t)
dfdt
```

```python
type(dfdt)
```

    sympy.core.function.Derivative

We need a symbol for `alpha`

```python
alpha = symbols('alpha')
alpha
```

Now we can write the differential equation for proportional growth.

```python
eq1 = Eq(dfdt, alpha*f(t))
eq1
```

And use `dsolve` to solve it.  The result is the general solution.

```python
solution_eq = dsolve(eq1)
solution_eq
```

We can tell it's a general solution because it contains an unspecified constant, `C1`.

In this example, finding the particular solution is easy: we just replace `C1` with `p_0`

```python
C1, p_0 = symbols('C1 p_0')
```

```python
particular = solution_eq.subs(C1, p_0)
particular
```

In the next example, we have to work a little harder to find the particular solution.

### Solving the quadratic growth equation

We'll use the (r, K) parameterization, so we'll need two more symbols:

```python
r, K = symbols('r K')
```

Now we can write the differential equation.

```python
eq2 = Eq(diff(f(t), t), r * f(t) * (1 - f(t)/K))
eq2
```

And solve it.

```python
solution_eq = dsolve(eq2)
solution_eq
```

The result, `solution_eq`, contains `rhs`, which is the right-hand side of the solution.

```python
general = solution_eq.rhs
general
```

We can evaluate the right-hand side at $t=0$

```python
at_0 = general.subs(t, 0)
at_0
```

Now we want to find the value of `C1` that makes `f(0) = p_0`.

So we'll create the equation `at_0 = p_0` and solve for `C1`.  Because this is just an algebraic identity, not a differential equation, we use `solve`, not `dsolve`.

The result from `solve` is a list of solutions.  In this case, [we have reason to expect only one solution](https://en.wikipedia.org/wiki/Picard%E2%80%93Lindel%C3%B6f_theorem), but we still get a list, so we have to use the bracket operator, `[0]`, to select the first one.

```python
solutions = solve(Eq(at_0, p_0), C1)
type(solutions), len(solutions)
```

    (list, 1)

```python
value_of_C1 = solutions[0]
value_of_C1
```

Now in the general solution, we want to replace `C1` with the value of `C1` we just figured out.

```python
particular = general.subs(C1, value_of_C1)
particular
```

The result is complicated, but SymPy provides a method that tries to simplify it.

```python
particular = simplify(particular)
particular
```

Often simplicity is in the eye of the beholder, but that's about as simple as this expression gets.

Just to double-check, we can evaluate it at `t=0` and confirm that we get `p_0`

```python
particular.subs(t, 0)
```

This solution is called the [logistic function](https://en.wikipedia.org/wiki/Population_growth#Logistic_equation).

In some places you'll see it written in a different form:

$f(t) = \frac{K}{1 + A e^{-rt}}$

where $A = (K - p_0) / p_0$.

We can use SymPy to confirm that these two forms are equivalent.  First we represent the alternative version of the logistic function:

```python
A = (K - p_0) / p_0
A
```

```python
logistic = K / (1 + A * exp(-r*t))
logistic
```

To see whether two expressions are equivalent, we can check whether their difference simplifies to 0.

```python
simplify(particular - logistic)
```

This test only works one way: if SymPy says the difference reduces to 0, the expressions are definitely equivalent (and not just numerically close).

But if SymPy can't find a way to simplify the result to 0, that doesn't necessarily mean there isn't one.  Testing whether two expressions are equivalent is a surprisingly hard problem; in fact, there is no algorithm that can solve it in general.

### Exercises

**Exercise:** Solve the quadratic growth equation using the alternative parameterization

$\frac{df(t)}{dt} = \alpha f(t) + \beta f^2(t) $

```python
# Solution

alpha, beta = symbols('alpha beta')
```

```python
# Solution

eq3 = Eq(diff(f(t), t), alpha*f(t) + beta*f(t)**2)
eq3
```

```python
# Solution

solution_eq = dsolve(eq3)
solution_eq
```

```python
# Solution

general = solution_eq.rhs
general
```

```python
# Solution

at_0 = general.subs(t, 0)
```

```python
# Solution

solutions = solve(Eq(at_0, p_0), C1)
value_of_C1 = solutions[0]
value_of_C1
```

```python
# Solution

particular = general.subs(C1, value_of_C1)
particular.simplify()
```

**Exercise:**  Use [WolframAlpha](https://www.wolframalpha.com/) to solve the quadratic growth model, using either or both forms of parameterization:

    df(t) / dt = alpha f(t) + beta f(t)^2

or

    df(t) / dt = r f(t) (1 - f(t)/K)

Find the general solution and also the particular solution where `f(0) = p_0`.

```python

```


# Symbolically Understanding Christoffel Symbol and Riemann Curvature Tensor using EinsteinPy

```python
import sympy
from einsteinpy.symbolic import MetricTensor, ChristoffelSymbols, RiemannCurvatureTensor

sympy.init_printing()  # enables the best printing available in an environment
```

### Defining the metric tensor for 3d spherical coordinates

```python
syms = sympy.symbols('r theta phi')
# define the metric for 3d spherical coordinates
metric = [[0 for i in range(3)] for i in range(3)]
metric[0][0] = 1
metric[1][1] = syms[0]**2
metric[2][2] = (syms[0]**2)*(sympy.sin(syms[1])**2)
# creating metric object
m_obj = MetricTensor(metric, syms)
m_obj.tensor()
```

### Calculating the christoffel symbols

```python
ch = ChristoffelSymbols.from_metric(m_obj)
ch.tensor()
```

```python
ch.tensor()[1,1,0]
```

### Calculating the Riemann Curvature tensor

```python
# Calculating Riemann Tensor from Christoffel Symbols
rm1 = RiemannCurvatureTensor.from_christoffels(ch)
rm1.tensor()
```

```python
# Calculating Riemann Tensor from Metric Tensor
rm2 = RiemannCurvatureTensor.from_metric(m_obj)
rm2.tensor()
```

### Calculating the christoffel symbols for Schwarzschild Spacetime Metric
  - The expressions are unsimplified

```python
syms = sympy.symbols("t r theta phi")
G, M, c, a = sympy.symbols("G M c a")
# using metric values of schwarschild space-time
# a is schwarzschild radius
list2d = [[0 for i in range(4)] for i in range(4)]
list2d[0][0] = 1 - (a / syms[1])
list2d[1][1] = -1 / ((1 - (a / syms[1])) * (c ** 2))
list2d[2][2] = -1 * (syms[1] ** 2) / (c ** 2)
list2d[3][3] = -1 * (syms[1] ** 2) * (sympy.sin(syms[2]) ** 2) / (c ** 2)
sch = MetricTensor(list2d, syms)
sch.tensor()
```

```python
# single substitution
subs1 = sch.subs(a,0)
subs1.tensor()
```

```python
# multiple substitution
subs2 = sch.subs([(a,0), (c,1)])
subs2.tensor()
```

```python
sch_ch = ChristoffelSymbols.from_metric(sch)
sch_ch.tensor()
```

### Calculating the simplified expressions

```python
simplified = sch_ch.simplify()
simplified
```

docs/source/examples/Symbolically Understanding Christoffel Symbol and Riemann Curvature Tensor using EinsteinPy.ipynb