# Solutions

## Question 1

> 1. For each of the following functions calculate $\frac{df}{dx}$,
>    $\frac{d^2f}{dx^2}$ and $\int f(x) dx$.

> $f(x) = x$

```python
import sympy as sym

x = sym.Symbol("x")
expression = x
sym.diff(expression, x)
```

$\displaystyle 1$

```python
sym.diff(expression, x, 2)
```

$\displaystyle 0$

```python
sym.integrate(expression, x)
```

$\displaystyle \frac{x^{2}}{2}$

> $f(x) = x ^{\frac{1}{3}}$

```python
expression = x ** (sym.S(1) / 3)
sym.diff(expression, x)
```

$\displaystyle \frac{1}{3 x^{\frac{2}{3}}}$

```python
sym.diff(expression, x, 2)
```

$\displaystyle - \frac{2}{9 x^{\frac{5}{3}}}$

```python
sym.integrate(expression, x)
```

$\displaystyle \frac{3 x^{\frac{4}{3}}}{4}$

> $f(x) = 2 x (x - 3) (\sin(x) - 5)$

```python
expression = 2 * x * (x - 3) * (sym.sin(x) - 5)
sym.diff(expression, x)
```

$\displaystyle 2 x \left(x - 3\right) \cos{\left(x \right)} + 2 x \left(\sin{\left(x \right)} - 5\right) + \left(x - 3\right) \left(2 \sin{\left(x \right)} - 10\right)$

```python
sym.diff(expression, x, 2)
```

$\displaystyle 2 \left(- x \left(x - 3\right) \sin{\left(x \right)} + 2 x \cos{\left(x \right)} + 2 \left(x - 3\right) \cos{\left(x \right)} + 2 \sin{\left(x \right)} - 10\right)$

```python
sym.integrate(expression, x)
```

$\displaystyle - \frac{10 x^{3}}{3} - 2 x^{2} \cos{\left(x \right)} + 15 x^{2} + 4 x \sin{\left(x \right)} + 6 x \cos{\left(x \right)} - 6 \sin{\left(x \right)} + 4 \cos{\left(x \right)}$

> $f(x) = 3  x ^ 3 + 6 \sqrt{x} + 3$

```python
expression = 3 * x ** 3 + 6 * sym.sqrt(x) + 3
sym.diff(expression, x)
```

$\displaystyle 9 x^{2} + \frac{3}{\sqrt{x}}$

```python
sym.diff(expression, x, 2)
```

$\displaystyle 3 \left(6 x - \frac{1}{2 x^{\frac{3}{2}}}\right)$

```python
sym.integrate(expression, x)
```

$\displaystyle 4 x^{\frac{3}{2}} + \frac{3 x^{4}}{4} + 3 x$

## Question 2

> `2`. Consider the function $f(x)=2x+1$. By differentiating _from first
> principles_ show that $f'(x)=2$.

Using the definition of the derivative:

```python
h = sym.Symbol("h")
expression = 2 * x + 1
sym.limit((expression - expression.subs({x: x - h})) / h, h, 0)
```

$\displaystyle 2$

## Question 3

> `3`. Consider the second derivative $f''(x)=6x+4$ of some cubic function $f(x)$.

> `1`. Find $f'(x)$

We know the derivative will be the integral of the second derivative with a
constant:

```python
c1 = sym.Symbol("c1")

second_derivative = 6 * x + 4
derivative = sym.integrate(second_derivative, x) + c1
derivative
```

$\displaystyle c_{1} + 3 x^{2} + 4 x$

> `2`. You are given that $f(0)=10$ and $f(1)=13$, find $f(x)$.

We know that the cubic will be the integral of the derivative with constant:

```python
c2 = sym.Symbol("c2")

cubic = sym.integrate(derivative, x) + c2
cubic
```

$\displaystyle c_{1} x + c_{2} + x^{3} + 2 x^{2}$

We substitute $x=0$:

```python
cubic.subs({x: 0})
```

$\displaystyle c_{2}$

This gives $c_2=10$. We substitute that back in to our expression for the cubic:

```python
cubic = cubic.subs({c2: 10})
cubic
```

$\displaystyle c_{1} x + x^{3} + 2 x^{2} + 10$

and now substitute $x=1$:

```python
cubic.subs({x: 1})
```

$\displaystyle c_{1} + 13$

which gives $c_1=0$ which we substitute back in to our expression for the cubic:

```python
cubic = cubic.subs({c1: 0})
cubic
```

$\displaystyle x^{3} + 2 x^{2} + 10$

> `3`. Find all the stationary points of $f(x)$ and determine their nature.

The stationary points are the points that give $\frac{df}{dx}=0$:

```python
stationary_points = sym.solveset(sym.diff(cubic, x), x)
stationary_points
```

$\displaystyle \left\{- \frac{4}{3}, 0\right\}$

We determine the nature of these turning points by considering the sign of $\frac{d^2f}{dx^2}$ at each point.

```python
second_derivative.subs({x: -4 / sym.S(3)})
```

$\displaystyle -4$

This is negative, so it is a local maximum.

```python
second_derivative.subs({x: 0})
```

$\displaystyle 4$

This is positive, so it is a local minimum.

## Question 4

> `4`. Consider the function $f(x)=\frac{2}{3}x ^ 3 + b x ^ 2 + 2 x + 3$, where
> $b$ is some undetermined coefficient.

> `1`. Find $f'(x)$ and $f''(x)$

```python
b = sym.Symbol("b")
expression = sym.S(2) / 3 * x ** 3 + b * x ** 2 + 2 * x + 3
derivative = sym.diff(expression, x)
derivative
```

$\displaystyle 2 b x + 2 x^{2} + 2$

```python
second_derivative = sym.diff(expression, x, 2)
```

> `2`. You are given that $f(x)$ has a stationary point at $x=2$. Use this
> information to find $b$.

We solve the equation that arises when substituting $x=2$ in to the derivative:

```python
equation = sym.Eq(derivative.subs({x: 2}), 0)
equation
```

$\displaystyle 4 b + 10 = 0$

```python
sym.solveset(equation, b)
```

$\displaystyle \left\{- \frac{5}{2}\right\}$

> `3`. Find the coordinates of the other stationary point.

We substitute this value of $b$ in to the expression:

```python
b_value = -sym.S(5) / 2
expression = expression.subs({b: b_value})
expression
```

$\displaystyle \frac{2 x^{3}}{3} - \frac{5 x^{2}}{2} + 2 x + 3$

and the derivative and then solve the equation:

```python
derivative = derivative.subs({b: b_value})
sym.solveset(derivative)
```

$\displaystyle \left\{\frac{1}{2}, 2\right\}$

> `4`. Determine the nature of both stationary points.

Substituting both values in to the second derivative:

```python
second_derivative = second_derivative.subs({b: b_value})
second_derivative.subs({x: sym.S(1) / 2})
```

$\displaystyle -3$

This is negative so it is a local maxima.

```python
second_derivative.subs({x: 2})
```

$\displaystyle 3$

This is positive so it is a local minima.

## Question 5

> `5`. Consider the functions $f(x)=-x^2+4x+4$ and $g(x)=3x^2-2x-2$.

> `1`. Create a variable `turning_points` which has value the turning points of
> $f(x)$.

```python
f = -(x ** 2) + 4 * x + 4
derivative = sym.diff(f, x)
turning_points = sym.solveset(derivative, x)
```

> `2`. Create variable `intersection_points` which has value of the points where
> $f(x)$ and $g(x)$ intersect.

```python
g = 3 * x ** 2 - 2 * x - 2
equation = sym.Eq(f, g)
intersection_points = sym.solveset(equation, x)
intersection_points
```

$\displaystyle \left\{\frac{3}{4} - \frac{\sqrt{33}}{4}, \frac{3}{4} + \frac{\sqrt{33}}{4}\right\}$

> `3`. Using your answers to parts 2., calculate the area of the region between
> $f$ and $g$. Assign this value to a variable `area_between`.

The area between $f$ and $g$ corresponds to the integral of $\pm (f - g)$
between the points of intersection. We here use $f - g$, if the outcome was
negative we would take the opposite.

```python
area_between = sym.integrate(
    f - g, (x, sym.S(3) / 4 - sym.sqrt(33) / 4, sym.S(3) / 4 + sym.sqrt(33) / 4)
)
sym.simplify(area_between)
```

$\displaystyle \frac{11 \sqrt{33}}{4}$


## Plotting the Ekman spiral

### a) The top Ekman spiral

The equations for the Ekman spiral are

\begin{align}
    u_E (z) & = u_s \cos\left(\frac{𝜋}{4} + \frac{\pi}{D} z\right) e^{\frac{\pi}{D}z} \\
    v_E (z) & = -u_s \sin\left(\frac{𝜋}{4} + \frac{\pi}{D} z\right) e^{\frac{\pi}{D}z}
\end{align}

Where $u_s = 1$ m/s is the flow at the surface and $D=\pi\sqrt{2K/f}$ is the Ekman layer thickness.

Plot these two functions for $f=10^{-4} s^{-1}$, $K=2\times10^{-2} m^2/s$ and `z=np.arange(-100, 0.01, 5)`.

Make three plots:
1. $u_E$ and $v_E$ as two lines as a function of depth. Put depth on the y-axis
2. A scatter-line-plot of $u_E$ vs. $v_E$, with each point color-coded for depth
3. a 3D quiver plot, you can use the code below to show the vector components $u_E$ and $v_E$

    ```
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    fig = plt.figure(figsize=(7, 5))
    ax = fig.gca(projection='3d')
    zs = np.zeros(len(z))
    ax.set_xlim((-.1,.1))
    ax.set_ylim((-.1,.1))
    ax.set_zlim((-100,0))
    ax.quiver(zs, zs, z, uE, vE, zs)
    ax.set_xlabel('zonal velocity [m/s]')
    ax.set_ylabel('meridional velocity [m/s]')
    ax.set_zlabel('Depth [m]')
    plt.show()
    ```

Explain the figures.

```python

```

### b) Bottom Ekman Layer
At the bottom there is also an Ekman layer, but only if the ocean interior geostrophic velocity is non-zero so that a no-slip boundary condition can be satisfied. The Ekman spiral looks like this:

\begin{align}
    u_E (z) & = u_g - e^{z/D} \left[u_g \cos(z/D) + v_g \sin(z/D) \right] \\
    v_E (z) & = v_g + e^{z/D} \left[u_g \sin(z/D) - v_g \cos(z/D) \right]
\end{align}

Here $u_g$ and $v_g$ are the geoestrophic velocities in the ocean's interior.

The derivation is similar to that of the surface Ekman layer and can be found in chapter 5.7.3 of _Atmospheric and Oceanic Fluid Dynamics_ (2nd edition) by Vallis or chapter 5.3.2 of _Dynamical Oceanography_ by Dijkstra.

Plot the same three figures as in (a) for $u_g = 0.1 m/s$, $v_g = 0 m/s$, and assuming the same $D$ and $f$ for the 250 meters above the sea floor (for simplicity, the sea floor can now be set to 0 and $z$ is still pointing up).

Explain the figures.

```python

```


# Exemplos de soluções numéricas para ODEs com o método de Euler

```python
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-poster')
```

## Exemplo 1
 Considere o problema de valor inicial:

 \begin{equation}
\dot{u}(t) = 2u
 \end{equation}

 com $u(t=0) = 1$

 Use o método de Euler para encontrar a solução aproximada. Compare-a com solução analítica $u(t) = e^{2t}$.

```python
# Definindo os parâmetros
h = 0.1 # passo
tfim = 5
nt = int(tfim/h) # numero de iterações
s0 = 1 # Condição Inicial
```

```python
u = np.zeros(nt)
u[0] = s0
for k in np.arange(1,nt):
  u[k] = u[k-1]+2*h*u[k-1]

```

```python
t = np.linspace(0,tfim,nt)
```

```python
plt.figure(figsize = (12, 8))
plt.plot(t, u, 'bo--', label='Numérica')
plt.plot(t, np.exp(2*t), 'g', label='Analítica')
plt.title('Solução Aproximada \
Solução Analítica')
plt.xlabel('t')
plt.ylabel('f(t)')
plt.grid()
plt.legend(loc='upper left')
plt.show()
```

É útil definir uma função para automatizar o gráfico acima:

```python
def faz_grafico_euler(tempo,y,f_analitica,h=0.01):
  plt.figure(figsize = (12, 8))
  plt.plot(tempo, y, 'bo--', label='Numérica com h={}'.format(h))
  plt.plot(tempo,f_analitica, 'g', label='Analítica')
  plt.title('Solução Aproximada \
  Solução Analítica')
  plt.xlabel('t')
  plt.ylabel('f(t)')
  plt.grid()
  plt.legend(loc='upper left')
  plt.show()
```

## Exemplo 2

Escreva uma função generalizada para o método de Euler.

```python
def Euler(f,h=0.01,s0=1,tmax=10):
  nt = int(tmax/h)
  u = np.zeros(nt)
  tempo = np.linspace(0,tmax,nt)
  u[0] = s0

  for k in np.arange(1,nt):
      u[k] = u[k-1] + h*f(tempo[k-1],u[k-1])

  return tempo,u

```

```python
# Testando para o exemplo 1

f = lambda t,u: 2*u # ODE
h = 0.1
s0=1
tmax = 5
```

```python

tempo,y = Euler(h,f,s0,tmax)
```

```python
faz_grafico_euler(tempo,y,np.exp(2*tempo),0.1)
```

## Exemplo 3

Teste a função Euler para o problema de valor inicial:

\begin{equation}
\dot{u} = u(t)(1-u(t))
\end{equation}

com $u(0) = 0.5$.
Compare com a solução analítica: $u(t)=e^t/(1+e^{t})$

```python
# Resolvendo o exemplo 3

f = lambda t,u: u*(1-u) # ODE
h = 0.1
s0=.5
tmax = 5
```

```python
tempo,y = Euler(h,f,s0,tmax)
```

```python
f_analitica = lambda t: np.exp(t)/(1+np.exp(t))
```

```python
faz_grafico_euler(tempo,y,f_analitica(tempo),h)
```

```python

```


# M2AA3 Chapter 3, Lesson 1 - Lagrange Interpolation

```python
import sympy as sp
```

Given points $\{(z_i, f_i) \}_{i=0}^n$, where $\forall i, z_i, f_i \in \mathbb{C}$, and $z_i$ are distinct. We would like to find a polynomial $p_n \in \mathbb{P}_n$ such that $\forall i, p_n(z_i) = f_i$.

The following polynomials work:
$$p_n(z) = \sum_{i=0}^n f_i l_i(z)$$ where $l_i (z)$ is the Largrange's basis function
$$l_i(z) = \prod_{\substack{0 \leq k \leq n \\ k \neq i}} \frac{z - z_k}{z_i - z_k}$$
The function 'lagrange' inputs 'zs' (list of $z_i$) and 'fs' (list of $f_i$) and output polynomials.

```python
x = sp.symbols('x')
l = sp.Function('l')
p = sp.Function('p')
```

Step 1 - Find Largrange Basis

```python
def basis(zs,i):

    # Initialization
    zi = zs[i]
    l = 1

    # Loop for Product
    for zj in zs:
        if zj != zi:
            l = l * (x-zj)/(zi-zj)

    l = sp.simplify(l)
    return l
```

Step 2 - Find Polynomial

```python
def lagrange(zs,fs):

    # Initialization
    p = 0

    # Loop for Sum
    for i in range(len(fs)):
        p += fs[i]*basis(zs,i)

    p = sp.simplify(p)
    return p
```

Example - Use the following data to find the largrange polynomial.

```python
xs = [-1, 0, 2, 5]
ys = [-3, -1, 4, 1]
```

```python
lagrange(xs,ys)
```

    -13*x**3/90 + 14*x**2/45 + 221*x/90 - 1

M2AA3/M2AA3-Polynomials/Lesson 01 - Basics/Largrange Interpolation.ipynb


# Solving equations by addition

Let's solve a linear equation using addition.

**Note**: Lesson inspired by https://www.basic-mathematics.com/solving-equations-using-addition.html

Here we import a few functions from SymPy. Each of these functions will be explained.

```python
from sympy import symbols, Eq, simplify, solve
```

## Overview

Given we have an equation

$$x + -b = c$$

we want to solve for the value of $x$.

We need to "move" the $b$ value over to the other side so we can have $x$ by itself.

Notice the negative or minus sign in front of $b$. We can add a postive version of the negative $b$ to move it over to the other side.

$$x + -b + b = c + b$$

Once we do this, the negative and positive $b$ values will cancel out to zero.

$$x + 0 = c + b$$

This will simplify to having $x$ on one side like we want it to.

$$x = c + b$$

## Setup

The above uses variables instead of numbers. Let's play around with this ourselves with Python and SymPy using real numbers.

Let's say we want to solve for $x$ in the following equation.

$$x + (-2) = 8$$

First we need to create our $x$ variable for our equation using the `symbols()` function.

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

We can write Python code very similar to our equation above.

Let us define our equation. This can be done using the `Eq()` function.

The first and second arguments for this function are the left and right sides of the equal side, respectively.

```python
eq1 = Eq(x + (-2), 8)
eq1
```

$\displaystyle x - 2 = 8$

You may notice the left side of our equation looks a bit different from above, which was $x + (-2)$.

These two expressions are equal and Python can check this for us.

```python
x + (-2) == x - 2
```

    True

## Using Python and SymPy to Help Us

Another great thing about Python and SymPy is that we can solve our equation using the `solve()` function. Why show this? Because we can have an end point we can go towards to check our math work immediately.

The `solve()` function takes two parameters, the first one is the equation and the second one is variable symbol you want to solve for.

```python
solve(eq1, x)
```

    [10]

This tells us that solving for $x$ in our equation $x + (-2) = 8$ requires $x$ to be $10$, or $x = 10$.

Let's use math to double check this.

## Adding on both sides

Based on our Overview above, we want to add the positive version of our values on both sides of the equation.

We can access the left hand side and the right hand side using the methods `.lhs` and `rhs` on our equation object, respectively.

```python
# Left side of the equals
eq1.lhs
```

$\displaystyle x - 2$

```python
# Right side of the equals
eq1.rhs
```

$\displaystyle 8$

Now that we have the left and right sides, let's add the positive version of 2 because we are subtracting from it.

```python
eq1 = Eq(eq1.lhs + 2, eq1.rhs + 2)
eq1
```

$\displaystyle x = 10$

## Exercise

Solve the following equation using addition.

$$x + (-6) = 5$$

**Hint**: Start by translating the math equation in to a SymPy equation using the `Eq()` function.

```python

```


#
 An Overview of SymPy

###
Dr. Subir Sarkar
Arya Vidyapeeth College
 Guwahati - 781 016
 Assam.

# Importing Sympy

>```python
from sympy import *
```

```python
from sympy import *
```

# Symbols

Symbols can be created in a few different ways in SymPy, for example,

`sympy.Symbol`, `sympy.symbols`, and `sympy.var`

```python
x = Symbol('x')
x
```

$\displaystyle x$

The variable $x$ now represents an abstract mathematical symbol which could, for example, represent a real number, an integer, a complex number, a function, as well as a large number of other possibilities.

If we have a mathematical variable $y$ that is known to be a real number, we can use the `real=True` keyword argument when creating the corresponding symbol instance. We can verify that SymPy indeed recognizes that the symbol is real by using
the `is_real` attribute of the Symbol class:

```python
y = Symbol('y', real=True)
y.is_real
```

    True

If, on the other hand we were to use `is_real` to query the previously defined symbol $x$, which was not explicitly specified to real, and therefore can represent both real and nonreal variables, we get None as result:

```python
x.is_real is None
```

    False

`is_real` returns **True** if the symbol is known to be **real**, **False** if the symbol is known to be **not real**, and **None** if it is not known if the symbol is real or not

```python
Symbol('z', imaginary=True).is_real
```

    False

Explicitly specifying when creating new symbols as real and positive or anything else as required, can help SymPy to simplify various expressions further than otherwise possible.

```python
x = Symbol('x')
y = Symbol('y', positive=True)
```

```python
sqrt(x**2)
```

$\displaystyle \sqrt{x^{2}}$

```python
sqrt(y**2)
```

$\displaystyle y$

When working with mathematical symbols that represent integers, rather than real numbers, it is also useful to explicitly specify this when creating the corresponding SymPy symbols, using, for example, the integer=True, or even=True or odd=True, if applicable. This may also allow SymPy to analytically simplify certain expressions and function evaluations.

```python
n1 = Symbol('n')
n2 = Symbol('n', integer=True)
n3 = Symbol('n', odd=True)
```

```python
cos(n1*pi)
```

$\displaystyle \cos{\left(\pi n \right)}$

```python
cos(n2*pi)
```

$\displaystyle \left(-1\right)^{n}$

```python
cos(n3*pi)
```

$\displaystyle -1$

Using Python’s tuple unpacking syntax together with a call to `sympy.symbols` is a convenient way to create multiple symbols:

```python
a, b, c = symbols('a, b, c', negative=True)
d, e, f = symbols('d, e, f', positive=True)
```

# Constants and Special Symbols

Selected mathematical constants and special symbols and their corresponding symbols in SymPy:

|Mathematical Symbol|SymPy Symbol|Description
|:---|:---|:---
|$\pi$|`pi`|Ratio of the circumference to the diameter of a circle.
|$e$|`E`|The base of the natural logarithm $e = exp (1)$.
|$\gamma$|`EulerGamma`|Euler's constant
|$i$|`I`|The imaginary unit.
|$\infty$|`oo`|Infinity

# Functions

## Undefined Functions

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

    sympy.core.function.UndefinedFunction

```python
f(x)
```

$\displaystyle f{\left(x \right)}$

```python
g = Function('g')(x, y, z)
g
```

$\displaystyle g{\left(x,y,z \right)}$

```python
g.free_symbols
```

    {x, y, z}

## Defined Functions

```python
type(sin)
```

    sympy.core.function.FunctionClass

```python
sin(1.5*pi)
```

$\displaystyle -1$

```python
n = Symbol('n')
sin(n*pi)
```

$\displaystyle \sin{\left(\pi n \right)}$

```python
n = Symbol('n', integer=True)
sin(n*pi)
```

$\displaystyle 0$

## Lambda Functions

It can be created `sympy.Lambda`

```python
f = Lambda(x, x**2)
f
```

$\displaystyle \left( x \mapsto x^{2} \right)$

```python
f(1.5)
```

$\displaystyle 2.25$

```python
f(1 + x)
```

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

# Expressions

In SymPy, mathematical expressions are represented as trees where leafs are symbols, and nodes are class instances that represent mathematical operations.

Examples of these classes are `Add`, `Mul`, and `Pow` for basic arithmetic operators, and `Sum`, `Product`, `Integral`, and `Derivative` for analytical mathematical operations.

```python
x = symbols('x')
expr = 1 + x + x**2 + x**3
expr
```

$\displaystyle x^{3} + x^{2} + x + 1$

```python
expr.args
```

    (1, x, x**2, x**3)

```python
expr.args[0]
```

$\displaystyle 1$

```python
expr.args[1]
```

$\displaystyle x$

```python
expr.args[2]
```

$\displaystyle x^{2}$

```python
expr.args[3]
```

$\displaystyle x^{3}$

# Manipulating Expressions

## Simplification

```python
expr = 2 * (x**2 - x) - x * (x + 1)
expr
```

$\displaystyle 2 x^{2} - x \left(x + 1\right) - 2 x$

```python
simplify(expr)
```

$\displaystyle x \left(x - 3\right)$

```python
expr.simplify()
```

$\displaystyle x \left(x - 3\right)$

```python
expr
```

$\displaystyle 2 x^{2} - x \left(x + 1\right) - 2 x$

```python
expr = 2 * cos(x) * sin(x)
expr
```

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

```python
expr.simplify()
```

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

```python
expr = exp(x)*exp(y)
expr
```

$\displaystyle e^{x} e^{y}$

```python
simplify(expr)
```

$\displaystyle e^{x + y}$

Each specific type of simplification can also be carried out with more specialized functions, such as `sympy.trigsimp` and `sympy.powsimp`, for trigonometric and power simplifications, respectively.

Summary of selected SymPy functions for simplifying expressions:

|Function|Description
|:---|:---
|`sympy.simplify`| Attempt various methods and approaches to obtain a simpler form of a given expression.
|`sympy.trigsimp`| Attempt to simplify an expression using trigonometric identities.
|`sympy.powsimp`| Attempt to simplify an expression using laws of powers.
|`sympy.compsimp`| Simplify combinatorial expressions.
|`sympy.ratsimp`| Simplify an expression by writing on a common denominator

## Expand

```python
expr = (x + 1) * (x + 2)
expr
```

$\displaystyle \left(x + 1\right) \left(x + 2\right)$

```python
expand(expr)
```

$\displaystyle x^{2} + 3 x + 2$

```python
sin(x + y).expand(trig=True)
```

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

```python
log(x * y).expand(log=True)
```

$\displaystyle \log{\left(x y \right)}$

```python
a, b = symbols('a, b', positive=True)
log(a*b).expand(log=True)
```

$\displaystyle \log{\left(a \right)} + \log{\left(b \right)}$

```python
expr = exp(a + I*b)
expr
```

$\displaystyle e^{a + i b}$

```python
expr.expand(complex=True)
```

$\displaystyle i e^{a} \sin{\left(b \right)} + e^{a} \cos{\left(b \right)}$

## Factor, Collect and Combine

```python
expr = x**2 - 1
factor(expr)
```

$\displaystyle \left(x - 1\right) \left(x + 1\right)$

```python
expr = x*cos(y) + x*sin(z)
factor(expr)
```

$\displaystyle x \left(\sin{\left(z \right)} + \cos{\left(y \right)}\right)$

```python
expr = log(a) - log(b)
logcombine(expr)
```

$\displaystyle \log{\left(\frac{a}{b} \right)}$

```python
expr = x + y + x*z + x*y
expr.collect(x)
```

$\displaystyle x \left(y + z + 1\right) + y$

```python
expr.collect(y)
```

$\displaystyle x z + x + y \left(x + 1\right)$

## Apart, Together and Cancel

```python
apart(1/(x**2 + 3*x + 2), x)
```

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

```python
together(1 / (y * x + y) + 1 / (1+x))
```

$\displaystyle \frac{y + 1}{y \left(x + 1\right)}$

```python
cancel(y / (y * x + y))
```

$\displaystyle \frac{1}{x + 1}$

## Substitutions

```python
(x + y).subs(x, y)
```

$\displaystyle 2 y$

```python
expr = sin(x*exp(x))
expr
```

$\displaystyle \sin{\left(x e^{x} \right)}$

```python
expr.subs(x, y)
```

$\displaystyle \sin{\left(y e^{y} \right)}$

For muliple substitutions, we can pass a dictionary as first and only argument to `subs`, which maps old symbols or expressions to new symbols or expressions:

```python
expr = sin(x * z)
expr
```

$\displaystyle \sin{\left(x z \right)}$

```python
expr.subs({sin:cos, x:y, z:exp(x)})
```

$\displaystyle \cos{\left(y e^{x} \right)}$

```python
expr = x*y + z**2* x
```

To substitute numerical values in place of symbolic number,
for numerical evaluation, a convenient way of doing this is to
define a dictionary that translates the symbols to numerical values, and passing this dictionary as argument
to the subs method.

```python
expr
```

$\displaystyle x y + x z^{2}$

```python
values = {x:1.25,
         y:0.04,
         z:3.2}
```

```python
expr.subs(values)
```

$\displaystyle 12.85$

# Numerical Evaluation

Even when working with symbolic mathematics, it is almost invariably sooner or later required to evaluate
the symbolic expressions numerically, for example, when producing plots or concrete numerical results.
A SymPy expression can be evaluated using either the `sympy.N` function, or the `evalf` method of SymPy
expression instances.

Both `sympy.N` and the `evalf` method take an optional argument that specifies the number of significant
digits to which the expression is to be evaluated.

```python
N(1 + pi)
```

$\displaystyle 4.14159265358979$

```python
N(pi, 10)
```

$\displaystyle 3.141592654$

```python
(x + 1/pi).evalf()
```

$\displaystyle x + 0.318309886183791$

```python
(x + 1/pi).evalf(5)
```

$\displaystyle x + 0.31831$

`sympy.lambdify` function takes a set of free symbols and an expression as arguments, and generates a function that efficiently evaluates the numerical value of the expression.
The produced function takes the same number of arguments as the number of free symbols passed as first argument to `sympy.lambdify`.

```python
x  = symbols('x')
expr = sin(pi*x*exp(x))
expr
```

$\displaystyle \sin{\left(\pi x e^{x} \right)}$

```python
expr_num = lambdify(x, expr)
expr_num(10)
```

    0.879393997597802

By passing the optional argument 'numpy' as third argument to `sympy.lambdify` SymPy creates a vectorized function that accepts NumPy arrays as input.

```python
expr_num = lambdify(x, expr, 'numpy')
import numpy as np
xvalues = np.arange(1, 10)
expr_num(xvalues)
```

    array([ 0.77394269,  0.64198244,  0.72163867,  0.94361635,  0.20523391,
            0.97398794,  0.97734066, -0.87034418, -0.69512687])

# Calculas

## Derivatives

In SymPy we can calculate the derivative of a function using `sympy.diff`

```python
x = Symbol('x')
f = Function('f')(x)
diff(f, x)
```

$\displaystyle \frac{d}{d x} f{\left(x \right)}$

```python
diff(f, x, x)
```

$\displaystyle \frac{d^{2}}{d x^{2}} f{\left(x \right)}$

```python
diff(f, x, 3)
```

$\displaystyle \frac{d^{3}}{d x^{3}} f{\left(x \right)}$

For multivariate functions -

```python
x, y, z = symbols('x, y, z')
g = Function('g')(x, y)
g.diff(x, y)
```

$\displaystyle \frac{\partial^{2}}{\partial y\partial x} g{\left(x,y \right)}$

```python
g.diff(x, 3, y, 2)
```

$\displaystyle \frac{\partial^{5}}{\partial y^{2}\partial x^{3}} g{\left(x,y \right)}$

For defined functions -

```python
expr = x**4 + x**3 + x**2 + x + 1
expr.diff(x)
```

$\displaystyle 4 x^{3} + 3 x^{2} + 2 x + 1$

```python
expr.diff(x, x)
```

$\displaystyle 2 \left(6 x^{2} + 3 x + 1\right)$

```python
expr = (x + 1)**3 * y**2 *(z - 1)
expr.diff(x, y, z)
```

$\displaystyle 6 y \left(x + 1\right)^{2}$

For trigonometric functions -

```python
expr = sin(x * y) * cos(x / 2)
expr.diff(x)
```

$\displaystyle y \cos{\left(\frac{x}{2} \right)} \cos{\left(x y \right)} - \frac{\sin{\left(\frac{x}{2} \right)} \sin{\left(x y \right)}}{2}$

Alternative way (delayed evaluation) -

```python
expr = exp(cos(x))
d = Derivative(expr, x)
d
```

$\displaystyle \frac{d}{d x} e^{\cos{\left(x \right)}}$

```python
d.doit()
```

$\displaystyle - e^{\cos{\left(x \right)}} \sin{\left(x \right)}$

## Integrals

```python
a, b, x, y = symbols('a, b, x, y')
f = Function('f')(x)
integrate(f)
```

$\displaystyle \int f{\left(x \right)}\, dx$

```python
integrate(f, (x, a, b))
```

$\displaystyle \int\limits_{a}^{b} f{\left(x \right)}\, dx$

```python
integrate(sin(x))
```

$\displaystyle - \cos{\left(x \right)}$

```python
integrate(sin(x), (x, a, b))
```

$\displaystyle \cos{\left(a \right)} - \cos{\left(b \right)}$

```python
integrate(exp(-x**2), (x, 0, oo))
```

$\displaystyle \frac{\sqrt{\pi}}{2}$

SymPy will not be able to give symbolic results for any integral. When SymPy fails to evaluate an integral, an
instance of `sympy.Integral`, representing the formal integral, is returned instead.

```python
integrate(sin(x*cos(x)), x)
```

$\displaystyle \int \sin{\left(x \cos{\left(x \right)} \right)}\, dx$

Multibariable expressions -

```python
integrate(sin(x*exp(y)), x)
```

$\displaystyle - e^{- y} \cos{\left(x e^{y} \right)}$

```python
expr = (x + y)**2
integrate(expr, x)
```

$\displaystyle \frac{x^{3}}{3} + x^{2} y + x y^{2}$

By passing more than one symbol, or more than one tuple that contain symbols and their integration limits, we can carry out multiple integration:

```python
expr = (x + y)**2
integrate(expr, (x, 0, 1), (y, 0, 1))
```

$\displaystyle \frac{7}{6}$

## Series

```python
x = Symbol('x')
f = Function('f')(x)
series(f, x)
```

$\displaystyle f{\left(0 \right)} + x \left. \frac{d}{d x} f{\left(x \right)} \right|_{\substack{ x=0 }} + \frac{x^{2} \left. \frac{d^{2}}{d x^{2}} f{\left(x \right)} \right|_{\substack{ x=0 }}}{2} + \frac{x^{3} \left. \frac{d^{3}}{d x^{3}} f{\left(x \right)} \right|_{\substack{ x=0 }}}{6} + \frac{x^{4} \left. \frac{d^{4}}{d x^{4}} f{\left(x \right)} \right|_{\substack{ x=0 }}}{24} + \frac{x^{5} \left. \frac{d^{5}}{d x^{5}} f{\left(x \right)} \right|_{\substack{ x=0 }}}{120} + O\left(x^{6}\right)$

To change the point around which the function is expanded, we specify $x_0$ argument -

```python
x0 = Symbol('{x_0}')
f.series(x, x0, n=2)
```

$\displaystyle f{\left({x_0} \right)} + \left(x - {x_0}\right) \left. \frac{d}{d \xi_{1}} f{\left(\xi_{1} \right)} \right|_{\substack{ \xi_{1}={x_0} }} + O\left(\left(x - {x_0}\right)^{2}; x\rightarrow {x_0}\right)$

```python
f.series(x, x0, n=2).removeO()
```

$\displaystyle \left(x - {x_0}\right) \left. \frac{d}{d \xi_{1}} f{\left(\xi_{1} \right)} \right|_{\substack{ \xi_{1}={x_0} }} + f{\left({x_0} \right)}$

For specified functions -

```python
sin(x).series()
```

$\displaystyle x - \frac{x^{3}}{6} + \frac{x^{5}}{120} + O\left(x^{6}\right)$

```python
cos(x).series()
```

$\displaystyle 1 - \frac{x^{2}}{2} + \frac{x^{4}}{24} + O\left(x^{6}\right)$

```python
exp(x).series()
```

$\displaystyle 1 + x + \frac{x^{2}}{2} + \frac{x^{3}}{6} + \frac{x^{4}}{24} + \frac{x^{5}}{120} + O\left(x^{6}\right)$

```python
(1/(1 + x)).series()
```

$\displaystyle 1 - x + x^{2} - x^{3} + x^{4} - x^{5} + O\left(x^{6}\right)$

For arbitrary expressions of symbols and functions, which in general can also be evaluated.

```python
expr = cos(x)/(1 + sin(x*y))
expr.series(x, n=4)
```

$\displaystyle 1 - x y + x^{2} \left(y^{2} - \frac{1}{2}\right) + x^{3} \left(- \frac{5 y^{3}}{6} + \frac{y}{2}\right) + O\left(x^{4}\right)$

```python
expr.series(y, n=4)
```

$\displaystyle \cos{\left(x \right)} - x y \cos{\left(x \right)} + x^{2} y^{2} \cos{\left(x \right)} - \frac{5 x^{3} y^{3} \cos{\left(x \right)}}{6} + O\left(y^{4}\right)$

## Limits

In SymPy, limits can be evaluated using the `sympy.limit` function, which takes an expression, a symbol it depends on, as well as the value that the symbol approaches in the limit.

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

$\displaystyle 1$

```python
f = Function('f')
x, h = symbols('x, h')
diff_limit = (f(x + h) - f(x))/h
limit(diff_limit.subs(f, cos), h, 0)
```

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

```python
limit(diff_limit.subs(f, sin), h, 0)
```

$\displaystyle \cos{\left(x \right)}$

## Sums and Products

```python
n = symbols('n', integer=True)
x = Sum(1/n**2, (n, 1, oo))
x
```

$\displaystyle \sum_{n=1}^{\infty} \frac{1}{n^{2}}$

```python
x.doit()
```

$\displaystyle \frac{\pi^{2}}{6}$

```python
x = Product(n, (n, 1, 7))
x
```

$\displaystyle \prod_{n=1}^{7} n$

```python
x.doit()
```

$\displaystyle 5040$

# Equations

```python
x = Symbol('x')
solve(x**2 + 2*x -3)
```

    [-3, 1]

```python
a, b, c, x = symbols('a, b, c, x')
solve(a*x**2 + b*x + c, x)
```

    [(-b + sqrt(-4*a*c + b**2))/(2*a), -(b + sqrt(-4*a*c + b**2))/(2*a)]

Trigonometric Functions -

```python
solve(sin(x) - cos(x), x)
```

    [-3*pi/4, pi/4]

Solving a system of equations for more than one unknown variable in SymPy is a straightforward generalization of the procedure used for univariate equations. Instead of passing a single expression as first argument to `sympy.solve`, a list of expressions that represent the system of equations is used, and in this case the second argument should be a list of symbols to solve for.

```python
eq1 = x + 2*y -1
eq2 = x - y + 1

solve([eq1, eq2], [x, y], dict=True)
```

    [{x: -1/3, y: 2/3}]

```python
eq1 = x**2 - y
eq2 = y**2 - x

sols = solve([eq1, eq2], [x, y], dict=True)
sols
```

    [{x: 0, y: 0},
     {x: 1, y: 1},
     {x: (-1/2 - sqrt(3)*I/2)**2, y: -1/2 - sqrt(3)*I/2},
     {x: (-1/2 + sqrt(3)*I/2)**2, y: -1/2 + sqrt(3)*I/2}]

Verification -

```python
[eq1.subs(sol).simplify() == 0 and eq2.subs(sol).simplify() == 0 for sol in sols]
```

    [True, True, True, True]

# Linear Algebra

```python
Matrix([1, 2])
```

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

```python
Matrix([ [1, 2 ] ])
```

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

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

$\displaystyle \left[\begin{matrix}1 & 2\\3 & 4\end{matrix}\right]$

```python
Matrix(3, 4, lambda m, n: 10*m + n)
```

$\displaystyle \left[\begin{matrix}0 & 1 & 2 & 3\\10 & 11 & 12 & 13\\20 & 21 & 22 & 23\end{matrix}\right]$

```python
a, b, c, d = symbols('a, b, c, d')
M = Matrix([[a, b], [c, d] ] )
M
```

$\displaystyle \left[\begin{matrix}a & b\\c & d\end{matrix}\right]$

```python
M * M
```

$\displaystyle \left[\begin{matrix}a^{2} + b c & a b + b d\\a c + c d & b c + d^{2}\end{matrix}\right]$

```python
x = Matrix(symbols("x_1, x_2"))
x
```

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

```python
M * x
```

$\displaystyle \left[\begin{matrix}a x_{1} + b x_{2}\\c x_{1} + d x_{2}\end{matrix}\right]$

# Practical Examples

## 1. Newton's Law of Cooling
### By Analytical Way using SymPy
$$\frac{dT(t)}{dt} = k(T(t) - T_a)$$
at $t=0$, $T(0)=T_0$.

```python
from sympy import *
```

```python
T, Ta, T0, k, t = symbols('T, T_a, T_0, k, t')
T  = Function('T')
diff_eq = Eq(T(t).diff(t),-k*(T(t) - Ta))
diff_eq
```

$\displaystyle \frac{d}{d t} T{\left(t \right)} = - k \left(- T_{a} + T{\left(t \right)}\right)$

```python
sol = dsolve(diff_eq)
sol
```

$\displaystyle T{\left(t \right)} = C_{1} e^{- k t} + T_{a}$

```python
ic = Eq(sol.rhs.subs(t, 0), T0)
ic
```

$\displaystyle C_{1} + T_{a} = T_{0}$

```python
const = solve(ic)
const
```

    [{C1: T_0 - T_a}]

```python
final_sol = sol.subs(const[0])
final_sol
```

$\displaystyle T{\left(t \right)} = T_{a} + \left(T_{0} - T_{a}\right) e^{- k t}$

```python
T =lambdify([Ta, T0, k, t], final_sol.rhs)
T
```

    <function _lambdifygenerated(T_a, T_0, k, t)>

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

k = 0.5
Ta = 22
T0 = 100
t = np.linspace(0, 10, 100)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(t, T(Ta, T0, k, t), color='red', label="""$T_a$ = %0.1f,
$T_0$ = %0.1f,
 k  = %0.2f"""%(Ta, T0, k))
ax.set_xlabel('Time ($t$) in sec.', fontsize=16)
ax.set_ylabel('Temperature ($T$) in $^{\circ}C$', fontsize=16)
ax.set_title('Cooling Curve', fontsize=20)

# removing top and right spines
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')

# move bottom and left spine to x = 0 and y = 0
ax.spines['bottom'].set_position(('data', 0))
ax.spines['left'].set_position(('data', 0))

ax.set_xticks(range(0, 11, 1))
ax.set_yticks(range(0, 110, 10))

ax.legend(frameon=False)
ax.grid(False)

```

## 2. Damped harmonic Oscillator
$$\frac{d^2 x(t)}{dt^2} + 2\gamma \omega_0 \frac{d x(t)}{dt} + \omega_0^2 x(t) = 0$$
where $x(t)$ is the position of the oscillator at time $t$, $\omega_0$ is the frequency of the oscillator in the undamped case and $\gamma$ is the damping ratio.

```python
t, omega0, gamma = symbols('t, omega_0, gamma', positive=True)
x                = Function('x')

ode = x(t).diff(t, 2) + 2*gamma*omega0*x(t).diff(t) + omega0**2 *x(t)
ode
```

$\displaystyle 2 \gamma \omega_{0} \frac{d}{d t} x{\left(t \right)} + \omega_{0}^{2} x{\left(t \right)} + \frac{d^{2}}{d t^{2}} x{\left(t \right)}$

```python
Eq(ode)
```

$\displaystyle 2 \gamma \omega_{0} \frac{d}{d t} x{\left(t \right)} + \omega_{0}^{2} x{\left(t \right)} + \frac{d^{2}}{d t^{2}} x{\left(t \right)} = 0$

```python
ode_sol = dsolve(Eq(ode))
ode_sol
```

$\displaystyle x{\left(t \right)} = C_{1} e^{\omega_{0} t \left(- \gamma - \sqrt{\gamma - 1} \sqrt{\gamma + 1}\right)} + C_{2} e^{\omega_{0} t \left(- \gamma + \sqrt{\gamma - 1} \sqrt{\gamma + 1}\right)}$

#### Initial conditions:

> 1.  at time $t=0$, displacement of the oscillator, $x(t) = 1$
1.   at time $t=0$, velocity of the oscillator, $\frac{d x(t)}{dt} = 0$

```python
ic1 = x(t).subs(t, 0)
ic2 = x(t).diff(t).subs(t, 0)
ics = {ic1:1, ic2:0}
ics
```

    {x(0): 1, Subs(Derivative(x(t), t), t, 0): 0}

```python
eq1 = Eq(ode_sol.rhs.subs(t, 0), 1)
eq2 = Eq(ode_sol.rhs.diff(t).subs(t, 0), 0)
constants = solve([eq1, eq2])
x_t_sol = ode_sol.subs(constants[0])
x_t_sol

```

$\displaystyle x{\left(t \right)} = \left(- \frac{\gamma}{2 \sqrt{\gamma^{2} - 1}} + \frac{1}{2}\right) e^{\omega_{0} t \left(- \gamma - \sqrt{\gamma - 1} \sqrt{\gamma + 1}\right)} + \left(\frac{\gamma}{2 \sqrt{\gamma^{2} - 1}} + \frac{1}{2}\right) e^{\omega_{0} t \left(- \gamma + \sqrt{\gamma - 1} \sqrt{\gamma + 1}\right)}$

```python
x_t_critical = limit(x_t_sol.rhs, gamma, 1)
x_t_critical
```

$\displaystyle \left(\omega_{0} t + 1\right) e^{- \omega_{0} t}$

```python
fig, ax = plt.subplots(figsize=(14, 8))
tt = np.linspace(0, 3, 300)
w0 = 2* np.pi
for g in [0.1, 0.5, 1, 2.0, 5.0]:
    if g == 1:
        x_t = lambdify(t, x_t_critical.subs({omega0:w0, gamma:g}), 'numpy')
    else:
        x_t = lambdify(t, x_t_sol.rhs.subs({omega0:w0, gamma:g}), 'numpy')
    ax.plot(tt, x_t(tt).real,  label="$\gamma = %.1f$" % g )
ax.set_xlabel(r"$t$", fontsize=18)
ax.set_ylabel(r"$x(t)$", fontsize=18)
ax.set_title('Damped Harmonic Oscillator', fontsize=20)

# removing top and right spines
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')

# move bottom and left spine to x = 0 and y = 0
ax.spines['bottom'].set_position(('data', 0))
ax.spines['left'].set_position(('data', 0))

ax.set_xticks(np.arange(0, 4, 1))
ax.set_yticks(np.arange(-1, 1.5, 0.5))

ax.legend(frameon=False)
ax.grid(False)
plt.legend(frameon=False, loc='upper right')
plt.grid(False)
```

```python

```


```python

from sympy import *
t, dt, n, w = symbols('t dt n w', real=True)

# Finite difference operators

def D_t_forward(u):
    return (u(t + dt) - u(t))/dt

def D_t_backward(u):
    return (u(t) - u(t-dt))/dt

def D_t_centered(u):
    return (u(t + dt/2) - u(t-dt/2))/dt

def D_2t_centered(u):
    return (u(t + dt) - u(t-dt))/(2*dt)

def D_t_D_t(u):
    return (u(t + dt) - 2*u(t) + u(t-dt))/(dt**2)

op_list = [D_t_forward, D_t_backward,
           D_t_centered, D_2t_centered, D_t_D_t]

def ft1(t):
    return t

def ft2(t):
    return t**2

def ft3(t):
    return t**3

def f_expiwt(t):
    return exp(I*w*t)

def f_expwt(t):
    return exp(w*t)

func_list = [ft1, ft2, ft3, f_expiwt, f_expwt]
import inspect

for func in func_list:
    print(('\n--- Function:', inspect.getsource(func), '---'))
    for op in op_list:
        print(('\nOperator:', op.__name__))
        f = func
        e = op(f)
        e = simplify(expand(e))
        print(('simplify(expand(operator(function)):', e))
        if func in [f_expiwt, f_expwt]:
            e = e/f(t)
        e = e.subs(t, n*dt)
        print(('t -> n*dt:', expand(e)))
        print(('factor(simplify(expand(e))):', factor(simplify(expand(e)))))

# https://github.com/sympy/sympy/wiki/Generating-tables-of-derivatives-and-integrals
```

```python

```

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


```python
from sympy import *
init_printing()

'''
r_GEO = 36000 + 6371 KM
r_LEO = 2000 + 6371 KM

G = 6.674e-11
Me = 5.972e24
'''

M, E = symbols("M E", Functions = True)
e_c, a, G, M_e, r, mu = symbols("e_c a G M_e r mu", Contstants = True)
T_circular, T_elliptical, T_GEO, T_GTO, T_LEO, r_LEO, r_GEO, T_tot = symbols("T_circular T_elliptical T_GEO T_GTO T_LEO r_LEO r_GEO T_tot", Constants = True)
t, x, y, Y = symbols("t x y Y", Variables = True)

mu_calculated = (6.674e-11 * 5.972e24)
```

The orbital period of a circular Orbit:

```python
Eq(T_circular, 2*pi*sqrt(r**3 / mu))
```

Where mu is:

```python
Eq(mu, G*M_e)
```

Then, the GEO's orbital period in hours is:

```python
r_GEO_Calculated = (36000 + 6371)*1000
T_GEO_Calculated = 2*pi*sqrt(r_GEO_Calculated**3 / mu_calculated)
Eq(T_GEO, T_GEO_Calculated.evalf()/60/60)
```

And the LEO's orbital period in hours is:

```python
r_LEO_Calculated = (2000 + 6371)*1000
T_LEO_Calculated = 2*pi*sqrt(r_LEO_Calculated**3 / mu_calculated)
Eq(T_LEO, T_LEO_Calculated.evalf()/60/60)
```

_____________________________________________
# Finding the GTO.
The goal is to get both 'e' (the eccentricity of our GTO) and 'a' (its semi-major axis). So, we need 2 eqns.

The equation of the GEO (A circle equation):

```python
geo = Eq(y**2, r**2-x**2)
geo
```

The equation of the GTO (An ellipse equation):

```python
gto = Eq(((x+a*e_c)**2/a**2)+(y**2/a**2*(1-e_c**2)), 1)
gto
```

We Wanna solve these two eqns to get the semi-major axis of our GTO and the raduis of our LEO.

first, substitute the GEO's eqn in the GTO's eqn.

```python
toSolve = gto.subs({y**2:geo.rhs})
toSolve
```

Now we can solve for x.

```python
solX = solveset(toSolve, x)
solX
```

Now we can calculate the y coordinate for each x.

```python
solY1 = solveset(Eq(geo.lhs, geo.rhs.subs({x:list(solX)[0]})), y)
solY1
```

```python
solY2 = solveset(Eq(geo.lhs, geo.rhs.subs({x:list(solX)[1]})), y)
solY2
```

We have 4 different possible points for the intersection between a circle and an ellipse, but the intersection between the GEO and the GTO is going to be at only one point with an x coordinate of '-r_GEO' (the radius of the GEO).

Now, we can get the first eqn.

```python
geoAndGtoIntersection = solveset(Eq(list(solX)[0], -r_GEO).subs({r:r_GEO}), a)
geoAndGtoIntersection
```

Surbrisingly, there are 2 possible values for a. But we're not interrested in the negative value. So our first eqn is:

```python
eqn1 = Eq(a, list(list(geoAndGtoIntersection.args)[2])[1])
eqn1
```

To get another eqn, we can do the same but this time with the LOE.

The intersection between our LEO and GTO is exactly at the x coordinate of 'r_LEO'.

```python
gtoAndLeoIntersection = solveset(Eq(list(solX)[1], r).subs({r:r_LEO}), a)
gtoAndLeoIntersection
```

Again, there are 2 possible values for 'r_LEO' but we need the positive one.

```python
eqn2 = Eq(a, list(list(gtoAndLeoIntersection.args)[2])[0])
eqn2
```

This is the positive because 0 < e_c < 1.

Now, we have 2 eqns and 2 variables. And we're ready to get 'a' and 'e_c'

```python
e_c_Exp = Eq(e_c, solveset(eqn1.subs({a:eqn2.rhs, r_GEO:r_GEO_Calculated, r_LEO:r_LEO_Calculated}), e_c).args[0])
e_c_Calculated = e_c_Exp.rhs
e_c_Exp
```

```python
s = solveset(eqn2.subs({r_LEO:r_LEO_Calculated, e_c:e_c_Calculated})).args[0]
a_Exp = Eq(a, s)
a_Calculated = a_Exp.rhs
a_Exp
```

There's another way for finding 'a'.

```python
p1 = plot(sqrt(r_GEO_Calculated**2-x**2), -sqrt(r_GEO_Calculated**2-x**2), sqrt(r_LEO_Calculated**2-x**2), -sqrt(r_LEO_Calculated**2-x**2), sqrt(a_Calculated**2*(1-e_c_Calculated**2)*(1-((x+a_Calculated*e_c_Calculated)**2/a_Calculated**2))), -sqrt(a_Calculated**2*(1-e_c_Calculated**2)*(1-((x+a_Calculated*e_c_Calculated)**2/a_Calculated**2))),(x, -5*10**7, 5*10**7),xlim = (-7.5*10**7, 7.5*10**7), ylim=((-5*10**7, 5*10**7)))
```

From the geometry we can say that:

```python
Eq(a, (r_LEO + r_GEO)/2)
```

This could've saved us a lot of math work :)

__________________________________
# Now let's calculate the periods.

The orbital period of an elliptical orbit is:

```python
Eq(T_elliptical, 2*pi*sqrt(a**3 / mu))
```

```python
T_GTO_Calculated = 2*pi*sqrt(a_Calculated**3/mu_calculated)
Eq(T_GTO, T_GTO_Calculated.evalf()/60/60)
```

So, the total time required to put our satellite in a GEO using Hohmann transfer is:

```python
Eq(T_tot, T_GTO / 2 + T_LEO / 2)
```

The total time required to put our satellite in a GEO of a 36,000 Kilometers above sea level in hours is:

```python
Eq(T_tot, (T_GTO_Calculated / 2 + T_LEO_Calculated / 2).evalf()/60/60)
```


# EIGENVALUES and EIGENVECTORS
## Prachee Javiya
---
# Notes

  * The most basic equation for eigenvalues is $Ax=\lambda x$. The number $\lambda$ is an eigenvalue of A.
   * To solve the above equation, calculate the roots of $(A-\lambda I)x=0$
   * Calculate the eigenvectors corresponding to an eigenvalue
  * The eigenvectors "x" do NOT change direction when you multiply by A.
  * Eigenvectors make understanding linear transformations easy.
  * The eigenvalue $\lambda$ tells whether the special vector x is stretched or shrunk or reversed or left unchanged-when it is multiplied by A
   * When $\lambda$ is 0, the vector x lies in the nullspace of A.
  * If A is the identity matrix, every vector has Ax= x. All vectors are eigenvectors of I.
  * |A-$\lambda$I|=0 ,always. Reason:For a particular value of $\lambda$, the vector Ax is parallel to $\lambda$x.
 #### Calculating eigenvectors and eigenvalues :

* The 2x2 identity matrix has $\lambda$=1 which occurs twice.

```python
import sympy as sp
```

```python
A=sp.Matrix([[1,0],[0,1]])
A.eigenvals()
```

    {1: 2}

* Eigenvalues for a 2x2 permutation matrix
 * any x in the plane is unchanged by P. The equation hence formed is Px=x
 * Any x perpendicular to the plane, equation becomes Px=0

```python
B=sp.Matrix([[0,1],[1,0]])
B.eigenvals()
```

    {-1: 1, 1: 1}

Let λ be an eigenvalue of P for the eigenvector v. You have $\lambda^2v=P^2v=Pv=\lambda v$. Because v≠0 it must be $\lambda^2=\lambda$. The solutions of the last equation are λ1=0 and λ2=1. Those are the only possible eigenvalues the projection might have.

### Properties of eigenvectors and eigenvalues:
 - Trace of the matrix A is equal to the sum of its eigenvalues.

 - Additive property for calculating Evalues of two matrices does not hold.
      >$ Ax=\lambda x$
      >$ Bx=\gamma x$
      >$ (A+B)x\neq (\lambda+\gamma) x $
 - Because it is not necessary that the vector x will be same for both matrices A and B
 - A matrix A will have zero as an eigenvalue if and only if it is singular.

 - Eigenvalue and eigenvector of $A^{-1}$ $$ Av=\lambda v$$ Multiplying both sides by $A^{-1}$, we get, $$ v=\lambda A^{-1}v$$ $$=A^{-1}v=\frac{1}{\lambda} v $$

**NOTE** :

 When $\lambda=0, A^{-1}$ does not exist.

- When we take a multiple of I (eg 3I) the eigenvalues get multiplied by a factor 3 and the eigenvectors remain **same**

```python
C=sp.Matrix([[3,1],[1,3]])
C.eigenvals()
```

    {4: 1, 2: 1}

```python
C.eigenvects()
```

    [(2, 1, [Matrix([
       [-1],
       [ 1]])]), (4, 1, [Matrix([
       [1],
       [1]])])]

* Eigenvalues of anti-symmetric matrices are purely imaginary
* Repeated eigenvalues leads to shortage of eigenvectors(not enough linearly independant eigenvectors)

  _DEGENERATE MATRIX_

```python
D=sp.Matrix([[3,1],[0,3]])
D.eigenvals()
```

    {3: 2}

```python
D.eigenvects()
```

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

### DIAGONALISATION
##### A matrix is diagonalisable when there exists a matrix S and $\Lambda$ such that $$A=S\Lambda S^{-1}$$
---
* Λ is the diagonal matrix, with the entries being the corresponding eigenvalues of A
* $AS=S\Lambda$ (Be careful when the matrix S is imaginary,conjugate terms will take part, $A^H={A^T}^C$,where $A^H$ is a hermetian matrix
* If a square matrix A has a full set of eigenvectors, then the matrix can be constructed by this formula.
* Multiply by $S^{-1}$ on both sides to get the diagonal matrix i.e. $$AS=S \Lambda $$ $$S^{-1}AS=\Lambda$$

_Diagonalizability is concerned with the number of eigenvectors - too few or enough_

#### AM/GM
---
AM(Algebraic multiplicity) is the number of times an **eigenvalue** appears in the characteristic equation.

GM(Geometric multiplicity) is the number of **eigenvectors** corresponding to an eigenvalue.

### Real Symmetric Matrices-Eigenvalues
---
- When $A=A^T$ for any matrix A, it is known as a symmetric matrix
- Evalues for a symmetric matrix are real
- Eigenvectors of real symmetric matrices are **orthogonal**
$\therefore$ the matrix S will have orthonormal columns

And, $$A=Q\Lambda Q^{-1}$$
- For orthonormal columns, $A^T=A^{-1}$
$$\therefore A=Q\Lambda Q^T$$
- Every symmetric matrix is a combination of perpendicular projection matrices

### Difference equation
---
$$u_{k+1}=Au_{k}$$
Given vector $u_0$,

$u_1=Au_0, u_2=A^2u_0.... u_k=A^ku_0$
To solve, $$u_0=c_1x_1+c_2x_2+..+c_nx_n=Sc$$
$$Au_0=\lambda_1x_1+c_2\lambda_2x_2+..+c_n\lambda_nx_n$$
$$A^{100}u_0=\lambda_1^{100}x_1+c_2\lambda_2^{100}x_2+..+c_n\lambda_n^{100}x_n$$
$$=\Lambda^{100}Sc$$

## Fibonacci equation
---
$$F_{k+2}=F_{k+1}+F_k$$
$$F_{k+1}=F_{k+1}$$
$$ u_k=
\begin{bmatrix}
F_{k+1}\\
F_k
\end{bmatrix}
$$
$$u_{k+1}=
\begin{bmatrix}
1 & 1\\ 1 & 0
\end{bmatrix}u_k
$$

## Positive Definiteness of symmetric matrix
How to tell if a matrix is positive symmetric?
$$\begin{bmatrix}
a & b \\
c & d
\end{bmatrix}
$$
 1. Eigenvalues are positive
 2. $a>0 , ac-b^2>0$
 3. $x^TAx>0$

**Borderline case example,**

```python
import sympy as sp
A=sp.Matrix([[2,6],[6,18]])
A
```

$\displaystyle \left[\begin{matrix}2 & 6\\6 & 18\end{matrix}\right]$

Testing:
$$
\begin{bmatrix}
x_1 & x_2
\end{bmatrix}
\begin{bmatrix}
2 & 6\\
6 & 18
\end{bmatrix}
\begin{bmatrix}
x_1 \\
x_2
\end{bmatrix}
$$

$$=2x_1^2+12x_1x_2+18x_2^2$$

Quadratic form : $ax^2+2bxy+cy^2$

**IF THE ABOVE QUADRATIC IS POSITIVE FOR ALL x1,x2, THEN A IS POSITIVE DEFINITE**

In the above case, the matrix is definite psitive for all values greater than 18

### Left Inverse/Right inverse and Pseude inverse
---
- Left Inverse
  - For a full rank(columns are independant) mxn matrix,  $(A^TA)^{-1}A^T$ is the left inverse of the matrix A.
  - A rectangular matrix cannot have two sided inverse because $m \neq n $ and there will be some free variables around
- Right Inverse
  - $A^T(AA^T)^{-1}$ is the right inverse.$AA^T(AA^T)^{-1}=I$)
  - We have independant rows and $N(A^T)={0}$.
  - Rank r=m<n
  - Infinite solutions
- Pseudo Inverse
  - It comess from SVD. $ A=U\Sigma V^T$ and
$$ \Sigma=
\begin{bmatrix}
\sigma_1 &0&0&0&0\\
0&\sigma_2&0&0&0\\
0&0&\sigma_3&0&0\\
0&0&0&.&. \\
.&.&.&.&\sigma_r
\end{bmatrix}
_{m\times n}
$$
then,Pseudo inverse of $\Sigma$ will be,
$$\Sigma^+=
\begin{bmatrix}
\frac{1}{\sigma_1} &0&0&0&0\\
0&\frac{1}{\sigma_2}&0&0&0\\
0&0&\frac{1}{\sigma_3}&0&0\\
0&0&0&.&. \\
.&.&.&.&\frac{1}{\sigma_r}
\end{bmatrix}
_{n\times m}
$$

```python

```


Introduction

From

What is Symbolic Computation?

Symbolic computation deals with the computation of mathematical objects symbolically.  This means that the mathematical objects are represented exactly, not approximately, and mathematical expressions with unevaluated variables are left in symbolic form.

Let's take an example. Say we wanted to use the built-in Python functions to compute square roots. We might do something like this

```julia
   >>> import math
   >>> math.sqrt(9)
   3.0
```

In Julia:

Of course, sqrt is already there:

```julia
sqrt(9)
```

    3.0

9 is a perfect square, so we got the exact answer, 3. But suppose we computed the square root of a number that isn't a perfect square

```julia
   >>> math.sqrt(8)
   2.82842712475
```

In Julia:

```julia
sqrt(8)
```

    2.8284271247461903

Here we got an approximate result. 2.82842712475 is not the exact square root of 8 (indeed, the actual square root of 8 cannot be represented by a finite decimal, since it is an irrational number).  If all we cared about was the decimal form of the square root of 8, we would be done.

But suppose we want to go further. Recall that $\sqrt{8} = \sqrt{4\cdot 2} = 2\sqrt{2}$.  We would have a hard time deducing this from the above result. This is where symbolic computation comes in.  With a symbolic computation system like SymPy, square roots of numbers that are not perfect squares are left unevaluated by default

```julia
   >>> import sympy
   >>> sympy.sqrt(3)
   sqrt(3)
```

In Julia:

```julia
using SymPy
sympy.sqrt(3)
```

\begin{equation*}\sqrt{3}\end{equation*}

When SymPy is loaded, the sqrt function is overloaded for symbolic objects, so this could also be done through:

```julia
sqrt(Sym(3))
```

\begin{equation*}\sqrt{3}\end{equation*}

Furthermore–-and this is where we start to see the real power of symbolic computation–-symbolic results can be symbolically simplified.

```julia
   >>> sympy.sqrt(8)
   2*sqrt(2)
```

In Julia:

```julia
sympy.sqrt(8)
```

\begin{equation*}2 \sqrt{2}\end{equation*}

A More Interesting Example

The above example starts to show how we can manipulate irrational numbers exactly using SymPy.  But it is much more powerful than that.  Symbolic computation systems (which by the way, are also often called computer algebra systems, or just CASs) such as SymPy are capable of computing symbolic expressions with variables.

As we will see later, in SymPy, variables are defined using symbols. Unlike many symbolic manipulation systems, variables in SymPy must be defined before they are used (the reason for this will be discussed in the :ref:next section <tutorial-gotchas-symbols>).

Let us define a symbolic expression, representing the mathematical expression x + 2y.

```julia
   >>> from sympy import symbols
   >>> x, y = symbols('x y')
   >>> expr = x + 2*y
   >>> expr
   x + 2*y
```

In Julia:

the command from sympy import * is essentially run (only functions are "imported", not all objects), so this becomes the same after adjusting the quotes:

```julia
x, y = symbols("x y")
expr = x + 2*y
expr
```

\begin{equation*}x + 2 y\end{equation*}

Note that we wrote x + 2*y just as we would if x and y were ordinary Python variables. But in this case, instead of evaluating to something, the expression remains as just x + 2*y.  Now let us play around with it:

```julia
   >>> expr + 1
   x + 2*y + 1
   >>> expr - x
   2*y
```

In Julia:

```julia
expr + 1
```

\begin{equation*}x + 2 y + 1\end{equation*}

```julia
expr - x
```

\begin{equation*}2 y\end{equation*}

Notice something in the above example.  When we typed expr - x, we did not get x + 2*y - x, but rather just 2*y.  The x and the -x automatically canceled one another.  This is similar to how sqrt(8) automatically turned into 2*sqrt(2) above.  This isn't always the case in SymPy, however:

```julia
   >>> x*expr
   x*(x + 2*y)
```

In Julia:

```julia
x*expr
```

\begin{equation*}x \left(x + 2 y\right)\end{equation*}

Here, we might have expected x(x + 2y) to transform into x^2 + 2xy, but instead we see that the expression was left alone.  This is a common theme in SymPy.  Aside from obvious simplifications like x - x = 0 and \sqrt{8} = 2\sqrt{2}, most simplifications are not performed automatically.  This is because we might prefer the factored form x(x + 2y), or we might prefer the expanded form x^2 + 2xy.  Both forms are useful in different circumstances. In SymPy, there are functions to go from one form to the other

```julia
   >>> from sympy import expand, factor
   >>> expanded_expr = expand(x*expr)
   >>> expanded_expr
   x**2 + 2*x*y
   >>> factor(expanded_expr)
   x*(x + 2*y)
```

In Julia:

```julia
expanded_expr = expand(x*expr)
expanded_expr
```

\begin{equation*}x^{2} + 2 x y\end{equation*}

```julia
factor(expanded_expr)
```

\begin{equation*}x \left(x + 2 y\right)\end{equation*}

The Power of Symbolic Computation

The real power of a symbolic computation system such as SymPy is the ability to do all sorts of computations symbolically.  SymPy can simplify expressions, compute derivatives, integrals, and limits, solve equations, work with matrices, and much, much more, and do it all symbolically.  It includes modules for plotting, printing (like 2D pretty printed output of math formulas, or \LaTeX), code generation, physics, statistics, combinatorics, number theory, geometry, logic, and more. Here is a small sampling of the sort of symbolic power SymPy is capable of, to whet your appetite.

```julia
 >>> from sympy import *
 >>> x, t, z, nu = symbols('x t z nu')
```

In Julia:

again, the functions in the sympy module are already imported:

```julia
x, t, z, nu = symbols("x t z nu")
```

    (x, t, z, nu)

This will make all further examples pretty print with unicode characters.

```julia
 >>> init_printing(use_unicode=True)
```

In Julia:

The printing in Julia is controlled by show and the appropriate MIME type.

Take the derivative of $\sin{(x)}e^x$.

```julia
 >>> diff(sin(x)*exp(x), x)
  x           x
 ℯ ⋅sin(x) + ℯ ⋅cos(x)
```

In Julia:

```julia
diff(sin(x)*exp(x), x)
```

\begin{equation*}e^{x} \sin{\left (x \right )} + e^{x} \cos{\left (x \right )}\end{equation*}

Compute $\int(e^x\sin{(x)} + e^x\cos{(x)})\,dx$.

```julia
 >>> integrate(exp(x)*sin(x) + exp(x)*cos(x), x)
  x
 ℯ ⋅sin(x)
```

In Julia:

```julia
integrate(exp(x)*sin(x) + exp(x)*cos(x), x)
```

\begin{equation*}e^{x} \sin{\left (x \right )}\end{equation*}

Compute $\int_{-\infty}^\infty \sin{(x^2)}\,dx$.

```julia
 >>> integrate(sin(x**2), (x, -oo, oo))
 √2⋅√π
 ─────
   2
```

In Julia:

In Julia ** is ^:

```julia
integrate(sin(x^2), (x, -oo, oo))
```

\begin{equation*}\frac{\sqrt{2} \sqrt{\pi}}{2}\end{equation*}

Find $\lim_{x\to 0}\frac{\sin{(x)}}{x}$.

```julia
 >>> limit(sin(x)/x, x, 0)
 1
```

In Julia:

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

\begin{equation*}1\end{equation*}

Solve $x^2 - 2 = 0$.

```julia
 >>> solve(x**2 - 2, x)
 [-√2, √2]
```

In Julia:

```julia
solve(x^2 - 2, x)
```

\[ \left[ \begin{array}{r}- \sqrt{2}\\\sqrt{2}\end{array} \right] \]

Solve the differential equation y'' - y = e^t.

```julia
 >>> y = Function('y')
 >>> dsolve(Eq(y(t).diff(t, t) - y(t), exp(t)), y(t))
            -t   ⎛     t⎞  t
 y(t) = C₂⋅ℯ   + ⎜C₁ + ─⎟⋅ℯ
                 ⎝     2⎠
```

In Julia:

Function is not a function, so is not exported. We must qualify its use:

```julia
y = sympy.Function("y")
dsolve(Eq(y(t).diff(t, t) - y(t), exp(t)), y(t))
```

\begin{equation*}y{\left (t \right )} = C_{2} e^{- t} + \left(C_{1} + \frac{t}{2}\right) e^{t}\end{equation*}

This is made more familiar looking with the SymFunction class:

```julia
y = SymFunction("y")
dsolve(y''(t) - y(t) - exp(t), y(t))
```

\begin{equation*}y{\left (t \right )} = C_{2} e^{- t} + \left(C_{1} + \frac{t}{2}\right) e^{t}\end{equation*}

Find the eigenvalues of \left[\begin{smallmatrix}1 & 2\\2 & 2\end{smallmatrix}\right].

```julia
 >>> Matrix([[1, 2], [2, 2]]).eigenvals()
 ⎧3   √17       √17   3   ⎫
 ⎨─ + ───: 1, - ─── + ─: 1⎬
 ⎩2    2         2    2   ⎭
```

In Julia:

Like Function, Matrix is not imported and its use must by qualified:

```julia
out = sympy.Matrix([[1, 2], [2, 2]]).eigenvals()
```

    Dict{Any,Any} with 2 entries:
      -sqrt(17)/2 + 3/2 => 1
      3/2 + sqrt(17)/2 => 1

This can be pretty printed if the keys become symbolic:

```julia
convert(Dict{Sym, Any}, out)
```

\begin{equation*}\begin{cases}- \frac{\sqrt{17}}{2} + \frac{3}{2} & \text{=>} &1\\\frac{3}{2} + \frac{\sqrt{17}}{2} & \text{=>} &1\\\end{cases}\end{equation*}

Rewrite the Bessel function $J_{\nu}\left(z\right)$ in terms of the spherical Bessel function $j_\nu(z)$.

```julia
  >>> besselj(nu, z).rewrite(jn)
  √2⋅√z⋅jn(ν - 1/2, z)
  ────────────────────
           √π
```

In Julia:

we need to call in SpecialFunctions

jn is imported as a function object and this is not what SymPy expects, instead we pass in the object sympy.jn

```julia
using SpecialFunctions
besselj(nu, z).rewrite(sympy.jn)
```

\begin{equation*}\frac{\sqrt{2} \sqrt{z} j_{\nu - \frac{1}{2}}\left(z\right)}{\sqrt{\pi}}\end{equation*}

Print $\int_{0}^{\pi} \cos^{2}{\left (x \right )}\, dx$ using $\LaTeX$.

```julia
  >>> latex(Integral(cos(x)**2, (x, 0, pi)))
  \int_{0}^{\pi} \cos^{2}{\left (x \right )}\, dx
```

In Julia:

Latex printing occurs when the mime type is requested. However, the latex function can be called directly. However, this is not imported by default to avoid name collisions, and so must be qualified. Below, the latex is output as a string, though

Integral, like Function and Matrix is not a function and must be qualified

** must become ^

and we use  PI, an alias for sympy.pi, the symbolic value for $\pi$:

```julia
sympy.latex(sympy.Integral(cos(x)^2, (x, 0, PI)))
```

\int_{0}^{\pi} \cos^{2}{\left (x \right )}\, dx

Why SymPy?

There are many computer algebra systems out there.  This <http://en.wikipedia.org/wiki/List_of_computer_algebra_systems>_ Wikipedia article lists many of them.  What makes SymPy a better choice than the alternatives?

First off, SymPy is completely free. It is open source, and licensed under the liberal BSD license, so you can modify the source code and even sell it if you want to.  This contrasts with popular commercial systems like Maple or Mathematica that cost hundreds of dollars in licenses.

Second, SymPy uses Python.  Most computer algebra systems invent their own language. Not SymPy. SymPy is written entirely in Python, and is executed entirely in Python. This means that if you already know Python, it is much easier to get started with SymPy, because you already know the syntax (and if you don't know Python, it is really easy to learn).  We already know that Python is a well-designed, battle-tested language.  The SymPy developers are confident in their abilities in writing mathematical software, but programming language design is a completely different thing.  By reusing an existing language, we are able to focus on those things that matter: the mathematics.

Another computer algebra system, Sage also uses Python as its language.  But Sage is large, with a download of over a gigabyte.  An advantage of SymPy is that it is lightweight.  In addition to being relatively small, it has no dependencies other than Python, so it can be used almost anywhere easily. Furthermore, the goals of Sage and the goals of SymPy are different.  Sage aims to be a full featured system for mathematics, and aims to do so by compiling all the major open source mathematical systems together into one. When you call some function in Sage, such as integrate, it calls out to one of the open source packages that it includes.  In fact, SymPy is included in Sage.  SymPy on the other hand aims to be an independent system, with all the features implemented in SymPy itself.

A final important feature of SymPy is that it can be used as a library. Many computer algebra systems focus on being usable in interactive environments, but if you wish to automate or extend them, it is difficult to do.  With SymPy, you can just as easily use it in an interactive Python environment or import it in your own Python application.  SymPy also provides APIs to make it easy to extend it with your own custom functions.

In Julia:

There are other symbolic packages for Julia:

Reduce.jl

Symata.jl

SymEngine.jl

Nemo.jl

SymPy is an attractive alternative as PyCall makes most all of its functinality directly available and SymPy is fairly feature rich.

return to index


```python
import sympy as sy
import numpy as np
from sympy.functions import sin, cos, ln
import matplotlib.pyplot as plt
plt.style.use("ggplot")

# Factorial function
def factorial(n):
    if n <= 0:
        return 1
    else:
        return n * factorial(n - 1)

# Taylor approximation at x0 of the function 'function'
def taylor(function, x0, n, x = sy.Symbol('x')):
    i = 0
    p = 0
    while i <= n:
        p = p + (function.diff(x, i).subs(x, x0))/(factorial(i))*(x - x0)**i
        i += 1
    return p

def plot(f, x0 = 0, n = 9, by = 2, x_lims = [-5, 5], y_lims = [-5, 5], npoints = 800, x = sy.Symbol('x')):
    x1 = np.linspace(x_lims[0], x_lims[1], npoints)
    # Approximate up until n starting from 1 and using steps of by
    for j in range(1, n + 1, by):
        func = taylor(f, x0, j)
        taylor_lambda = sy.lambdify(x, func, "numpy")
        print('Taylor expansion at n=' + str(j), func)
        plt.plot(x1, taylor_lambda(x1), label = 'Order '+ str(j))
    # Plot the function to approximate (sine, in this case)
    func_lambda = sy.lambdify(x, f, "numpy")
    plt.plot(x1, func_lambda(x1), label = 'Function of x')

    plt.xlim(x_lims)
    plt.ylim(y_lims)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.legend()
    plt.grid(True)
    plt.title('Taylor series approximation')
    plt.show()

# Define the variable and the function to approximate
x = sy.Symbol('x')
f = ln(1 + x)
plot(f)
```


```python
import sympy as sp
import pandas as pd
```

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

```python
nodes = [-0.3, -0.2, -0.1, 0, 0.1, 0.3]
values = [f.evalf(subs={x: n}) for n in nodes]
```

```python
new_value = 0.8
true_node = sp.log(0.8)
```

```python
def construct_lagrange_polynom(nodes, values, x):
    polynom = 0
    for i in range(len(nodes)):
        term = 1
        for j in range(len(nodes)):
            if i == j:
                continue
            term *= x - nodes[j]
            term /= nodes[i] - nodes[j]
        polynom += term * values[i]
    return polynom
```

```python
polynom = construct_lagrange_polynom(nodes, values, x)
```

```python
solutions = sp.solve(polynom - new_value)
```

```python
pd.DataFrame([[true_node], [solutions[0]], [true_node - solutions[0]]],
             index=['Exact value', 'Estimated', 'Error'], columns=[''])
```

      Exact value
      -0.223143551314210

      Estimated
      -0.223143564849920

      Error
      1.35357104968925e-8


# Exercise 2
Write a function to compute the roots of a mathematical equation of the form
\begin{align}
  ax^{2} + bx + c = 0.
\end{align}
Your function should be sensitive enough to adapt to situations in which a user might accidentally set $a=0$, or $b=0$, or even $a=b=0$.  For example, if $a=0, b\neq 0$, your function should print a warning and compute the roots of the resulting linear function.  It is up to you on how to handle the function header: feel free to use default keyword arguments, variable positional arguments, variable keyword arguments, or something else as you see fit.  Try to make it user friendly.

Your function should return a tuple containing the roots of the provided equation.

**Hint:** Quadratic equations can have complex roots of the form $r = a + ib$ where $i=\sqrt{-1}$ (Python uses the notation $j=\sqrt{-1}$).  To deal with complex roots, you should import the `cmath` library and use `cmath.sqrt` when computing square roots.  `cmath` will return a complex number for you.  You could handle complex roots yourself if you want, but you might as well use available libraries to save some work.


Vamos continuar nossa trilha em computação simbólica estendendo o conhecimento sobre o tipo `bool`, expressões e testes lógicos.

## Operadores lógicos

Vimos que `True` e `False` são os dois valores atribuíves a um objeto de tipo `bool`. Eles são úteis para testar condições, realizar verificações e comparar quantidades. Vamos estudar *operadores de comparação*, *operadores de pertencimento* e *operadores de identidade*.

### Operadores de comparação

A tabela abaixo resume os operadores de comparação utilizados em Python.

| operador | significado | símbolo matemático |
|---|---|---|
| `<` | menor do que | $<$ |
| `<=` | menor ou igual a | $\leq$ |
| `>` | maior do que | $>$ |
| `>=` | maior ou igual a | $\geq$ |
| `==` | igual a | $=$ |
| `!=` | diferente de | $\neq$ |

Podemos usá-los para comparar objetos.

**Nota:** `==` está relacionado à igualdade, ao passo que `=` é uma atribuição. São conceitos operadores com finalidade distinta.

```python
2 < 3 # o resultado é um 'bool'
```

    True

```python
5 < 2 # isto é falso
```

    False

```python
2 <= 2 # isto é verdadeiro
```

    True

```python
4 >= 3 # isto é verdadeiro
```

    True

```python
6 != -2
```

    True

```python
4 == 4 # isto não é uma atribuição!
```

    True

Podemos realizar comparações aninhadas:

```python
x = 2
1 < x < 3
```

    True

```python
3 > x > 4
```

    False

```python
2 == x > 3
```

    False

As comparações aninhadas acima são resolvidas da esquerda para a direita e em partes. Isso nos leva a introduzir os seguintes operadores.

| operador | símbolo matemático | significado | uso relacionado a |
|---|---|---|---|
| `or` | $\vee$ | "ou" booleano | união, disjunção |
| `and` | $\wedge$ | "e" booleano | interseção, conjunção |
| `not` | $\neg$ | "não" booleano | exclusão, negação |

```python
# parênteses não são necessários aqui
(2 == x) and (x > 3) # 1a. comparação: 'True'; 2a.: 'False'. Portanto, ambas: 'False'
```

    False

```python
# parênteses não são necessários aqui
(x < 1) or (x < 2) # nenhuma das duas é True. Portanto,
```

    False

```python
not (x == 2) # nega o "valor-verdade" que é 'True'
```

    False

```python
not x + 1 > 3 # estude a precedência deste exemplo. Por que é 'True'?
```

    True

```python
not (x + 1 > 3) # estude a precedência deste exemplo. Por que também é 'True'?
```

    True

### Operadores de pertencimento

A tabela abaixo resume os operadores de pertencimento.

| operador | significado | símbolo matemático
|---|---|---|
| `in` | pertence a | $\in$ |
| `not in` | não pertence a | $\notin$ |

Eles terão mais utilidade quando falarmos sobre sequências, listas. Neste momento, vejamos exemplos com objetos `str`.

```python
'2' in '2 4 6 8 10' # o caracter '2' pertence à string
```

    True

```python
frase_teste = 'maior do que'
'maior' in frase_teste
```

    True

```python
'menor' in frase_teste # a palavra 'menor' está na frase
```

    False

```python
1 in 2 # 'in' e 'not in' não são aplicáveis aqui
```

### Operadores de identidade

A tabela abaixo resume os operadores de identidade.

| operador | significado
|---|---|
| `is` | "aponta para o mesmo objeto"
| `is not` | "não aponta para o mesmo objeto" |

Esses operadores são úteis para verificar se duas variáveis se referem ao mesmo objeto. Exemplo:

```python
a is b
a is not b
```

- `is` é `True` se `a` e `b` se referem ao mesmo objeto; `False`, caso contrário.
- `is not` é `False` se `a` e `b` se referem ao mesmo objeto; `True`, caso contrário.

```python
a = 2
b = 3
a is b # valores distintos
```

    False

```python
a = 2
b = a
a is b # mesmos valores
```

    True

```python
a = 2
b = 3
a is not b # de fato, valores não são distintos
```

    True

```python
a = 2
b = a
a is not b # de fato, valores são distintos
```

    False

## Equações simbólicas

Equações simbólicas podem ser formadas por meio de `Eq` e não com `=` ou `==`.

```python
# importação
from sympy.abc import a,b
import sympy as sy
sy.init_printing(pretty_print=True)
```

```python
sy.Eq(a,b) # equação simbólica
```

```python
sy.Eq(sy.cos(a), b**3) # os objetos da equação são simbólicos
```

### Resolução de equações algébricas simbólicas

Podemos resolver equações algébricas da seguinte forma:

```python
solveset(equação,variável,domínio)
```

**Exemplo:** resolva $x^2 = 1$ no conjunto $\mathbb{R}$.

```python
from sympy.abc import x
sy.solveset( sy.Eq( x**2, 1), x,domain=sy.Reals)
```

Podemos reescrever a equação como: $x^2 - 1 = 0$.

```python
sy.solveset( sy.Eq( x**2 - 1, 0), x,domain=sy.Reals)
```

Com `solveset`, não precisamos de `Eq`. Logo, a equação é passada diretamente.

```python
sy.solveset( x**2 - 1, x,domain=sy.Reals)
```

**Exemplo:** resolva $x^2 + 1 = 0$ no conjunto $\mathbb{R}$.

```python
sy.solveset( x**2 + 1, x,domain=sy.Reals) # não possui solução real
```

**Exemplo:** resolva $x^2 + 1 = 0$ no conjunto $\mathbb{C}$.

```python
sy.solveset( x**2 + 1, x,domain=sy.Complexes) # possui soluções complexas
```

**Exemplo:** resolva $\textrm{sen}(2x) = 3 + x$ no conjunto $\mathbb{R}$.

```python
sy.solveset( sy.sin(2*x) - x - 3,x,sy.Reals) # a palavra 'domain' também pode ser omitida.
```

O conjunto acima indica que nenhuma solução foi encontrada.

**Exemplo:** resolva $\textrm{sen}(2x) = 1$ no conjunto $\mathbb{R}$.

```python
sy.solveset( sy.sin(2*x) - 1,x,sy.Reals)
```

## Expansão, simplificação e fatoração de polinômios

Vejamos exemplos de polinômios em uma variável.

```python
a0, a1, a2, a3 = sy.symbols('a0 a1 a2 a3') # coeficientes
P3x = a0 + a1*x + a2*x**2 + a3*x**3 # polinômio de 3o. grau em x
P3x
```

```python
b0, b1, b2, b3 = sy.symbols('b0 b1 b2 b3') # coeficientes
Q3x = b0 + b1*x + b2*x**2 + b3*x**3 # polinômio de 3o. grau em x
Q3x
```

```python
R3x = P3x*Q3x # produto polinomial
R3x
```

```python
R3x_e = sy.expand(R3x) # expande o produto
R3x_e
```

```python
sy.simplify(R3x_e) # simplify às vezes não funciona como esperado
```

```python
sy.factor(R3x_e) # 'factor' pode funcionar melhor
```

```python
# simplify funciona para casos mais gerais
ident_trig = sy.sin(x)**2 + sy.cos(x)**2
ident_trig
```

```python
sy.simplify(ident_trig)
```

## Identidades trigonométricas

Podemos usar `expand_trig` para expandir funções trigonométricas.

```python
sy.expand_trig( sy.sin(a + b) ) # sin(a+b)
```

```python
sy.expand_trig( sy.cos(a + b) ) # cos(a+b)
```

```python
sy.expand_trig( sy.sec(a - b) ) # sec(a-b)
```

## Propriedades de logaritmo

Com `expand_log`, podemos aplicar propriedades válidas de logaritmo.

```python
sy.expand_log( sy.log(a*b) )
```

A identidade não foi validada pois `a` e `b` são símbolos irrestritos.

```python
a,b = sy.symbols('a b',positive=True) # impomos que a,b > 0
```

```python
sy.expand_log( sy.log(a*b) ) # identidade validada
```

```python
sy.expand_log( sy.log(a/b) )
```

```python
m = sy.symbols('m', real = True) # impomos que m seja um no. real
sy.expand_log( sy.log(a**m) )
```

Com `logcombine`, compactamos as propriedades.

```python
sy.logcombine( sy.log(a) + sy.log(b) ) # identidade recombinada
```

## Fatorial

A função `factorial(n)` pode ser usada para calcular o fatorial de um número.

```python
sy.factorial(m)
```

```python
sy.factorial(m).subs(m,10) # 10!
```

```python
sy.factorial(10) # diretamente
```

**Exemplo:** Sejam $m,n,x$ inteiros positivos. Se $f(m) = 2m!$, $g(n) = \frac{(n + 1)!}{n^2!}$ e $h(x) = f(x)g(x)$, qual é o valor de $h(2)$?

```python
from sympy.abc import m,n,x

f = 2*sy.factorial(m)
g = sy.factorial(n + 1)/sy.factorial(n**2)

h = (f.subs(m,x)*g.subs(n,x)).subs(x,4)
h
```

## Funções anônimas

A terceira classe de funções que iremos aprender é a de *funções anônimas*. Uma **função anônima** em Python consiste em uma função cujo nome não é explicitamente definido e que pode ser criada em apenas uma linha de código para executar uma tarefa específica.

Funções anônimas são baseadas na palavra-chave `lambda`. Este nome tem inspiração em uma área da ciência da computação chamada de cálculo-$\lambda$.

Uma função anônima tem a seguinte forma:

```python
lambda lista_de_parâmetros: expressão
```

Funções anônimas podem são bastante úteis para tornar um código mais conciso.

Por exemplo, na aula anterior, definimos a função

```python
def repasse(V):
    return 0.0103*V
```

para calcular o repasse financeiro ao corretor imobiliário.

Com uma função anônima, a mesma função seria escrita como:

```python
repasse = lambda V: 0.0103*V
```

Não necessariamente temos que atribui-la a uma variável. Neste caso, teríamos:

```python
lambda V: 0.0103*V
```

    <function __main__.<lambda>(V)>

Para usar a função, passamos um valor:

```python
repasse(100000) # repasse sobre R$ 100.000,00
```

O modelo completo com "bonificação" seria escrito como:

```python
r3 = lambda c,V,b: c*V + b # aqui há 3 parâmetros necessários
```

Redefinamos objetos simbólicos:

```python
from sympy.abc import b,c,V
r3(b,c,V)
```

O resultado anterior continua sendo um objeto simbólico, mas obtido de uma maneira mais direta. Podemos usar funções anônimas para tarefas de menor complexidade.

## "Lambdificação" simbólica

Usando `lambdify`, podemos converter uma expressão simbólica do *sympy* para uma expressão que pode ser numericamente avaliada em outra biblioteca. Essa função desempenha papel similar a uma função *lambda* (anônima).

```python
expressao = sy.sin(x) + sy.sqrt(x) # expressão simbólica
f = sy.lambdify(x,expressao,"math") # lambdificação para o módulo math
f(0.2) # avalia
```

Para avaliações simples como a anterior, podemos usar `evalf` e `subs`. A lambdificação será útil quando quisermos avaliar uma função em vários pontos, por exemplo. Na próxima aula, introduziremos sequencias e listas. Para mostrar um exemplo de lambdificação melhor veja o seguinte exemplo.

```python
from numpy import arange # importação de função do módulo numpy

X = arange(40) # gera 40 valores de 0 a 39
```

```python
X
```

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

```python
f = sy.lambdify(x,expressao,"numpy")(X) # avalia 'expressao' em X
f
```

    array([0.        , 1.84147098, 2.32351099, 1.87317082, 1.2431975 ,
           1.2771437 , 2.17007424, 3.30273791, 3.81778537, 3.41211849,
           2.61825655, 2.31663458, 2.9275287 , 4.02571831, 4.73226474,
           4.52327119, 3.71209668, 3.16170813, 3.49165344, 4.50877615,
           5.38508121, 5.41923133, 4.68156445, 3.94961112, 3.99340112,
           4.86764825, 5.86157796, 6.15252835, 5.56240841, 4.72153092,
           4.48919395, 5.16372672, 6.20828093, 6.74447451, 6.36003458,
           5.48789711, 5.00822115, 5.4392244 , 6.46078258, 7.20879338])


# 17 Importance Sampling

The Monte Carlo integration procedure with uniform sampling

$$
I = \int_{a_1}^{b_1} \cdots \int_{a_M}^{b_M} f(x_1, \dots, x_M) dx_1\cdots dx_M \approx V \langle f \rangle_\text{mc}
$$

works well with monotonic and smooth integrands $f$. However,

* sharply peaked
* oscillating

integrands are problematic (like for any integration method).

Oscillations increase fluctuations in random sampling and require more sampling.

Sharp peaks are especially annoying for *uniform sampling* because most of the samples will come from regions outside the peak and not contribute to the integral.

## Importance sampling method

**Importance sampling** is a method to turn a non-smooth $f(x)$ into a smoother $g(x)$ by separating the integrand

$$
\int_a^b \! f(x) dx = \int_a^b \! \frac{f(x)}{P(x)} P(x) dx = \int_a^b \! g(x) P(x) dx, \quad g(x) := \frac{f(x)}{P(x)}
$$

The new function $g(x)$ is supposed to be smoother than $f(x)$.

$P(x)$ is a known probability distribution and we are again calculating a *weighted average*, this time over our new function $g(x)$. The trick will be to generate samples according to $P(x)$ and then calculate the average $\langle g \rangle_\text{mc}$:

$$
\int_a^b \! f(x) dx = \langle g \rangle_\text{mc} = \frac{1}{N} \sum_{i=1}^N g(x_i) = \frac{1}{N} \sum_{i=1}^N \frac{f(x_i)}{P(x_i)}
$$

* The average $\langle g \rangle_\text{mc}$ *must* be calculated for samples $x \sim P$.
* The probability distribution $P(x)$ should be chosen so that the modified integrand $g(x) = f(x)/P(x)$ should become as smooth as possible (ideally, close to uniform).
* The integration volume does not explicitly appear in the importance sampling equation. It is taken into account implicitly by the sampling process.
* Importance sampling with the uniform distribution $P(x) = (b-a)^{-1}$ reduces to the *weighted average method*.
* Importance sampling generalizes to $M$ dimensions just as standard MC sampling.

## Example

The integral
$$
\int_a^b \! f(x) dx = \int_0^\infty \! \cos x \, e^{-x} dx = \frac{1}{2}
$$
oscillates and is strongly peaked at the origin.

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

def f(x):
    return np.cos(x) * np.exp(-x)

X = np.linspace(0, 100, 400)
plt.plot(X, f(X))
plt.xlabel("x"); plt.ylabel("f(x)");
```

Oscillations are not so problematic but if we want to integrate "to infinity" then we really should take care of the peak at the origin.

Choose the exponential distribution
$$
P(x) = e^{-x}
$$
which is already normalized on the interval $[0, +\infty[$.

We then get
$$
g(x) = \frac{f(x)}{P(x)} = \cos x.
$$

```python
def g(x):
    return np.cos(x)
```

```python
plt.plot(X, g(X), X, np.exp(-X))
plt.legend((r"$g(x)$", r"$P(x)$"))
plt.xlabel("x");
```

Note that the exponential distribution stretches to infinity. However, it is very unlikely to draw samples for large $x$ values.

But how do we sample from the exponential distribution?

1. Look through the docs for the [distributions](https://numpy.org/doc/stable/reference/random/generator.html#distributions) that [numpy's Random Generator](https://numpy.org/doc/stable/reference/random/generator.html) can provide, namely the [exponential distribution](https://numpy.org/doc/stable/reference/random/generated/numpy.random.Generator.exponential.html#numpy.random.Generator.exponential).
2. Transform samples from the *uniform* distribution to the exponential distribution.

### Exponential distribution with `numpy.random.Generator.exponential`

If we have well-tested, documented, and high performance code then we should know how to use it:

```python
import numpy as np
rng = np.random.default_rng()
```

```python
N = 1000
x = rng.exponential(scale=1.0, size=N)
fMC = np.mean(g(x))

fanalytical = 1/2
error = 1 - fMC/fanalytical

print(f"fMC = {fMC} ({N} samples)")
print(f"f = {fanalytical} (error = {error})")
```

    fMC = 0.48969567139965026 (1000 samples)
    f = 0.5 (error = 0.020608657200699487)

The error decreases with increasing sample size (as can be seen by modifying `N`).

Note that the samples are overwhelmingly appearing in the region where the function $f(x)$ was peaked, i.e., near the origin, and so $g(x)$ is only evaluated near the origin. Hence only those *important* data points contribute to the average. To obtain samples farther out ($x>10$) requires many samples to be drawn. The integration boundaries are implicitly taken into account via the sampling from the exponential distribution: in principle, a sample for very large $x$ *could* be drawn even though it is overwhelmingly unlikely.

```python
plt.plot(X, g(X), X, np.exp(-X))
plt.plot(x, g(x), 'k.')
plt.legend((r"$g(x)$", r"$P(x)$", "MC"))
plt.xlim(0, 20)
plt.xlabel("x");
```

### Exponential distribution with inverse transform
(follows *Computational Methods* Ch 10.B)

We often have to draw samples from a nonuniform distribution. The general approach is to draw samples from the [uniform distribution](https://en.wikipedia.org/wiki/Continuous_uniform_distribution) $\mathcal{U}_\text{[a, b]}$ over the interval $[a, b]$:

\begin{align}
x &\sim \mathcal{U}_\text{[a, b]}\\
p_x(x) &= \begin{cases}
   \frac{1}{b -a }, \quad a \le x \le b\\
   0, \text{otherwise}
   \end{cases}
\end{align}

and then transform the random samples $x$ to the desired samples $y \sim p_y$ that are non-uniformly distributed according to $p_y(y)$.

In the best case, we can obtain the nonuniform distribution *analytically* by using the *inverse transform method*. Let's assume that the transformation
$$
y = G(x)
$$
exists, which transforms a sample $x \sim p_x$ into $y \sim p_y$.

The derivation starts from the *conservation of probability*
$$
|p_y(y) dy| = |p_x(x) dx|
$$

The probability in $[y, y+dy]$ must equal the probability in the corresponding interval $[x, x+dx]$ because $G: x \mapsto y=G(x)$ in a one-to-one fashion.

Let's sample from the uniform distribution $\mathcal{U}_\text{[0, 1]}$ so $p_x(x) = 1$.

Integrate the conservation of probability equation (and pulling the absolute magnitude out of the integral because $\sum_i |a_i| = \left|\sum_i a_i\right|$ if $a_i \ge 0\ \forall i$):

\begin{gather}
\left|\int p_y(y) dy\right| = \int 1 dx = x\\
F(y) := \left|\int p_y(y) dy\right| = x\\
F(y) = x
\end{gather}

If we can solve the integral $\int p_y(y) dy$ analytically and if the inverse of $F(x)$ exists,
$$
y = F^{-1}(x) = G(x)
$$
then we have the **inverse transform**.

Apply to the *exponential distribution*:
\begin{gather}
p_y(y) = e^{-y}\\
F(y) = \left|\int p_y(y) dy\right| = \left|-e^{-y}\right| = e^{-y}\\
F(y) = x\\
e^{-y} = x\\
\end{gather}

to yield the inverse transform

$$
y = -\ln(x), \quad 0 \le x \le 1, \ 0 \le y \le + \infty
$$

The logarithm is the inverse transform of the exponential.

Thus the logarithm of a uniform sample in $[0, 1]$ yields a sample that is exponentially distributed. It "squishes" the samples near $x=1$ towards $y=0$ and expands the samples near $y=0$ towards infinity.

Our MC importance sampling now just has one extra step: transform the uniform samples:

```python
N = 1000
x = rng.uniform(low=0, high=1, size=N)
y = -np.log(x)
fMC = np.mean(g(y))

fanalytical = 1/2
error = 1 - fMC/fanalytical

print(f"fMC = {fMC} ({N} samples)")
print(f"f = {fanalytical} (error = {error})")
```

    fMC = 0.49704661874486056 (1000 samples)
    f = 0.5 (error = 0.0059067625102788845)

```python
plt.plot(X, g(X), X, np.exp(-X))
plt.plot(y, g(y), 'k.')
plt.plot(x, np.zeros_like(x), 'y|')
plt.legend((r"$g(x)$", r"$P(x)$", "MC", "uniform"))
plt.xlim(0, 20)
plt.xlabel("x");
```

```python

```


# Poincare-Lindstedt method

## Swing pendulum

The equation for the swing pendulum is nonlinear due to the rotation nature of the movement.
$$\ddot{x}+\sin x\approx\ddot{x}+x-\frac{1}{6}x^3=0$$
Rewrite the equation as
$$\ddot{x}+x=\epsilon x^3,\quad\epsilon=-1/6$$
The value $\epsilon$ can be considered as small parameter.
\begin{align}
x(t)&\approx x_0(\omega t)+\epsilon x_1(\omega t)+\dots=x_0(t')+\epsilon x_1(t')+\dots\\
\omega&\approx1+\epsilon \omega_1 + \epsilon^2\omega_2+\dots
\end{align}
Change time scale again, once $\omega$ in steady state solution is a function of amplitude. Scaling the time in this way makes frequency in solution independent of amplitude.
$$\hat{t}=\omega t,\quad\frac{d^2}{dt^2}=\omega^2\frac{d^2}{dt'^2}$$
The equation after changing time scale
$$(1+\epsilon \omega_1+\dots)^2(\ddot{x}_0+\epsilon \ddot{x}_1+\dots)+x_0+\epsilon x_1+\dots=\epsilon (x_0+\epsilon x_1+\dots)^3$$
Expand and collect terms with the same power of the small parameter $\epsilon$:
$$(\ddot{x}_0+x_0)+\epsilon(\ddot{x}_1+x_1)+\dots=0+\epsilon(x_0^3-2\omega_1\ddot{x_0})+\dots$$
which can be broken down into sequence of equation:
\begin{align}
\ddot{x}_0+x_0&=0\\
\ddot{x}_1+x_1&=-2\omega_1\ddot{x}_0+x_0^3
\end{align}
with initial conditions like this $x_0(0)=a$, $\dot{x}_0(0)=0$, $x_1(0)=0$, $\dot{x}_1(0)=0$

Solution to the 1st equation:
$$x_0=a\cos t$$
Substituting to the next equation yields
$$\ddot{x}_1+x_1=\color{brown}{a(2\omega_1+\frac{3}{4}a^2)\cos t}+\frac{1}{4}a^3\cos 3t=\frac{1}{4}a^3\cos 3t$$
where the term resulting in secular (aperiodic) solution is highlighted with brown color. Equating to zero the coefficient in this term results in condition for the first order correction to the frequency:
$$\omega_1=-\frac{3}{8}a^2,\quad x_1=\frac{1}{32}a^3(\cos 3t-\cos t)$$
Solution accounting for the next harmonic
$$x\approx a\cos\omega t-\frac{a^3}{192}(\cos 3\omega t-\cos\omega t),\quad \omega\approx 1-\frac{1}{16}a^2$$

## Secular terms

This is aperiodic terms in solution appering because of equation of idealized system does not account for dessipation processes usually limiting the amplitude in real world. For instance, the spicific solution to this equation
$$\ddot{x}+x=\sin(t),\quad\implies\quad x=-\frac{t}{2}\cos t$$
The solution is not a steady state.

## Compare numerical solution with analytical approximation

Equation
\begin{equation}
\ddot{x}+\sin x=0 \qquad
x(0) = x_0 \quad
\dot{x}(0) = 0
\end{equation}
introducing new variable
\begin{equation}
z_1 = x \quad
z_2 = \dot{x}
\end{equation}
get the system of 1st order equation for numerical procedure
\begin{equation}
\frac{d}{dt}
\begin{pmatrix}
z_1 \\ z_2
\end{pmatrix}=
\begin{pmatrix}
z_2 \\
-\sin z_1
\end{pmatrix}
\end{equation}

```python
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
```

```python
def duffing_eqs(z, t):
    return [ z[1], -np.sin(z[0]) ]
```

Numerical solution

```python
x1 = 0.1  # rad
x2 = np.pi/2
x3 = 0.9*np.pi

t = np.linspace(0, 5*2*np.pi, 100)

sol1 = odeint(duffing_eqs, [x1, 0], t)
sol2 = odeint(duffing_eqs, [x2, 0], t)
sol3 = odeint(duffing_eqs, [x3, 0], t)
```

```python
def plot_duffing(t, sol, fcn, *, title):
    plt.plot(t, sol, t, fcn)
    plt.xlabel('t')
    plt.ylabel('x')
    plt.legend(['numerical', 'analytic'])
    plt.title(title)
```

Approximation to analytical solution with frequency correction
$$x(t)\approx x_0\cdot\cos\left(\left(1-\frac{1}{16}x_0^2\right)t\right)$$

```python
def approx_sol_1(t, x0):
    w = 1 - x0**2 / 16
    return x0 * np.cos(w*t)
```

```python
def approx_sol_2(t, x0):
    w = 1 - x0**2 / 16
    return x0 * np.cos(w*t) - \
           x0**3 / 192 * (np.cos(3*w*t) - np.cos(w*t))
```

Solution for different amplitudes from $\left[0, \pi\right)$ range

```python
plt.figure(figsize=(15,10))
plt.subplot(2,2,1)
plot_duffing(t, sol1[:,0], x1*np.cos(t),
             title='small amplitude')
plt.subplot(2,2,2)
plot_duffing(t, sol2[:,0], x2*np.cos(t),
             title='$x_0=0.5\pi$, no freq. correction')
plt.subplot(2,2,3)
plot_duffing(t, sol2[:,0], approx_sol_1(t, x2),
             title='$x_0=0.5\pi$, with freq. correction')
plt.subplot(2,2,4)
plot_duffing(t, sol3[:,0],
             np.append(np.reshape(approx_sol_1(t, x3), (len(t),1)),
                       np.reshape(approx_sol_2(t, x3), (len(t),1)),
                       axis=1),
             title='$x_0=0.9\pi$, with correction')
plt.show()
```

```python
plt.plot(sol1[:,0], sol1[:,1],
         sol2[:,0], sol2[:,1],
         sol3[:,0], sol3[:,1])
plt.title('Phase plane')
plt.xlabel('$x$')
plt.ylabel('$\dot{x}$')
plt.legend(['$x_0=0.1\pi$','$x_0=0.5\pi$','$x_0=0.9\pi$'])
plt.show()
```

```python

```