```python
"""
Solution of 1D differential equation by linear combination of
basis functions in function spaces and a variational formulation
of the differential equation problem.
"""
import sympy as sym
import numpy as np
import mpmath
import matplotlib.pyplot as plt

def solver(integrand_lhs, integrand_rhs, psi, Omega,
           boundary_lhs=None, boundary_rhs=None,
           symbolic=True, verbose=False):
    """
    psi: dictionary of lists, psi[0] holdes the basis functions,
    psi[1] holdes the first-order derivatives, and psi[2] the
    second-order derivatives (and so on), as symbolic expressions.
    integrand_lhs and integrand_rhs are functions of psi
    defining the integrands in integrals over Omega in the variational
    formulation. boundary_lhs/rhs are similar functions defining
    contributions from the boundary (boundary integrals, which are point
    values in 1D).

    if symbolic is False, all integrals are calculated by mpmath.quad
    to high precision.
    if verbose is True, integrations and linear system A*c=b are printed
    during the computations.
    """
    N = len(psi[0]) - 1
    A = sym.zeros(N+1, N+1)
    b = sym.zeros(N+1, 1)
    x = sym.Symbol('x')
    print('...evaluating matrix...', end=' ')
    for i in range(N+1):
        for j in range(i, N+1):
            integrand = integrand_lhs(psi, i, j)
            if verbose:
                print('(%d,%d):' % (i, j), integrand)
            if symbolic:
                I = sym.integrate(integrand, (x, Omega[0], Omega[1]))
                if isinstance(I, sym.Integral):
                    symbolic = False  # force numerical integration hereafter
                    print('numerical integration of', integrand)
            if not symbolic:
                integrand_ = sym.lambdify([x], integrand, 'mpmath')
                try:
                    I = mpmath.quad(integrand_, [Omega[0], Omega[1]])
                except NameError as e:
                    raise NameError('Numerical integration of\n%s\nrequires symbol %s to be given a value' %
                                    (integrand, str(e).split()[2]))
            if boundary_lhs is not None:
                I += boundary_lhs(psi, i, j)
            A[i,j] = A[j,i] = I
        integrand = integrand_rhs(psi, i)
        if verbose:
            print('rhs:', integrand)
        if symbolic:
            I = sym.integrate(integrand, (x, Omega[0], Omega[1]))
            if isinstance(I, sym.Integral):
                symbolic = False
                print('numerical integration of', integrand)
        if not symbolic:
            integrand_ = sym.lambdify([x], integrand, 'mpmath')
            try:
                I = mpmath.quad(integrand_, [Omega[0], Omega[1]])
            except NameError as e:
                raise NameError('Numerical integration of\n%s\nrequires symbol %s to be given a value' %
                                (integrand, str(e).split()[2]))
        if boundary_rhs is not None:
            I += boundary_rhs(psi, i)
        b[i,0] = I
    print()
    if verbose: print('A:\n', A, '\nb:\n', b)
    c = A.LUsolve(b)
    #c = mpmath.lu_solve(A, b)
    c = [c[i,0] for i in range(c.shape[0])]
    if verbose: print('coeff:', c)
    u = 0
    for i in range(len(psi[0])):
        u += c[i]*psi[0][i]
    if verbose: print('approximation:', u)
    return u, c

def collocation(term_lhs, term_rhs, psi, points):
    """
    Solve a differential equation by collocation. term_lhs is
    a function of psi (dict of basis functions and their derivatives)
    and points (the collocation points throughout the domain)
    as well as i and j (the matrix index) returning elements in the
    coefficient matrix, while term_rhs is a function of psi, i and
    points returning the element i in the right-hand side vector.
    Note that the given psi is transformed to Python functions through
    sym.lambdify such that term_lhs and term_rhs can simply evaluate
    psi[0][i], ... at a point.
    """
    N = len(psi[0]) - 1
    A = sym.zeros(N+1, N+1)
    b = sym.zeros(N+1, 1)
    # Wrap psi in Python functions (psi_) rather than expressions
    # so that we can evaluate psi_ at points[i] (alternative to subs?)
    x = sym.Symbol('x')
    psi_ = {}
    module = "numpy" if N > 2 else "sympy"
    for derivative in psi:
        psi_[derivative] = [sym.lambdify([x], psi[derivative][i],
                                        modules="sympy")
                            for i in range(N+1)]
    print('...evaluating matrix...', end=' ')
    for i in range(N+1):
        for j in range(N+1):
            print('(%d,%d)' % (i, j))
            A[i,j] = term_lhs(psi_, points, i, j)
        b[i,0] = term_rhs(psi_, points, i)
    print()

    # Drop symbolic expressions (and symbolic solve) for
    # all but the smallest problems (troubles maybe caused by
    # derivatives of psi that trigger full symbolic expressions
    # in A; this problem is not evident in interpolation in approx1D.py)
    if N > 2:
        A = A.evalf()
        b = b.evalf()
    print('A:\n', A, '\nb:\n', b)
    c = A.LUsolve(b)
    print('coeff:', c)
    u = 0
    for i in range(len(psi_[0])):
        u += c[i,0]*psi_[0][i](x)
    print('approximation:', u)
    return u

def comparison_plot(u, Omega, u_e=None, filename='tmp.eps',
                    plot_title='', ymin=None, ymax=None):
    x = sym.Symbol('x')
    u = sym.lambdify([x], u, modules="numpy")
    if len(Omega) != 2:
        raise ValueError('Omega=%s must be an interval (2-list)' % str(Omega))
    # When doing symbolics, Omega can easily contain symbolic expressions,
    # assume .evalf() will work in that case to obtain numerical
    # expressions, which then must be converted to float before calling
    # linspace below
    if not isinstance(Omega[0], (int,float)):
        Omega[0] = float(Omega[0].evalf())
    if not isinstance(Omega[1], (int,float)):
        Omega[1] = float(Omega[1].evalf())

    resolution = 401  # no of points in plot
    xcoor = np.linspace(Omega[0], Omega[1], resolution)
    # Vectorized functions expressions does not work with
    # lambdify'ed functions without the modules="numpy"
    approx = u(xcoor)
    plt.plot(xcoor, approx)
    legends = ['approximation']
    if u_e is not None:
        exact  = u_e(xcoor)
        plt.plot(xcoor, exact)
        legends = ['exact']
    plt.legend(legends)
    plt.title(plot_title)
    plt.xlabel('x')
    if ymin is not None and ymax is not None:
        plt.axis([xcoor[0], xcoor[-1], ymin, ymax])
    plt.savefig(filename)

#if __name__ == '__main__':
    #print('Module file not meant for execution.')

```

```python

```

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


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, G.F. Forsyth, C. Cooper. Based on [CFDPython](https://github.com/barbagroup/CFDPython), (c)2013 L.A. Barba, also under CC-BY license.

# Space & Time

## Burgers' Equation

Hi there! We have reached the final lesson of the series *Space and Time — Introduction to Finite-difference solutions of PDEs*, the second module of ["Practical Numerical Methods with Python"](https://openedx.seas.gwu.edu/courses/course-v1:MAE+MAE6286+2017/about).

We have learned about the finite-difference solution for the linear and non-linear convection equations and the diffusion equation. It's time to combine all these into one: *Burgers' equation*. The wonders of *code reuse*!

Before you continue, make sure you have completed the previous lessons of this series, it will make your life easier. You should have written your own versions of the codes in separate, clean Jupyter Notebooks or Python scripts.

You can read about Burgers' Equation on its [wikipedia page](http://en.wikipedia.org/wiki/Burgers'_equation).
Burgers' equation in one spatial dimension looks like this:

$$
\begin{equation}
\frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} = \nu \frac{\partial ^2u}{\partial x^2}
\end{equation}
$$

As you can see, it is a combination of non-linear convection and diffusion. It is surprising how much you learn from this neat little equation!

We can discretize it using the methods we've already detailed in the previous notebooks of this module.  Using forward difference for time, backward difference for space and our 2nd-order method for the second derivatives yields:

$$
\begin{equation}
\frac{u_i^{n+1}-u_i^n}{\Delta t} + u_i^n \frac{u_i^n - u_{i-1}^n}{\Delta x} = \nu \frac{u_{i+1}^n - 2u_i^n + u_{i-1}^n}{\Delta x^2}
\end{equation}
$$

As before, once we have an initial condition, the only unknown is $u_i^{n+1}$. We will step in time as follows:

$$
\begin{equation}
u_i^{n+1} = u_i^n - u_i^n \frac{\Delta t}{\Delta x} (u_i^n - u_{i-1}^n) + \nu \frac{\Delta t}{\Delta x^2}(u_{i+1}^n - 2u_i^n + u_{i-1}^n)
\end{equation}
$$

### Initial and Boundary Conditions

To examine some interesting properties of Burgers' equation, it is helpful to use different initial and boundary conditions than we've been using for previous steps.

The initial condition for this problem is going to be:

$$
\begin{eqnarray}
u &=& -\frac{2 \nu}{\phi} \frac{\partial \phi}{\partial x} + 4 \\\
\phi(t=0) = \phi_0 &=& \exp \bigg(\frac{-x^2}{4 \nu} \bigg) + \exp \bigg(\frac{-(x-2 \pi)^2}{4 \nu} \bigg)
\end{eqnarray}
$$

This has an analytical solution, given by:

$$
\begin{eqnarray}
u &=& -\frac{2 \nu}{\phi} \frac{\partial \phi}{\partial x} + 4 \\\
\phi &=& \exp \bigg(\frac{-(x-4t)^2}{4 \nu (t+1)} \bigg) + \exp \bigg(\frac{-(x-4t -2 \pi)^2}{4 \nu(t+1)} \bigg)
\end{eqnarray}
$$

The boundary condition will be:

$$
\begin{equation}
u(0) = u(2\pi)
\end{equation}
$$

This is called a *periodic* boundary condition. Pay attention! This will cause you a bit of headache if you don't tread carefully.

### Saving Time with SymPy

The initial condition we're using for Burgers' Equation can be a bit of a pain to evaluate by hand.  The derivative $\frac{\partial \phi}{\partial x}$ isn't too terribly difficult, but it would be easy to drop a sign or forget a factor of $x$ somewhere, so we're going to use SymPy to help us out.

[SymPy](http://sympy.org/en/) is the symbolic math library for Python.  It has a lot of the same symbolic math functionality as Mathematica with the added benefit that we can easily translate its results back into our Python calculations (it is also free and open source).

Start by loading the SymPy library, together with our favorite library, NumPy.

```python
import numpy
import sympy
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

We're also going to tell SymPy that we want all of its output to be rendered using $\LaTeX$. This will make our Notebook beautiful!

```python
sympy.init_printing()
```

Start by setting up symbolic variables for the three variables in our initial condition.  It's important to recognize that once we've defined these symbolic variables, they function differently than "regular" Python variables.

If we type `x` into a code block, we'll get an error:

```python
x
```

`x` is not defined, so this shouldn't be a surprise.  Now, let's set up `x` as a *symbolic* variable:

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

Now let's see what happens when we type `x` into a code cell:

```python
x
```

The value of `x` is $x$.  Sympy is also referred to as a computer algebra system -- normally the value of `5*x` will return the product of `5` and whatever value `x` is pointing to.  But, if we define `x` as a symbol, then something else happens:

```python
5 * x
```

This will let us manipulate an equation with unknowns using Python!  Let's start by defining symbols for $x$, $\nu$ and $t$ and then type out the full equation for $\phi$.  We should get a nicely rendered version of our $\phi$ equation.

```python
x, nu, t = sympy.symbols('x nu t')
phi = (sympy.exp(-(x - 4 * t)**2 / (4 * nu * (t + 1))) +
       sympy.exp(-(x - 4 * t - 2 * sympy.pi)**2 / (4 * nu * (t + 1))))
phi
```

It's maybe a little small, but that looks right.  Now to evaluate our partial derivative $\frac{\partial \phi}{\partial x}$ is a trivial task.  To take a derivative with respect to $x$, we can just use:

```python
phiprime = phi.diff(x)
phiprime
```

If you want to see the non-rendered version, just use the Python print command.

```python
print(phiprime)
```

    -(-8*t + 2*x)*exp(-(-4*t + x)**2/(4*nu*(t + 1)))/(4*nu*(t + 1)) - (-8*t + 2*x - 4*pi)*exp(-(-4*t + x - 2*pi)**2/(4*nu*(t + 1)))/(4*nu*(t + 1))

### Now what?

Now that we have the Pythonic version of our derivative, we can finish writing out the full initial condition equation and then translate it into a usable Python expression.  For this, we'll use the *lambdify* function, which takes a SymPy symbolic equation and turns it into a callable function.

```python
from sympy.utilities.lambdify import lambdify

u = -2 * nu * (phiprime / phi) + 4
print(u)
```

    -0.14*(-(-8*t + 2*x)*exp(-(-4*t + x)**2/(4*nu*(t + 1)))/(4*nu*(t + 1)) - (-8*t + 2*x - 4*pi)*exp(-(-4*t + x - 2*pi)**2/(4*nu*(t + 1)))/(4*nu*(t + 1)))/(exp(-(-4*t + x - 2*pi)**2/(4*nu*(t + 1))) + exp(-(-4*t + x)**2/(4*nu*(t + 1)))) + 4

### Lambdify

To lambdify this expression into a usable function, we tell lambdify which variables to request and the function we want to plug them into.

```python
u_lamb = lambdify((t, x, nu), u)
print('The value of u at t=1, x=4, nu=3 is {}'.format(u_lamb(1, 4, 3)))
```

    The value of u at t=1, x=4, nu=3 is 3.49170664206445

### Back to Burgers' Equation

Now that we have the initial conditions set up, we can proceed and finish setting up the problem.  We can generate the plot of the initial condition using our lambdify-ed function.

```python
# Set parameters.
nx = 101  # number of spatial grid points
L = 2.0 * numpy.pi  # length of the domain
dx = L / (nx - 1)  # spatial grid size
nu = 0.07  # viscosity
nt = 100  # number of time steps to compute
sigma = 0.1  # CFL limit
dt = sigma * dx**2 / nu  # time-step size

# Discretize the domain.
x = numpy.linspace(0.0, L, num=nx)
```

We have a function `u_lamb` but we need to create an array `u0` with our initial conditions.  `u_lamb` will return the value for any given time $t$, position $x$ and $nu$.  We can use a `for`-loop to cycle through values of `x` to generate the `u0` array.  That code would look something like this:

```Python
u0 = numpy.empty(nx)

for i, x0 in enumerate(x):
    u0[i] = u_lamb(t, x0, nu)
```

But there's a cleaner, more beautiful way to do this -- *list comprehension*.

We can create a list of all of the appropriate `u` values by typing

```Python
[u_lamb(t, x0, nu) for x0 in x]
```

You can see that the syntax is similar to the `for`-loop, but it only takes one line.  Using a list comprehension will create... a list.  This is different from an *array*, but converting a list to an array is trivial using `numpy.asarray()`.

With the list comprehension in place, the three lines of code above become one:

```Python
u = numpy.asarray([u_lamb(t, x0, nu) for x0 in x])
```

```python
# Set initial conditions.
t = 0.0
u0 = numpy.array([u_lamb(t, xi, nu) for xi in x])
u0
```

    array([4.        , 4.06283185, 4.12566371, 4.18849556, 4.25132741,
           4.31415927, 4.37699112, 4.43982297, 4.50265482, 4.56548668,
           4.62831853, 4.69115038, 4.75398224, 4.81681409, 4.87964594,
           4.9424778 , 5.00530965, 5.0681415 , 5.13097336, 5.19380521,
           5.25663706, 5.31946891, 5.38230077, 5.44513262, 5.50796447,
           5.57079633, 5.63362818, 5.69646003, 5.75929189, 5.82212374,
           5.88495559, 5.94778745, 6.0106193 , 6.07345115, 6.136283  ,
           6.19911486, 6.26194671, 6.32477856, 6.38761042, 6.45044227,
           6.51327412, 6.57610598, 6.63893783, 6.70176967, 6.76460125,
           6.82742866, 6.89018589, 6.95176632, 6.99367964, 6.72527549,
           4.        , 1.27472451, 1.00632036, 1.04823368, 1.10981411,
           1.17257134, 1.23539875, 1.29823033, 1.36106217, 1.42389402,
           1.48672588, 1.54955773, 1.61238958, 1.67522144, 1.73805329,
           1.80088514, 1.863717  , 1.92654885, 1.9893807 , 2.05221255,
           2.11504441, 2.17787626, 2.24070811, 2.30353997, 2.36637182,
           2.42920367, 2.49203553, 2.55486738, 2.61769923, 2.68053109,
           2.74336294, 2.80619479, 2.86902664, 2.9318585 , 2.99469035,
           3.0575222 , 3.12035406, 3.18318591, 3.24601776, 3.30884962,
           3.37168147, 3.43451332, 3.49734518, 3.56017703, 3.62300888,
           3.68584073, 3.74867259, 3.81150444, 3.87433629, 3.93716815,
           4.        ])

Now that we have the initial conditions set up, we can plot it to see what $u(x,0)$ looks like:

```python
# Plot the initial conditions.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.title('Initial conditions')
pyplot.xlabel('x')
pyplot.ylabel('u')
pyplot.grid()
pyplot.plot(x, u0, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 10.0);
```

This is definitely not the hat function we've been dealing with until now. We call it a "saw-tooth function".  Let's proceed forward and see what happens.

### Periodic Boundary Conditions

We will implement Burgers' equation with *periodic* boundary conditions.  If you experiment with the linear and non-linear convection notebooks and make the simulation run longer (by increasing `nt`) you will notice that the wave will keep moving to the right until it no longer even shows up in the plot.

With periodic boundary conditions, when a point gets to the right-hand side of the frame, it *wraps around* back to the front of the frame.

Recall the discretization that we worked out at the beginning of this notebook:

$$
\begin{equation}
u_i^{n+1} = u_i^n - u_i^n \frac{\Delta t}{\Delta x} (u_i^n - u_{i-1}^n) + \nu \frac{\Delta t}{\Delta x^2}(u_{i+1}^n - 2u_i^n + u_{i-1}^n)
\end{equation}
$$

What does $u_{i+1}^n$ *mean* when $i$ is already at the end of the frame?

Think about this for a minute before proceeding.

```python
# Integrate the Burgers' equation in time.
u = u0.copy()
for n in range(nt):
    un = u.copy()
    # Update all interior points.
    u[1:-1] = (un[1:-1] -
               un[1:-1] * dt / dx * (un[1:-1] - un[:-2]) +
               nu * dt / dx**2 * (un[2:] - 2 * un[1:-1] + un[:-2]))
    # Update boundary points.
    u[0] = (un[0] -
            un[0] * dt / dx * (un[0] - un[-1]) +
            nu * dt / dx**2 * (un[1] - 2 * un[0] + un[-1]))
    u[-1] = (un[-1] -
            un[-1] * dt / dx * (un[-1] - un[-2]) +
            nu * dt / dx**2 * (un[0] - 2 * un[-1] + un[-2]))
```

```python
# Compute the analytical solution.
u_analytical = numpy.array([u_lamb(nt * dt, xi, nu) for xi in x])
```

```python
# Plot the numerical solution along with the analytical solution.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('x')
pyplot.ylabel('u')
pyplot.grid()
pyplot.plot(x, u, label='Numerical',
            color='C0', linestyle='-', linewidth=2)
pyplot.plot(x, u_analytical, label='Analytical',
            color='C1', linestyle='--', linewidth=2)
pyplot.legend()
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 10.0);
```

Let's now create an animation with the `animation` module of Matplotlib to observe how the numerical solution changes over time compared to the analytical solution.
We start by importing the module from Matplotlib as well as the special `HTML` display method.

```python
from matplotlib import animation
from IPython.display import HTML
```

We create a function `burgers` to computes the numerical solution of the 1D Burgers' equation over time.
(The function returns the history of the solution: a list with `nt` elements, each one being the solution in the domain at a time step.)

```python
def burgers(u0, dx, dt, nu, nt=20):
    """
    Computes the numerical solution of the 1D Burgers' equation
    over the time steps.

    Parameters
    ----------
    u0 : numpy.ndarray
        The initial conditions as a 1D array of floats.
    dx : float
        The grid spacing.
    dt : float
        The time-step size.
    nu : float
        The viscosity.
    nt : integer, optional
        The number of time steps to compute;
        default: 20.

    Returns
    -------
    u_hist : list of numpy.ndarray objects
        The history of the numerical solution.
    """
    u_hist = [u0.copy()]
    u = u0.copy()
    for n in range(nt):
        un = u.copy()
        # Update all interior points.
        u[1:-1] = (un[1:-1] -
                   un[1:-1] * dt / dx * (un[1:-1] - un[:-2]) +
                   nu * dt / dx**2 * (un[2:] - 2 * un[1:-1] + un[:-2]))
        # Update boundary points.
        u[0] = (un[0] -
                un[0] * dt / dx * (un[0] - un[-1]) +
                nu * dt / dx**2 * (un[1] - 2 * un[0] + un[-1]))
        u[-1] = (un[-1] -
                un[-1] * dt / dx * (un[-1] - un[-2]) +
                nu * dt / dx**2 * (un[0] - 2 * un[-1] + un[-2]))
        u_hist.append(u.copy())
    return u_hist
```

```python
# Compute the history of the numerical solution.
u_hist = burgers(u0, dx, dt, nu, nt=nt)
```

```python
# Compute the history of the analytical solution.
u_analytical = [numpy.array([u_lamb(n * dt, xi, nu) for xi in x])
                for n in range(nt)]
```

```python
fig = pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('x')
pyplot.ylabel('u')
pyplot.grid()
u0_analytical = numpy.array([u_lamb(0.0, xi, nu) for xi in x])
line1 = pyplot.plot(x, u0, label='Numerical',
                    color='C0', linestyle='-', linewidth=2)[0]
line2 = pyplot.plot(x, u0_analytical, label='Analytical',
                    color='C1', linestyle='--', linewidth=2)[0]
pyplot.legend()
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 10.0)
fig.tight_layout()
```

```python
def update_plot(n, u_hist, u_analytical):
    """
    Update the lines y-data of the Matplotlib figure.

    Parameters
    ----------
    n : integer
        The time-step index.
    u_hist : list of numpy.ndarray objects
        The history of the numerical solution.
    u_analytical : list of numpy.ndarray objects
        The history of the analytical solution.
    """
    fig.suptitle('Time step {:0>2}'.format(n))
    line1.set_ydata(u_hist[n])
    line2.set_ydata(u_analytical[n])
```

```python
# Create an animation.
anim = animation.FuncAnimation(fig, update_plot,
                               frames=nt, fargs=(u_hist, u_analytical),
                               interval=100)
```

```python
# Display the video.
HTML(anim.to_html5_video())
```

## Array Operation Speed Increase

Coding up discretization schemes using array operations can be a bit of a pain.  It requires much more mental effort on the front-end than using two nested `for` loops.  So why do we do it?  Because it's fast.  Very, very fast.

Here's what the Burgers code looks like using two nested `for` loops.  It's easier to write out, plus we only have to add one "special" condition to implement the periodic boundaries.

At the top of the cell, you'll see the decorator `%%timeit`.
This is called a "cell magic".  It runs the cell several times and returns the average execution time for the contained code.

Let's see how long the nested `for` loops take to finish.

```python
%%timeit
# Set initial conditions.
u = numpy.array([u_lamb(t, x0, nu) for x0 in x])
# Integrate in time using a nested for loop.
for n in range(nt):
    un = u.copy()
    # Update all interior points and the left boundary point.
    for i in range(nx - 1):
        u[i] = (un[i] -
                un[i] * dt / dx *(un[i] - un[i - 1]) +
                nu * dt / dx**2 * (un[i + 1] - 2 * un[i] + un[i - 1]))
    # Update the right boundary.
    u[-1] = (un[-1] -
             un[-1] * dt / dx * (un[-1] - un[-2]) +
             nu * dt / dx**2 * (un[0]- 2 * un[-1] + un[-2]))
```

    41.4 ms ± 103 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Less than 50 milliseconds.  Not bad, really.

Now let's look at the array operations code cell.  Notice that we haven't changed anything, except we've added the `%%timeit` magic and we're also resetting the array `u` to its initial conditions.

This takes longer to code and we have to add two special conditions to take care of the periodic boundaries.  Was it worth it?

```python
%%timeit
# Set initial conditions.
u = numpy.array([u_lamb(t, xi, nu) for xi in x])
# Integrate in time using array operations.
for n in range(nt):
    un = u.copy()
    # Update all interior points.
    u[1:-1] = (un[1:-1] -
               un[1:-1] * dt / dx * (un[1:-1] - un[:-2]) +
               nu * dt / dx**2 * (un[2:] - 2 * un[1:-1] + un[:-2]))
    # Update boundary points.
    u[0] = (un[0] -
            un[0] * dt / dx * (un[0] - un[-1]) +
            nu * dt / dx**2 * (un[1] - 2 * un[0] + un[-1]))
    u[-1] = (un[-1] -
            un[-1] * dt / dx * (un[-1] - un[-2]) +
            nu * dt / dx**2 * (un[0] - 2 * un[-1] + un[-2]))
```

    4.28 ms ± 197 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Yes, it is absolutely worth it.  That's a nine-fold speed increase.  For this exercise, you probably won't miss the extra 40 milliseconds if you use the nested `for` loops, but what about a simulation that has to run through millions and millions of iterations?  Then that little extra effort at the beginning will definitely pay off.

---

###### The cell below loads the style of the notebook.

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
    font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
    font-family: "Source Code Pro";
    font-size: 90%;
}

.alert-box {
    padding:10px 10px 10px 36px;
    margin:5px;
}

.success {
    color:#666600;
    background:rgb(240,242,229);
}
</style>

```python

```


# Лабораторая работа 4.03V
# Кольца Ньютона

## Выполнил: Коняхин Всеволод Владимирович, M32051, Вариант №10

## Краткие теоретические сведения
В этой работе рассматривается интерференционная картина колец Ньютона, получаемая методом деления амплитуд, когда возникает оптическая разность хода.

## Цель работы
Изучение интерференционной картины колец Ньютона. Определение радиуса кривизны плоско-выпуклой линзы с помощью
интерференционной картины колец Ньютона.

```python
import sympy
import scipy
import numpy as np
import pandas as pd
from scipy.signal import argrelextrema
import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = (10,5)
%matplotlib inline
```

## Монохроматический источник, показатель преломления среды $n_1$

Длина волны $\lambda_1 = 645 $ нм, показатель преломления среды $n_1 = 1.4$

```python
lambd = 645 * 10 ** (-9)
n_1 = 1.4
```

#### Вирутальная установка:

#### Интенсивность от радиальной координаты $I_1(r)$

```python
file_n1 = 'data/intensity_n1.csv'
n1_df = pd.read_csv(file_n1, sep=';', index_col=False)
```

```python
I = np.array([float(element.replace(',', '.')) for element in np.array(n1_df['I'])])
r = np.array([float(element.replace(',', '.')) for element in np.array(n1_df['r, mm'])])
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Интенсивность от радиальной координаты $I_1(r)$')
ax.set_ylabel('Интенсивность')
ax.set_xlabel('Радиальная координата, мм')
ax.plot(r, I)
plt.show()
```

#### Определим радиусы светлых и темных колец:

```python
# найдем локальные минимумы и максимумы
maxm = argrelextrema(I, np.greater)
minm = argrelextrema(I, np.less)
```

```python
dark_rings_radiuses = r[minm]
light_rings_radiuses = r[maxm]
```

#### Радиусы светлых колец:

```python
for r in light_rings_radiuses:
    print(str(r) + ' мм', end=', ')
```

    0.48 мм, 0.8325 мм, 1.0725 мм, 1.27 мм, 1.44 мм, 1.5925 мм, 1.73 мм, 1.86 мм, 1.98 мм, 2.0925 мм, 2.2 мм, 2.3025 мм, 2.4 мм, 2.495 мм,

#### Радиусы темных колец:

```python
for r in dark_rings_radiuses:
    print(str(r) + ' мм', end=', ')
```

    0.68 мм, 0.96 мм, 1.175 мм, 1.3575 мм, 1.5175 мм, 1.6625 мм, 1.795 мм, 1.92 мм, 2.0375 мм, 2.1475 мм, 2.25 мм, 2.3525 мм, 2.4475 мм,

#### Построение графиков зависимостей квадратов радиусов колец от порядкового номера:

```python
dark_rings_radiuses_squared = dark_rings_radiuses ** 2
light_rings_radiuses_squared = light_rings_radiuses ** 2
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Квадрат радиуса темного кольца от порядкового номера')
ax.set_ylabel('$R^2$')
ax.set_xlabel('Порядковый номер')
k, b = np.polyfit([i for i in range(1, len(dark_rings_radiuses_squared) + 1)], dark_rings_radiuses_squared, 1)
x = np.linspace(-b/k, 13.5, 6)
ax.scatter([i for i in range(1, len(dark_rings_radiuses_squared) + 1)], dark_rings_radiuses_squared)
ax.plot(x, np.polyval([k, b], x), 'r--')
plt.show()
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Квадрат радиуса светлого кольца от порядкового номера')
ax.set_ylabel('$R^2$')
ax.set_xlabel('Порядковый номер')
k, b = np.polyfit([i for i in range(1, len(light_rings_radiuses_squared) + 1)], light_rings_radiuses_squared, 1)
x = np.linspace(-b/k, 14.5, 6)
ax.scatter([i for i in range(1, len(light_rings_radiuses_squared) + 1)], light_rings_radiuses_squared)
ax.plot(x, np.polyval([k, b], x), 'r--')
plt.show()
```

Как видно из графиков, получилась линейная зависимость!

#### Расчет радиуса кривизны линзы:

Формула: $R = \frac{r_m^2 - r_n^2}{(m - n) \lambda}$, где $m$ и $n$ различные порядки интерференции для темных колец

```python
def calculate_сurvature_radius(r_m, r_n, m, n, lambd):
    radius = (r_m ** 2 - r_n ** 2) / ((m - n) * lambd)
    return radius

def calculate_сurvature_radius_range(dark_rings_radiuses, lambd):
    n_rings = len(dark_rings_radiuses)
    radiuses = []

    for start in range(n_rings):
        for end in range(start + 1, n_rings):
            rad = calculate_сurvature_radius(
                dark_rings_radiuses[start] * 10 ** (-3),
                dark_rings_radiuses[end] * 10 ** (-3),
                start, end, lambd)
            radiuses.append(rad)

    return np.mean(radiuses), np.std(radiuses)
```

```python
сurvature_radius_mean, сurvature_radius_std = calculate_сurvature_radius_range(dark_rings_radiuses, lambd)

print('Радиус кривизны линзы: {:.3f} +- {:.3f} м'.format(сurvature_radius_mean, сurvature_radius_std))
```

    Радиус кривизны линзы: 0.715 +- 0.003 м

## Монохроматический источник, показатель преломления среды $n_2$

Длина волны $\lambda_1 = 645 $ нм, показатель преломления среды $n_2 = 1.55$

```python
lambd = 645 * 10 ** (-9)
n_2 = 1.55
```

#### Вирутальная установка:

#### Интенсивность от радиальной координаты $I_2(r)$

```python
file_n2 = 'data/intensity_n2.csv'
n2_df = pd.read_csv(file_n2, sep=';', index_col=False)
```

```python
I = np.array([float(element.replace(',', '.')) for element in np.array(n2_df['I'])])
r = np.array([float(element.replace(',', '.')) for element in np.array(n2_df['r, mm'])])
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Интенсивность от радиальной координаты $I_2(r)$')
ax.set_ylabel('Интенсивность')
ax.set_xlabel('Радиальная координата, мм')
ax.plot(r, I)
plt.show()
```

#### Определим радиусы светлых и темных колец:

```python
# найдем локальные минимумы и максимумы
maxm = argrelextrema(I, np.greater)
minm = argrelextrema(I, np.less)
```

```python
dark_rings_radiuses = r[minm]
light_rings_radiuses = r[maxm]
```

#### Радиусы светлых колец:

```python
for r in light_rings_radiuses:
    print(str(r) + ' мм', end=', ')
```

    0.455 мм, 0.79 мм, 1.02 мм, 1.2075 мм, 1.3675 мм, 1.5125 мм, 1.645 мм, 1.7675 мм, 1.88 мм, 1.9875 мм, 2.09 мм, 2.1875 мм, 2.28 мм, 2.37 мм, 2.4575 мм,

#### Радиусы темных колец:

```python
for r in dark_rings_radiuses:
    print(str(r) + ' мм', end=', ')
```

    0.645 мм, 0.9125 мм, 1.1175 мм, 1.29 мм, 1.4425 мм, 1.58 мм, 1.7075 мм, 1.825 мм, 1.935 мм, 2.04 мм, 2.14 мм, 2.235 мм, 2.325 мм, 2.4125 мм,

#### Построение графиков зависимостей квадратов радиусов колец от порядкового номера:

```python
dark_rings_radiuses_squared = dark_rings_radiuses ** 2
light_rings_radiuses_squared = light_rings_radiuses ** 2
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Квадрат радиуса темного кольца от порядкового номера')
ax.set_ylabel('$R^2$')
ax.set_xlabel('Порядковый номер')
k, b = np.polyfit([i for i in range(1, len(dark_rings_radiuses_squared) + 1)], dark_rings_radiuses_squared, 1)
x = np.linspace(-b/k, 14.5, 6)
ax.scatter([i for i in range(1, len(dark_rings_radiuses_squared) + 1)], dark_rings_radiuses_squared)
ax.plot(x, np.polyval([k, b], x), 'r--')
plt.show()
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Квадрат радиуса светлого кольца от порядкового номера')
ax.set_ylabel('$R^2$')
ax.set_xlabel('Порядковый номер')
k, b = np.polyfit([i for i in range(1, len(light_rings_radiuses_squared) + 1)], light_rings_radiuses_squared, 1)
x = np.linspace(-b/k, 15.5, 6)
ax.scatter([i for i in range(1, len(light_rings_radiuses_squared) + 1)], light_rings_radiuses_squared)
ax.plot(x, np.polyval([k, b], x), 'r--')
plt.show()
```

#### Расчет радиуса кривизны линзы:

```python
сurvature_radius_mean, сurvature_radius_std = calculate_сurvature_radius_range(dark_rings_radiuses, lambd)

print('Радиус кривизны линзы: {:.3f} +- {:.3f} м'.format(сurvature_radius_mean, сurvature_radius_std))
```

    Радиус кривизны линзы: 0.645 +- 0.002 м

## Бихроматическое излучение

Длина волны 1 $\lambda_1 = 645 $ нм, Длина волны 2 $\lambda_2 = 594 $ нм, показатель преломления среды $n_2 = 1.55$

```python
lambda_1 = 645 * 10 ** (-9)
lambda_2 = 594 * 10 ** (-9)
n_2 = 1.55
```

#### Вирутальная установка:

#### Построение графика распределения интенсивности $I_3(r)$:

```python
file_n3 = 'data/intensity_two_waves.csv'
n3_df = pd.read_csv(file_n3, sep=';', index_col=False)
```

```python
I = np.array([float(element.replace(',', '.')) for element in np.array(n3_df['I'])])
r = np.array([float(element.replace(',', '.')) for element in np.array(n3_df['r, mm'])])
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Интенсивность от радиальной координаты $I_3(r)$')
ax.set_ylabel('Интенсивность')
ax.set_xlabel('Радиальная координата, мм')
ax.plot(r, I)
plt.show()
```

#### Значения видности $V(\Delta)$:

$V_{эксп}(r) = \frac{I_{max} - I_{min}}{I_{max} + I_{min}}$

```python
def calculate_experimental_visibility(i_max, i_min):
    visib = (i_max - i_min) / (i_max + i_min)
    return visib
```

$V_{теор}(r) = \alpha * |sinc\left[\frac{\Delta w}{2c} \Delta_{opt}\right]|$
$\Delta_{opt} = \frac{r^2}{R_{lens}} + \frac{\lambda}{2} \approx \frac{r^2}{R_{lens}}$

```python
def calculate_theoretical_visibility(w_range, r, R_lens, alpha=1.):
    light_speed = 299792458
    delta_opt = r ** 2 / R_lens
    visib = alpha * abs(np.sinc(w_range / (2 * light_speed) * delta_opt))
    return visib
```

```python
maxm = argrelextrema(I, np.greater)
minm = argrelextrema(I, np.less)
```

```python
theoretical_points = []
experimental_points = []
```

```python
for min_idx, max_idx in zip(list(minm[0]), list(maxm[0])):
    min_radius, max_radius = r[min_idx], r[max_idx]
    min_intensity, max_intensity = I[min_idx], I[max_idx]

    theor_visib = calculate_theoretical_visibility(
        lambda_1 - lambda_2, (min_radius + max_radius) / 2, сurvature_radius_mean)
    exp_visib = calculate_experimental_visibility(
        max_intensity, min_intensity)

    theoretical_points.append(theor_visib)
    experimental_points.append(exp_visib)
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Видность: экспериментальная (синяя) и теоретическая (красная)')
ax.set_ylabel('Видность')
ax.plot([i for i in range(len(theoretical_points))], theoretical_points, 'r')
ax.plot([i for i in range(len(experimental_points))], experimental_points, 'b')
plt.show()
```

## Однородный сплошной спектр

Длина волны 1 $\lambda_1 = 594 $ нм, Длина волны 2 $\lambda_2 = 645 $ нм, показатель преломления среды $n_2 = 1.55$

```python
lambda_1 = 594 * 10 ** (-9)
lambda_2 = 645 * 10 ** (-9)
n_2 = 1.55
```

#### Вирутальная установка:

```python
file_n4 = 'data/intensity_period.csv'
n4_df = pd.read_csv(file_n4, sep=';', index_col=False)
```

```python
I = np.array([float(element.replace(',', '.')) for element in np.array(n4_df['I'])])
r = np.array([float(element.replace(',', '.')) for element in np.array(n4_df['r, mm'])])
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Интенсивность от радиальной координаты $I_4(r)$')
ax.set_ylabel('Интенсивность')
ax.set_xlabel('Радиальная координата, мм')
ax.plot(r, I)
plt.show()
```

#### Значения видности $V(\Delta)$:

```python
maxm = argrelextrema(I, np.greater)
minm = argrelextrema(I, np.less)
```

```python
theoretical_points = []
experimental_points = []
```

```python
for min_idx, max_idx in zip(list(minm[0]), list(maxm[0])):
    min_radius, max_radius = r[min_idx], r[max_idx]
    min_intensity, max_intensity = I[min_idx], I[max_idx]

    theor_visib = calculate_theoretical_visibility(
        lambda_1 - lambda_2, (min_radius + max_radius) / 2, сurvature_radius_mean)
    exp_visib = calculate_experimental_visibility(
        max_intensity, min_intensity)

    theoretical_points.append(theor_visib)
    experimental_points.append(exp_visib)
```

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Видность: экспериментальная (синяя) и теоретическая (красная)')
ax.set_ylabel('Видность')
ax.plot([i for i in range(len(theoretical_points))], theoretical_points, 'r')
ax.plot([i for i in range(len(experimental_points))], experimental_points, 'b')
plt.show()
```

## Выводы и анализ результатов работы

В ходе работы была рассмотренна интерференционная картина колец Ньютона. В частности, для монохроматического источника с двумя разными показателями преломления среды были построены графики зависимости интенсивности от радиальной координаты, найдены радиусы темных и светлых колец, продемонстрирована линейная зависимость квадрата радиуса колец к порядковому номеру. Так же были найдены радиусы кривизны для линзы. Для бихроматического излучения и однородного сплошного спектра были найдены зависимости интенсивности от радиальной координаты, теоретические и экспериментальные видности.


# Análisis de lazo
__UNLZ - Facultad de Ingeniería__
__Electrotecnia__
__Alumno:__ Daniel Antonio Lorenzo

La ley de la tensión de Kirchhoff (LTK) constituye la base del análisis de lazo.

## Ejemplo 10-13
Determine la corriente $I_0$ en el circuito de la figura, aplicando el análisis de lazo.

### Solución
Al aplicar la LTK al lazo 1 se obtiene

$$ (8 + j10 - j2) I_1 - (-j2)I_2 - (j10) I_3 = 0 \tag{1} $$

En cuanto al lazo 2

$$ (j2) I_1 + (4 - 2j -2j)I_2 + (j2) I_3 + 20 \angle 90^\circ = 0 \tag{2} $$

En cuanto al lazo 3

$$ I_3 = 5 \angle  0^\circ \tag{3} $$

En forma matricial

$$\left[
\begin{array}{cc}
8 + 8j & j2 \\
j2 & 4 - 4j
\end{array}
\right]
\left[
\begin{array}{c}
I_1 \\
I_2
\end{array}
\right]
=
\left[
\begin{array}{c}
j50 \\
-j30
\end{array}
\right]$$

De donde se obtivieron los determinantes

$$ \Delta =
\left|
\begin{array}{cc}
8 + 8j & j2 \\
j2 & 4 - j4
\end{array}
\right|$$

```python
import numpy as np
```

```python
M = np.array([ [8 + 8j , 2j],[2j , 4 - 4j] ])
Delta = np.linalg.det(M)
print('Delta = {:.2f}'.format(Delta))
```

    Delta = 68.00+0.00j

$$ \Delta_1 =
\left|
\begin{array}{cc}
j50 & j2 \\
-j30 & 4 - j4
\end{array}
\right|$$

```python
M1 = np.array([ [50j , 2j],[-30j , 4 - 4j] ])
Delta1 = np.linalg.det(M1)
print('Delta1 = {:.2f}'.format(Delta1))
```

    Delta1 = 140.00+200.00j

$$ \Delta_2 =
\left|
\begin{array}{cc}
8 + j8 & j50 \\
j2 & -j30
\end{array}
\right|$$

```python
M2 = np.array([ [8 + 8j , 50j],[2j , -30j] ])
Delta2 = np.linalg.det(M2)
print('Delta2 = {:.2f}'.format(Delta2))
```

    Delta2 = 340.00-240.00j

$$ I_1 = \frac{\Delta_1}{\Delta} \qquad ; \qquad I_2 = \frac{\Delta_2}{\Delta} $$

```python
import cmath, math
```

```python
I1 = Delta1/Delta
I2 = Delta2/Delta
I1_pol = cmath.polar(I1)
I2_pol = cmath.polar(I2)
```

En coordenadas rectangulares

```python
print('I1 = {:.2f} A'.format(I1))
print('I2 = {:.2f} A'.format(I2))
```

    I1 = 2.06+2.94j A
    I2 = 5.00-3.53j A

En coordenadas polares (radianes)

```python
print('I1_pol = (%.2f<%.2f rad) A'%I1_pol)
print('I2_pol = (%.2f<%.2f rad) A'%I2_pol)
```

    I1_pol = (3.59<0.96 rad) A
    I2_pol = (6.12<-0.61 rad) A

En coordenadas polares (grados)

```python
print('I1_pol = (%.2f<%.2f°) A'%(I1_pol[0],I1_pol[1]*180/math.pi))
print('I2_pol = (%.2f<%.2f°) A'%(I2_pol[0],I2_pol[1]*180/math.pi))
```

    I1_pol = (3.59<55.01°) A
    I2_pol = (6.12<-35.22°) A

La corriente deseada es
$$ I_0 = - I_2 $$

```python
I0 = -I2
I0_pol = cmath.polar(I0)
```

```python
print('En coordenadas rectangulares:')
print('I0 = {:.2f} A'.format(I0))
print('En coordenadas polares (radianes):')
print('I0_pol = (%.2f<%.2f rad) A'%I0_pol)
print('En coordenadas polares (grados):')
print('I0_pol = (%.2f<%.2f°) A'%(I0_pol[0],I0_pol[1]*180/math.pi))
```

    En coordenadas rectangulares:
    I0 = -5.00+3.53j A
    En coordenadas polares (radianes):
    I0_pol = (6.12<2.53 rad) A
    En coordenadas polares (grados):
    I0_pol = (6.12<144.78°) A

```python
%reset -s -f
```

## Problema de práctica 10.3
Halle $I_0$ en la fig. aplicando el análisis de malla.

### Solución
Al aplicar LTK al lazo 1 se obtiene

$$ I_1 = 6 \angle 0^\circ $$

Lazo 2

$$ (8 - 2j + 4j)I_2 - (4j) I_3 = 0 $$

Lazo 3

$$  (-6)I_1 + (-4j)I_2 + (6 + 4j)I_3 - 30 \angle 30^\circ = 0 $$

En forma matricial

$$\left[
\begin{array}{cc}
8 + 2j & -4j \\
-4j & 6 + 4j
\end{array}
\right]
\left[
\begin{array}{c}
I_2 \\
I_3
\end{array}
\right]
=
\left[
\begin{array}{c}
0 \\
36 + 30 \angle 30^\circ
\end{array}
\right] \cong
\left[
\begin{array}{c}
0 \\
61,98 + 15j
\end{array}
\right]$$

```python
import numpy as np
```

```python
import cmath, math
```

$$ \Delta =
\left|
\begin{array}{cc}
8 + 2j & -4j \\
-4j & 6 + 4j
\end{array}
\right|$$

```python
M = np.array( [ [8 + 2j , -4j],[-4j , 6 + 4j] ] )
Delta = np.linalg.det(M)
print('Delta = {:.2f}'.format(Delta))
```

    Delta = 56.00+44.00j

$$ \Delta_2 =
\left|
\begin{array}{cc}
0 & -4j \\
61,98 + 15j & 6 + 4j
\end{array}
\right|$$

```python
Vs = cmath.rect(30,math.pi/6)
M2 = np.array([ [0 , -4j],[36 + Vs , 6 + 4j] ])
Delta2 = np.linalg.det(M2)
print('Delta2 = {:.2f}'.format(Delta2))
```

    Delta2 = -60.00+247.92j

$$ \Delta_3 =
\left|
\begin{array}{cc}
8 + 2j & 0 \\
-4j & 61,98 + 15j
\end{array}
\right|$$

```python
M3 = np.array([ [8 + 2j , 0],[-4j ,  36 + Vs] ])
Delta3 = np.linalg.det(M3)
print('Delta3 = {:.2f}'.format(Delta3))
```

    Delta3 = 465.85+243.96j

$$ I_2 = \frac{\Delta_2}{\Delta} \qquad ; \qquad I_3 = \frac{\Delta_3}{\Delta} $$

```python
I2 = Delta2/Delta
I3 = Delta3/Delta
I2_pol = cmath.polar(I2)
I3_pol = cmath.polar(I3)
```

```python
print('Coordenadas rectangulares:')
print('I2 = {:.2f} A'.format(I2))
print('I3 = {:.2f} A'.format(I3))
```

    Coordenadas rectangulares:
    I2 = 1.49+3.26j A
    I3 = 7.26-1.35j A

```python
print('Coordenadas polares (radianes):')
print('I2_pol = (%.2f<%.3f rad) A'%I2_pol)
print('I3_pol = (%.2f<%.3f rad) A'%I3_pol)
```

    Coordenadas polares (radianes):
    I2_pol = (3.58<1.142 rad) A
    I3_pol = (7.38<-0.184 rad) A

```python
print('Coordenadas polares (grados):')
print('I2_pol = (%.3f<%.2f°) A'%(I2_pol[0],I2_pol[1]*180/math.pi))
print('I3_pol = (%.3f<%.2f°) A'%(I3_pol[0],I3_pol[1]*180/math.pi))
```

    Coordenadas polares (grados):
    I2_pol = (3.582<65.45°) A
    I3_pol = (7.384<-10.52°) A

Entonces, $I_0 = I_2 = (3,582 \angle 65,45^\circ) \, \mathrm{A}$

----------------
### Utilizando sympy para resolvel el sistema de ecuaciones (opcional)

```python
import sympy as sp
```

```python
I1, I2, I3 = sp.symbols('I1 I2 I3')
Vs = cmath.rect(30,math.pi/6)
Eq1 = sp.Eq(I1,6)
Eq2 = sp.Eq( (8 + 2j)*I2 - (4j)*I3 , 0 )
Eq3 = sp.Eq( (6 + 4j)*I3 - (4j)*I2 - 6*I1 - Vs , 0 )
```

```python
Sol = sp.solve([Eq1,Eq2,Eq3],[I1,I2,I3])
```

```python
I2_pol = cmath.polar(Sol[I2])
I3_pol = cmath.polar(Sol[I3])
```

```python
print('Coordenadas rectangulares:')
print('I2 = ',Sol[I2])
print('I3 = ',Sol[I3])
```

    Coordenadas rectangulares:
    I2 =  1.48829142980715 + 3.25782545611819*I
    I3 =  7.25979662713994 - 1.3476701315552*I

```python
print('Coordenadas polares (radianes):')
print('I2 = (%.3f<%.3f rad) A'%I2_pol)
print('I3 = (%.3f<%.3f rad) A'%I3_pol)
```

    Coordenadas polares (radianes):
    I2 = (3.582<1.142 rad) A
    I3 = (7.384<-0.184 rad) A

```python
print('Coordenadas polares (grados):')
print('I2_pol = (%.3f<%.2f°) A'%(I2_pol[0],I2_pol[1]*180/math.pi))
print('I3_pol = (%.3f<%.2f°) A'%(I3_pol[0],I3_pol[1]*180/math.pi))
```

    Coordenadas polares (grados):
    I2_pol = (3.582<65.45°) A
    I3_pol = (7.384<-10.52°) A

Entonces, $I_0 = I_2 = (3,582 \angle 65,45^\circ) \, \mathrm{A}$

```python
%reset -s -f
```

```python
# Esta celda da el estilo al notebook
from IPython.core.display import HTML
css_file = 'styles/aeropython.css'
HTML(open(css_file, "r").read())
```

/* This template is inspired in the one used by Lorena Barba
in the numerical-mooc repository: https://github.com/numerical-mooc/numerical-mooc
We thank her work and hope you also enjoy the look of the notobooks with this style */

/* <link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro|Josefin+Sans:400,700,400italic|Ubuntu+Condensed' rel='stylesheet' type='text/css'>

El estilo se ha aplicado =)

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


# Estimating sample quantiles

# Document

        Title
        Estimating sample quantiles

        Last modified
        2019-04-11

        Author
        Gilles Pilon <gillespilon13@gmail.com>

        Status
        Active

        Type
        Jupyter notebook

        Created
        2018-08-18

        File name
        estimating_sample_quantiles.ipynb

        Other files required
        estimating_sample_quantiles.csv

# In brevi

The purpose of this notebook is to explore the ways that Python calculates quantiles. During the development of the anova_one_factor notebook, I discovered that [Python](https://www.python.org), [LibreOffice](https://www.libreoffice.org), and [Excel](https://office.microsoft.com/excel/) calculate quartines in the same way, but that Minitab and an online source calculate them differently. I've discovered that there are at least eleven ways to calculate quantiles.

In addition, this notebook explores how to replicate Minitab's "graphical summary" report.

# Data

Download the data file.

[estimating_sample_quantiles](https://drive.google.com/open?id=1EuMKLYCh2aLllTQoYSGzXNQZKeexaWTn)

# Methodology

Various data munging operations are performed using pandas.

# Explanation of the eleven methods

Quantiles divide the range of a probability distribution into continuous intervals with equal probabilities, or divide the observations in a sample in the same way [Wikipedia](https://en.wikipedia.org/wiki/Quantile). A sample drawn from an unknown population requires estimating the quantiles. There are twelve known methods that commonly appear in statistical packages. Methods 1-3 are based on rounding. Methods 4-9 are based on linear interpolation.

The data are sorted in increasing order. Each method computes $Q_{\text{p}}$, the estimate for the $k^{th}$ $q$-quantile, where $p = k/q$, from a sample of size $N$ by computing a real-valued index $h$. When $h$ is an integer, the $h^{th}$ smallest of the $N$ values, $x_h$, is the quantile estimate. Otherwise, a rounding or interpolation scheme is used to compute the quantile estimate from $h$, $x_{\lfloor \text{h}\rfloor}$, and $x_{\lceil \text{h}\rceil}$.

Sample quantiles are defined by:

$$
\begin{equation}
Q_p = (1 - \gamma) \space x_j \space + \space \gamma \space x_{\text{j+1}}
\end{equation}
$$

where:

$$
\begin{equation}
x_j \text{ is the } j^{th} \text{ order statistic} \\
\gamma \text{ is a function of } j = \lfloor \text{(n} \times \text{p + m)} \rfloor \\
m = \text{alphap + p } \times \text{(1} - \text{alphap} - \text{betap)} \\
g = \text{n} \times \text{p} + \text{m} - \text{j} \\
\frac{j - m}{n} \leq p \lt \frac{j - m + 1}{n}
\end{equation}
$$

Reinterpreting the above equations to compare to R gives:

$$
\begin{equation}
p(k) = \frac{k - \text{alphap}}{n + 1 - \text{alphap} - \text{betap}}
\end{equation}$$

In this notebook I am particularly interested in the 4-quantiles, called the 1st, 2nd and 3rd quartiles (Q1, Q2. Q3).

The following table is a work in progress, to combine the eleven methods into one table.

Method
Software
$h$
$Q_p$
SciPy (alphap, betap)
Notes

1
R-1, SAS-3
$Np + \frac{1}{2}$
$x_{\lceil \text{h} - \frac{1}{2}\rceil}$

Inverse of empirical cumulative distribution function (CDF). When $p = 0$, use $x_1$.

2
R-2, SAS-5
$Np + \frac{1}{2}$
$\frac{x_{\lceil \text{h} - \frac{1}{2}\rceil} + x_{\lfloor \text{h} + \frac{1}{2}\rfloor}}{2}$

The same as R-1, but with averaging at discontinuities. When $p = 0$, use $x_1$.When $p = 1$, use $x_N$.

3
R-3, SAS-2
$Np$
$x_{\lfloor \text{h}\rceil}$

The observation numbered closest to $Np$ (piecewise linear function). It is also called the nearest even-order statistic. Here, $\lfloor \text{h}\rceil$ indicates rounding to the nearest integer, choosing the even integer in the case of a tie. When $p \leq \frac{\frac{1}{2}}{N}$, use $x_1$.

4
R-4, SAS-1
$Np$
$x_{\lfloor \text{h}\rfloor} + (h - \lfloor \text{h} \rfloor) (x_{\lfloor \text{h} \rfloor + 1} - x_{\lfloor \text{h} \rfloor})$
(0,1)
$\frac{k}{n}$ Linear interpolation of the emperical distribution function. When $p \lt \frac{1}{N}$, use $x_1$. When $p = 1$, use $x_N$.

5
R-5
$Np + \frac{1}{2}$
$x_{\lfloor \text{h}\rfloor} + (h - \lfloor \text{h} \rfloor) (x_{\lfloor \text{h} \rfloor + 1} - x_{\lfloor \text{h} \rfloor})$
(0.5,0.5)
$\frac{k - \frac{1}{2}}{n}$ Piecewise linear function where the knots are the values midway through the steps of the emperical distribution function. When $p \lt \frac{\frac{1}{2}}{N}$, use $x_1$. When $p \geq \frac{(N - \frac{1}{2}}{N})$, use $x_N$.

6
R-6, SAS-4 Minitab, SPSS
$(N + 1)p$
$x_{\lfloor \text{h}\rfloor} + (h - \lfloor \text{h} \rfloor) (x_{\lfloor \text{h} \rfloor + 1} - x_{\lfloor \text{h} \rfloor})$
(0,0)
$\frac{k}{n +1}$ Linear interpolation of the expectations for the order statistics for the uniform distribution on [0,1]. That is, it is the linear interpolation between points ($p_h, x_h$), where $p_h = \frac{h}{N+ 1}$ is the probability that the last of $(N + 1)$ randomly drawn values will not exceed the $h^\text{th}$ smallest of the first $N$ randomly drawn values. When $p \lt \frac{1}{N + 1}$, use $x_1$. When $p \geq \frac{N}{N + 1}$, use $x_N$.

7
R-7
$(N - 1)p + 1$
$x_{\lfloor \text{h}\rfloor} + (h - \lfloor \text{h} \rfloor) (x_{\lfloor \text{h} \rfloor + 1} - x_{\lfloor \text{h} \rfloor})$
(1,1)
$p(k) = \frac{k - 1}{n - 1}\\ p(k) = \text{mode[F(x[k])]}$
Linear interpolation of the modes for the order statistics for the uniform distribution on [0,1].  When $p = 1$, use $x_N$.

8
R-8
$(N + \frac{1}{3})p + \frac{1}{3}$
$x_{\lfloor \text{h}\rfloor} + (h - \lfloor \text{h} \rfloor) (x_{\lfloor \text{h} \rfloor + 1} - x_{\lfloor \text{h} \rfloor})$
$(\frac{1}{3},\frac{1}{3})$
$p(k) = \frac{k - \frac{1}{3}}{n + \frac{1}{3}}$ Then $p(k) \text{ ~ median}{[F(x[k])]}.$ The resulting quantile estimtes are approximately median-unbiased regardless of the distribution of x. Linear interpolation of the approximate medians for order statistics. When $p \lt \frac{\frac{2}{3}}{N + \frac{1}{3}}$, use $x_1$. When $p \geq \frac{(N - \frac{1}{3})}{(N + \frac{1}{3})}$, use $x_N$.

9
R-9
$(N + \frac{1}{4})p + \frac{3}{8}$
$x_{\lfloor \text{h}\rfloor} + (h - \lfloor \text{h} \rfloor) (x_{\lfloor \text{h} \rfloor + 1} - x_{\lfloor \text{h} \rfloor})$
($\frac{3}{8},\frac{3}{8}$)
$p(k) = \frac{k - \frac{3}{8}}{n + \frac{1}{4}}$ Blom. The resulting quantile estimates are approximately unbiased for the expected order statistics if $x$ is normally distributed. When $p \lt \frac{\frac{5}{8}}{N + \frac{1}{4}}$, use $x_1$. When $p \geq \frac{N - \frac{3}{8}}{N + \frac{1}{4}}$, use $x_N$.

10

(0.4,0.4)
Cunnane's approximately quantile unbiased definition.

11

Filliben's estimate.

12

(0.35,0.35)
APL, used with PWM.

# Software

As shown above, R (version 2.0.0 onwards) implements methods 1-9. SciPy implements methods 4-9, 10, and a method called APL (need info on this).

# Does it matter?

TBD

# Code

```python
# Start of time estimation for the notebook.
import datetime as dt
start_time = dt.datetime.now()
```

```python
# Import the required librairies.
import pandas as pd
import scipy.stats.mstats as ms
from scipy.stats import anderson
import scipy.stats as sm
import statsmodels.stats.diagnostic as smd
import numpy as np

# This is a kluge until I learn to create a package for /datasense
#import sys
#sys.path.insert(0, '/home/gilles/documents/websites/support.gillespilon.ca/fil/datasense')

import datasense as ds
```

```python
# Read the data file.
# y is the column of response values.
df = pd.read_csv('estimating_sample_quantiles.csv')
```

```python
# Calculate basic statistics.
df.describe()
```

```python
# Test scipy.stats.mstats mquantiles function on its own
# Incorporate into my datasense package.
# Need to test all options for alphap and betap. Following is R-8 (recommended).
# Finally, add code to do CI with method R-8.
resultd = ms.mquantiles(df['y'], prob=(0.25, 0.50, 0.75), alphap=0.33, betap=0.33)
resultd
```

```python
resultd[0]
```

# Minitab results

        Anderson-Darling normality test

        A2
        0.83

        p
        0.018

        Statistics

        Average
        20.875

        Sample standard deviation
        27.010

        Sample variance
        729.554

        Skewness
        1.02777

        Kurtosis
        -0.77149

        Sample size
        8

        Minimum
        0.000

        Q1
        0.250

        Q2 (median)
        7.500

        Q3
        52.500

        Maximum
        63.000

        Confidence intervals

        95 % CI for average
        -1.706, 43.456

        95 % CI for median
        0.000 61.129

        95 % CI for standard deviation
        177.858, 54.973

```python
resultminitab = ms.mquantiles(df['y'], prob=(0.25, 0.50, 0.75), alphap=0, betap=0)
resultminitab
```

```python
ds.nonparametric_summary(df['y']) # alphap=0.33 betap=0.33 by default
```

```python
ds.nonparametric_summary(df['y'], alphap=0, betap=0) # Minitab
```

```python
# Calculate minimum value of the dataframe.
df.min()
```

```python
# Calculate maximum value of the dataframe.
df.max()
```

```python
# Calculate the average of the dataframe.
df.mean()
```

```python
# Calculate sample standard deviation of the dataframe.
df.std()
```

```python
# Calculate sample variance of the dataframe.
df.var()
```

```python
# Calculate skewness of the dataframe.
df.skew()
```

```python
# Calculate kurtosis of the dataframe.
df.kurt()
```

```python
# Calculate the sample size of the dataframe.
df.count()
```

```python
adresult = anderson(df['y'], dist='norm')
adresult.statistic
```

```python
# Use statsmodels to calculate the Anderson-Darling test statistic and p value.
smd.normal_ad(df['y'])
```

```python
# Calculate confidence interval of the mean.
# Result should be -1.706, 43.456 according to Minitab.
# Might be because n is small and sm is using z rather than t.
sm.norm.interval(0.95,
                 loc=np.mean(df['y']),
                 scale=sm.sem(df['y']))
```

```python
# Close but not yet.
sm.t.interval(0.95,
              len(df['y']-1),
              loc=np.mean(df['y']),
              scale=sm.sem(df['y']))
```

```python
# End of time estimation for the notebook.
end_time = dt.datetime.now()
(end_time - start_time).total_seconds()
```

# References

[datasense module](https://github.com/gillespilon/datasense).

[Five-number summary](https://en.wikipedia.org/wiki/Five-number_summary).

Hyndman, Rob J. and Yanan Fan. "Sample Quantiles in Statistical Packages." *The American Statistician* Vol. 50, No. 4 (Nov. 1996): 361-365. [JSTOR 2684934](http://www.jstor.org/stable/2684934).

McGill, Robert, John W. Tukey, and Wayne A. Larsen. 1978. "Variations of Box Plots." *The American Statistician 21 (February 1978), no. 1; 12-16. [https://www.jstor.org/stable/2683468](https://www.jstor.org/stable/2683468).

[pandas](https://pandas.pydata.org/pandas-docs/stable/api.html).

[R documentation on quantiles](https://www.rdocumentation.org/packages/stats/topics/quantile).

[R quantile function](https://stat.ethz.ch/R-manual/R-devel/library/stats/html/quantile.html).

[SciPy documentation on mquantiles](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.mstats.mquantiles.html).

[SciPy documentation on masked arrays](https://docs.scipy.org/doc/numpy-1.15.1/reference/maskedarray.html).

Wikipedia. "Quantile". Last modified 2018-08-01. [https://en.wikipedia.org/wiki/Quantile](https://en.wikipedia.org/wiki/Quantile).


# Learn More

We're about ready to wrap up this brief course on Python for scientific
computing.

In this last lecture we give some pointers to the major scientific libraries
and suggestions for further reading.

## NumPy

Fundamental matrix and array processing capabilities are provided by the
excellent [NumPy](http://www.numpy.org/) library.

For example, let\'s build some arrays

```
import numpy as np                     # Load the library

a = np.linspace(-np.pi, np.pi, 100)    # Create even grid from -π to π
b = np.cos(a)                          # Apply cosine to each element of a
c = np.sin(a)                          # Apply sin to each element of a
```

Now let\'s take the inner product

```
b @ c
```

The number you see here might vary slightly due to floating point arithmetic
but it\'s essentially zero.

As with other standard NumPy operations, this inner product calls into highly
optimized machine code.

It is as efficient as carefully hand-coded FORTRAN or C.

## SciPy

The [SciPy](http://www.scipy.org) library is built on top of NumPy and
provides additional functionality.

(tuple_unpacking_example)=
For example, let\'s calculate $\int_{-2}^2 \phi(z) dz$ where $\phi$ is
the standard normal density.

```
from scipy.stats import norm
from scipy.integrate import quad

ϕ = norm()
value, error = quad(ϕ.pdf, -2, 2)  # Integrate using Gaussian quadrature
value
```

SciPy includes many of the standard routines used in

-   [linear algebra](http://docs.scipy.org/doc/scipy/reference/linalg.html)
-   [integration](http://docs.scipy.org/doc/scipy/reference/integrate.html)
-   [interpolation](http://docs.scipy.org/doc/scipy/reference/interpolate.html)
-   [optimization](http://docs.scipy.org/doc/scipy/reference/optimize.html)
-   [distributions and random number generation](http://docs.scipy.org/doc/scipy/reference/stats.html)
-   [signal processing](http://docs.scipy.org/doc/scipy/reference/signal.html)

See them all [here](http://docs.scipy.org/doc/scipy/reference/index.html).

## Graphics

The most popular and comprehensive Python library for creating figures
and graphs is [Matplotlib](http://matplotlib.org/), with functionality
including

-   plots, histograms, contour images, 3D graphs, bar charts etc.
-   output in many formats (PDF, PNG, EPS, etc.)
-   LaTeX integration

Example 2D plot with embedded LaTeX annotations

```{figure} /_static/lecture_specific/about_py/qs.png
:scale: 55%
```

Example contour plot

```{figure} /_static/lecture_specific/about_py/bn_density1.png
:scale: 55%
```

Example 3D plot

```{figure} /_static/lecture_specific/about_py/career_vf.png
:scale: 80%
```

More examples can be found in the [Matplotlib thumbnail
gallery](http://matplotlib.org/gallery.html).

Other graphics libraries include

-   [Plotly](https://plot.ly/python/)
-   [Bokeh](http://bokeh.pydata.org/en/latest/)

## Symbolic Algebra

It\'s useful to be able to manipulate symbolic expressions, as in
Mathematica or Maple.

The [SymPy](http://www.sympy.org/) library provides this functionality
from within the Python shell.

```
from sympy import Symbol

x, y = Symbol('x'), Symbol('y')  # Treat 'x' and 'y' as algebraic symbols
x + x + x + y
```

We can manipulate expressions

```
expression = (x + y)**2
expression.expand()
```

solve polynomials

```
from sympy import solve

solve(x**2 + x + 2)
```

and calculate limits, derivatives and integrals

```
from sympy import limit, sin, diff

limit(1 / x, x, 0)
```

```
limit(sin(x) / x, x, 0)
```

```
diff(sin(x), x)
```

The beauty of importing this functionality into Python is that we are
working within a fully fledged programming language.

We can easily create tables of derivatives, generate LaTeX output, add
that output to figures and so on.

## Pandas

One of the most popular libraries for working with data is
[pandas](http://pandas.pydata.org/).

Pandas is fast, efficient, flexible and well designed.

Here\'s a simple example, using some dummy data generated with Numpy\'s
excellent `random` functionality.

```
import pandas as pd
np.random.seed(1234)

data = np.random.randn(5, 2)  # 5x2 matrix of N(0, 1) random draws
dates = pd.date_range('28/12/2010', periods=5)

df = pd.DataFrame(data, columns=('price', 'weight'), index=dates)
print(df)
```

```
df.mean()
```

## Further Reading

These lectures were originally taken from a longer and more complete lecture
series on Python programming hosted by [QuantEcon](https://quantecon.org).

The [full set of lectures](https://python-programming.quantecon.org/) might be
useful as the next step of your study.


# Symbolic Mathematics in Python

There are times when you need to solve a difficult problem symbollically or analytically. If you have ever used Wolfram Alpha, then you have already done this. Sympy is a python library that allows you to do symbolic mathematics in python.

```python
import sympy as sym
```

## 1. Introduction

### Example 1.1

If you try to write the follwing in python by itself, you will get an error telling you x is undefined:
$$x-x$$

```python
x-x
```

(The error above is on purpose). Variables in python need to be defined before you can say something specific about them

```python
x=102
x-x
```

If you are trying to show that $x-x=0$ is true for any $x$, the above answer would not be valid. Instead you can use a symbolic expression to show that it is true

**First we define the variable as a symmbolic expression**

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

**Now we can use the variable in a symbolic expression**

```python
x-x
```

### Example 1.2

Sympy can be used to perform algebreic operations (among other things). Consider the following expression: $$(3a-4b)^3$$

We can use symppy to expand the expression algebraically.

**First we need to define the variables as symbolic expressions**

```python
a,b = sym.symbols('a,b')
```

**Side note** Notice that the left hand side of the epression has two variables being defined. Python can define more than one variable at a time:

```python
x1,y1 =10,20
print(x1)
print(y1)
```

    10
    20

**Back to the expression** We can define an expression using the variables $a$ and $b$.

```python
a,b = sym.symbols('a,b')
expr = (3*a-4*b)**3
print(expr)
```

    (3*a - 4*b)**3

We can also make it look nicer in our notebook. This doesn't affect the math, but it makes our notebook prettier.

```python
sym.init_printing()
```

```python
expr
```

**Now we expand the function algebreically**

```python
expr.expand()
```

Sympy can also factor the equation

```python
sym.factor(26*a**3-108*a**2*b+144*a*b**2-64*b**3)
```

If you want to copy and paste a result, you print the result.

```python
print(sym.factor(26*a**3-108*a**2*b+144*a*b**2-64*b**3))
```

    2*(a - 2*b)*(13*a**2 - 28*a*b + 16*b**2)

You can also chain together functions

```python
expr.expand().factor()
```

### Exercise 1.1

Show that the following two expressions are true.
$$(2w-3z)(2w+3z)=4w^2-9z^2$$
$$(2w-3z)^2\ne4w^2-9z^2$$

```python
# Your code here
import sympy as sym
w,z=sym.symbols('w,z')
expr1=(2*w-3*z)*(2*w+3*z)
expr2=(2*w-3*z)**2

expr1.expand()
expr2.factor()

print(expr1.expand())
print(expr2.factor())
```

    4*w**2 - 9*z**2
    (-2*w + 3*z)**2

## 2. Solving Equations

Sympy can be used to symbolilically solve equations. As before, you need to define which variables are symbols

### Example 2.1

Use sympy to solve the following equation
$$ax^3+bx^2+cx+d=0$$

```python
# Define the variables
a,b,c,d,x = sym.symbols('a,b,c,d,x')
```

```python
# Define the expression
expr=a*x**3+b*x**2+c*x+d
expr
```

$\displaystyle a x^{3} + b x^{2} + c x + d$

We can use the `solvset` function to solve this equation

```python
solutions=sym.solveset(expr,x)
```

```python
print(solutions)
```

    {-(-3*c/a + b**2/a**2)/(3*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a), -(-3*c/a + b**2/a**2)/(3*(-1/2 - sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (-1/2 - sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a), -(-3*c/a + b**2/a**2)/(3*(-1/2 + sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (-1/2 + sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a)}

```python
solutions
```

$\displaystyle \left\{- \frac{- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}}{3 \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}} - \frac{\sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}}{3} - \frac{b}{3 a}, - \frac{- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}}{3 \left(- \frac{1}{2} - \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}} - \frac{\left(- \frac{1}{2} - \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}}{3} - \frac{b}{3 a}, - \frac{- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}}{3 \left(- \frac{1}{2} + \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}} - \frac{\left(- \frac{1}{2} + \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 c}{a} + \frac{b^{2}}{a^{2}}\right)^{3} + \left(\frac{27 d}{a} - \frac{9 b c}{a^{2}} + \frac{2 b^{3}}{a^{3}}\right)^{2}}}{2} + \frac{27 d}{2 a} - \frac{9 b c}{2 a^{2}} + \frac{b^{3}}{a^{3}}}}{3} - \frac{b}{3 a}\right\}$

What if I need help. You can do this with any python function. `function?`

```python
# Run this command to see a help box
sym.solveset?
```

### Exercise 2.1

Use the `solveset` function to solve the following chemical problem.

Phosgene gas, $\text{COCl}_2$, dissociates at high temperatures according to the following equilibrium:

$$ \text{COCl}_2 \rightleftharpoons  \text{CO} + \text{Cl}_2 $$

At $\text{400 C}$, the equilibrium constant $K_c=8.05$.

If you start with a  $\text{0.250 M}$ phosgene sample at $\text{400 C}$, determine the concentrations of all species at equilibrium.

```python
# Your code here
import sympy as sym
x=sym.symbols("x")
expr=(x**2/(0.25-x))-8.05

solutions=sym.solveset(expr,x)
solutions
```

$\displaystyle \left\{-8.29268379803378, 0.242683798033777\right\}$

Why did you pick your answer?

Because positvity is more attractive

## 3. Calculus

We can use also Sympy to differentiate and integrate. Let us experiment with differentiating the following expression:

$$x ^ 2 - \cos(x)$$

```python
sym.diff(x ** 2 - sym.cos(x), x)
```

$\displaystyle 2 x + \sin{\left(x \right)}$

Similarly we can integrate:

```python
sym.integrate(x ** 2 - sym.cos(x), x)
```

$\displaystyle \frac{x^{3}}{3} - \sin{\left(x \right)}$

We can also carry out definite integrals:

```python
sym.integrate(x ** 2 - sym.cos(x), (x, 0, 5))
```

$\displaystyle \frac{125}{3} - \sin{\left(5 \right)}$

### Exercise 3.1

Use Sympy to calculate the following:

1. $\frac{d(x ^2 + xy - \ln(y))}{dy}$
1. $\int_0^5 e^{2x}\;dx$

```python
import sympy as sym
x,y=sym.symbols("x,y")
sym.diff(x**2+x*y-sym.ln(y),x)
```

$\displaystyle 2 x + y$

```python
import sympy as sym
x=sym.symbols("x")
sym.integrate(e**(2*x), (x,0,5))
```

$\displaystyle \begin{cases} \frac{e^{10}}{2 \log{\left(e \right)}} - \frac{1}{2 \log{\left(e \right)}} & \text{for}\: \left(e \geq 0 \wedge e < 1\right) \vee e > 1 \\5 & \text{otherwise} \end{cases}$

### Exercise 3.2
Solve the following definate integral
$$\int\limits_{ - \infty }^\infty  {\frac{1}{{\sigma \sqrt {2\pi } }}{e^{ - \frac{1}{2}{{\left( {\frac{{x - \mu }}{\sigma }} \right)}^2}}}}$$

Hint, the sympy symbol for infinity is `oo`

```python
# Your code here
from sympy import oo
from sympy import pi
from sympy import sqrt
from sympy import E
from sympy import Symbol

o,u,x = sym.symbols('o,u,x')

x = Symbol('x', real=True); x
u = Symbol('u', real=True); u
o = Symbol('o', real=True); o

sym.integrate((1/(o*sqrt(2*pi))*(E**(-(1/2)*((x-u)/o)**2))), (x,-oo,oo))
```

$\displaystyle \begin{cases} 0.707106781186547 \sqrt{2} & \text{for}\: 2 \left|{\arg{\left(o \right)}}\right| \leq \frac{\pi}{2} \\\int\limits_{-\infty}^{\infty} \frac{\sqrt{2} e^{- \frac{0.5 \left(- u + x\right)^{2}}{o^{2}}}}{2 \sqrt{\pi} o}\, dx & \text{otherwise} \end{cases}$

```python
expr = 0.707106781186547*sqrt(2)
a = (0.707106781186547*sqrt(2).evalf())
print (expr,'=', a, '=', a.round(1))
```

    0.707106781186547*sqrt(2) = 0.999999999999999 = 1.

Lookup Gaussian functions: https://en.wikipedia.org/wiki/Gaussian_function
Does your answer maake sense?

## 4. Plotting with Sympy

Finally Sympy can be used to plot functions. Note that this makes use of [matplotlib](http://matplotlib.org/).

Let us plot $x^2$:

```python
expr = x ** 2
p = sym.plot(expr)
```

### Exercise 4.1 Plot the following function:

1. $y=x + cos(x)$
1. ${\frac{1}{{ \sqrt {2\pi } }}{e^{ - \frac{x^2}{2}}}}$

```python
# Your code here
expr1=x+sym.cos(x)
p=sym.plot(expr1)
```

```python
expr =  (1/sqrt(2*pi))*E**(-x**2/2)
p = sym.plot(expr)
```

# Lecture

## L1. Hydrogen Atom

Sympy has built in modules for the eigenfunctions of the hydrogen atom.

```python
import sympy.physics.hydrogen
import numpy as np
```

You can caluclate the eigenvalues ($E$) in Hartrees

`sym.physics.hydrogen.E_nl(n,Z)`

```python
sym.physics.hydrogen.E_nl(1,1)
```

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

We can use a loop to print out many energies

```python
for n in range(1,5):
    print(sym.physics.hydrogen.E_nl(n,1))
```

    -1/2
    -1/8
    -1/18
    -1/32

We can plot the hydrogen radial wavefunction (1s orbital)

```python
r=sympy.symbols('r')
sympy.physics.hydrogen.R_nl(1, 0, r, 1)
```

$\displaystyle 2 e^{- r}$

```python
sym.plot(sympy.physics.hydrogen.R_nl(1, 0, r, 1),(r,0,10.50))
```

And the probablity distribution function

```python
sympy.symbols('r')
prob_1s=sympy.physics.hydrogen.R_nl(1, 0, r, 1)*sympy.physics.hydrogen.R_nl(1, 0, r, 1)
prob_1s
```

$\displaystyle 4 e^{- 2 r}$

```python
sym.plot(prob_1s,(r,0,10))
```

Plot a 2s orbital

```python
sympy.symbols('r')
prob_2s=sympy.physics.hydrogen.R_nl(2, 0, r, 1)*sympy.physics.hydrogen.R_nl(2, 0, r, 1)
prob_2s
```

$\displaystyle \frac{\left(2 - r\right)^{2} e^{- r}}{8}$

```python
sym.plot(prob_2s,(r,0,10))
```

We can change the range to see the node better.

```python
sym.plot(prob_2s,(r,1,8))
```

Notice the node!

### Exercise L1.1

Plot the radial distriubution function for a 2p, 3s, 4s, and 3d orbital.

```python
# Your code here
prob_2p=sympy.physics.hydrogen.R_nl(2, 1, r, 1)*sympy.physics.hydrogen.R_nl(2, 1, r, 1)
prob_2p
```

$\displaystyle \frac{r^{2} e^{- r}}{24}$

```python
sym.plot(prob_2p,(r,0,8))
```

```python
prob_3s=sympy.physics.hydrogen.R_nl(3, 0, r, 1)*sympy.physics.hydrogen.R_nl(3, 0, r, 1)
prob_3s
```

$\displaystyle \frac{4 \left(\frac{2 r^{2}}{9} - 2 r + 3\right)^{2} e^{- \frac{2 r}{3}}}{243}$

```python
sym.plot(prob_3s,(r,1,8))
```

```python
prob_4s=sympy.physics.hydrogen.R_nl(4, 0, r, 1)*sympy.physics.hydrogen.R_nl(4, 0, r, 1)
prob_4s
```

$\displaystyle \left(- \frac{r^{3}}{768} + \frac{r^{2}}{32} - \frac{3 r}{16} + \frac{1}{4}\right)^{2} e^{- \frac{r}{2}}$

```python
sym.plot(prob_4s,(r,1,8))
```

```python
prob_3d=sympy.physics.hydrogen.R_nl(3, 2, r, 1)*sympy.physics.hydrogen.R_nl(3, 2, r, 1)
prob_3d
```

$\displaystyle \frac{8 r^{4} e^{- \frac{2 r}{3}}}{98415}$

```python
sym.plot(prob_3d,(r,1,15))
```

```python

```