```python
# Shabat, IUM, spring 2019, Algebra-2 course, excercises 5.2 and 5.3
# Generic code for displaying multiplication tables
import itertools as it
from IPython.core.display import Latex, Math
import sympy as sp
import pandas as pd
from tabulate import tabulate
```

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

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

```python
def zero_polynomial(p):
    F_p = sp.FF(p)
    return sp.Poly([], x, domain=F_p)

def iter_all_polynomials(p, deg):
    F_p = sp.FF(p)
    all_scalars = [F_p(n) for n in range(p)]
    powers = [x**n for n in range(deg + 1)]
    for coeff_tuple in it.product(all_scalars, repeat=deg + 1):
        if coeff_tuple[0] == 0:
            continue
        yield sp.Poly(coeff_tuple, x, domain=F_p)

def expr_product(exprs):
    res = exprs[0].as_expr()
    for expr in exprs[1:]:
        res *= expr.as_expr()
    return res

def iter_all_polynomials_with_factorings(p, max_deg):
    table = {}
    for f in iter_all_polynomials(p, 1):
        table[f] = (f,)
        yield f, (f,)
    for deg in range(2, max_deg + 1):
        for f, f_factors in list(table.items()):
            for g, g_factors in list(table.items()):
                h = f * g
                if h.degree() == deg:
                    table[h] = (*f_factors, *g_factors)
        for f in iter_all_polynomials(p, deg):
            f_factors = table.setdefault(f, (f, ))
            yield f, f_factors

def return_dict(generator):
    def func(*args, **kwargs):
        return dict(generator(*args, **kwargs))
    return func

@return_dict
def as_table(polynomials_with_factorings):
    for f, f_factors in polynomials_with_factorings:
        yield f.as_expr(), expr_product(f_factors)

def sympy_to_latex(v):
    return "$" + sp.latex(v) + "$"

display(pd.Series(as_table(iter_all_polynomials_with_factorings(2, 3))))
display(pd.Series(as_table(iter_all_polynomials_with_factorings(3, 2))))
```

    x                                           x
    x + 1                                   x + 1
    x**2                                     x**2
    x**2 + 1                           (x + 1)**2
    x**2 + x                            x*(x + 1)
    x**2 + x + 1                     x**2 + x + 1
    x**3                                     x**3
    x**3 + 1               (x + 1)*(x**2 + x + 1)
    x**3 + x                         x*(x + 1)**2
    x**3 + x + 1                     x**3 + x + 1
    x**3 + x**2                      x**2*(x + 1)
    x**3 + x**2 + 1               x**3 + x**2 + 1
    x**3 + x**2 + x              x*(x**2 + x + 1)
    x**3 + x**2 + x + 1                (x + 1)**3
    dtype: object

    x                               x
    x + 1                       x + 1
    x - 1                       x - 1
    -x                             -x
    1 - x                       1 - x
    -x - 1                     -x - 1
    x**2                         x**2
    x**2 + 1                 x**2 + 1
    x**2 - 1         (1 - x)*(-x - 1)
    x**2 + x              -x*(-x - 1)
    x**2 + x + 1           (1 - x)**2
    x**2 + x - 1         x**2 + x - 1
    x**2 - x               -x*(1 - x)
    x**2 - x + 1          (-x - 1)**2
    x**2 - x - 1         x**2 - x - 1
    -x**2                       -x**2
    1 - x**2         (-x - 1)*(x - 1)
    -x**2 - 1               -x**2 - 1
    -x**2 + x               x*(1 - x)
    -x**2 + x + 1       -x**2 + x + 1
    -x**2 + x - 1    (-x - 1)*(x + 1)
    -x**2 - x              x*(-x - 1)
    -x**2 - x + 1       -x**2 - x + 1
    -x**2 - x - 1     (1 - x)*(x - 1)
    dtype: object

```python
def elements_and_multiplication_table(p, f):
    f = sp.Poly(f, domain=sp.FF(p))
    deg = f.degree()
    elements = list(it.chain([zero_polynomial(p)], *[iter_all_polynomials(p, elem_deg) for elem_deg in range(deg)]))
    return [g.as_expr() for g in elements], [[(g * h % f).as_expr() for g in elements] for h in elements]

def as_matrix(e, m):
    return pd.DataFrame(m, columns=list(map(sympy_to_latex, e)), index=list(map(sympy_to_latex, e))).applymap(sympy_to_latex)

e, m = elements_and_multiplication_table(2, x**3 + x**2 + 1)
display(as_matrix(e, m))

e, m = elements_and_multiplication_table(3, x**2 - x - 1)
display(as_matrix(e, m))
```

      $0$
      $1$
      $x$
      $x + 1$
      $x^{2}$
      $x^{2} + 1$
      $x^{2} + x$
      $x^{2} + x + 1$

      $0$
      $0$
      $0$
      $0$
      $0$
      $0$
      $0$
      $0$
      $0$

      $1$
      $0$
      $1$
      $x$
      $x + 1$
      $x^{2}$
      $x^{2} + 1$
      $x^{2} + x$
      $x^{2} + x + 1$

      $x$
      $0$
      $x$
      $x^{2}$
      $x^{2} + x$
      $x^{2} + 1$
      $x^{2} + x + 1$
      $1$
      $x + 1$

      $x + 1$
      $0$
      $x + 1$
      $x^{2} + x$
      $x^{2} + 1$
      $1$
      $x$
      $x^{2} + x + 1$
      $x^{2}$

      $x^{2}$
      $0$
      $x^{2}$
      $x^{2} + 1$
      $1$
      $x^{2} + x + 1$
      $x + 1$
      $x$
      $x^{2} + x$

      $x^{2} + 1$
      $0$
      $x^{2} + 1$
      $x^{2} + x + 1$
      $x$
      $x + 1$
      $x^{2} + x$
      $x^{2}$
      $1$

      $x^{2} + x$
      $0$
      $x^{2} + x$
      $1$
      $x^{2} + x + 1$
      $x$
      $x^{2}$
      $x + 1$
      $x^{2} + 1$

      $x^{2} + x + 1$
      $0$
      $x^{2} + x + 1$
      $x + 1$
      $x^{2}$
      $x^{2} + x$
      $1$
      $x^{2} + 1$
      $x$

      $0$
      $1$
      $-1$
      $x$
      $x + 1$
      $x - 1$
      $- x$
      $1 - x$
      $- x - 1$

      $0$
      $0$
      $0$
      $0$
      $0$
      $0$
      $0$
      $0$
      $0$
      $0$

      $1$
      $0$
      $1$
      $-1$
      $x$
      $x + 1$
      $x - 1$
      $- x$
      $1 - x$
      $- x - 1$

      $-1$
      $0$
      $-1$
      $1$
      $- x$
      $- x - 1$
      $1 - x$
      $x$
      $x - 1$
      $x + 1$

      $x$
      $0$
      $x$
      $- x$
      $x + 1$
      $1 - x$
      $1$
      $- x - 1$
      $-1$
      $x - 1$

      $x + 1$
      $0$
      $x + 1$
      $- x - 1$
      $1 - x$
      $-1$
      $x$
      $x - 1$
      $- x$
      $1$

      $x - 1$
      $0$
      $x - 1$
      $1 - x$
      $1$
      $x$
      $- x - 1$
      $-1$
      $x + 1$
      $- x$

      $- x$
      $0$
      $- x$
      $x$
      $- x - 1$
      $x - 1$
      $-1$
      $x + 1$
      $1$
      $1 - x$

      $1 - x$
      $0$
      $1 - x$
      $x - 1$
      $-1$
      $- x$
      $x + 1$
      $1$
      $- x - 1$
      $x$

      $- x - 1$
      $0$
      $- x - 1$
      $x + 1$
      $x - 1$
      $1$
      $- x$
      $1 - x$
      $x$
      $-1$


```python
%matplotlib inline
```

```python
%run proof_setup
```

```python
import numpy as np
import sympy as sm

def rotate(sinw, cosw, sini, cosi, x2, y2, z2):
    Rwinv = sm.Matrix([[cosw, sinw, 0], [-sinw, cosw, 0], [0, 0, 1]])
    Riinv = sm.Matrix([[1, 0, 0], [0, cosi, sini], [0, -sini, cosi]])
    v2 = sm.Matrix([[x2], [y2], [z2]])
    v0 = Rwinv * Riinv * v2
    return sm.simplify(v0), sm.simplify(v2)

def get_quadratic_eqs(circular=False, edge=False, printer=None, wcase=False):
    if printer is None:
        printer = lambda x: x

    semimajor, ecc, w, incl, x, y, z, L = sm.symbols("a, e, omega, i, x, y, z, L")
    sinw = sm.sin(w)
    cosw = sm.cos(w)
    sini = sm.sin(incl)
    cosi = sm.cos(incl)

    if edge:
        cosi = 0
        sini = 1

    y = z * cosi / sini

    if wcase:
        sinw = 0
        cosw = 1

    if circular:
        ecc = 0

    v0, v2 = rotate(sinw, cosw, sini, cosi, x, y, z)

    print("x0 =", printer(v0[0]))
    print("y0 =", printer(v0[1]))
    print("z0 =", printer(v0[2]))
    print()

    eq = (v0[0] - semimajor * ecc) ** 2 + v0[1] ** 2 / (1 - ecc ** 2) - semimajor ** 2

    eq1 = sm.poly(eq, x, z)

    denom = ecc ** 2 - 1
    print("A =", printer(sm.simplify(denom * eq1.coeff_monomial(x ** 2))))
    print("B =", printer(sm.cancel(denom * eq1.coeff_monomial(x * z))))
    print("C =", printer(sm.simplify(denom * eq1.coeff_monomial(z ** 2))))
    print("D =", printer(sm.simplify(denom * eq1.coeff_monomial(x))))
    print("E =", printer(sm.simplify(denom * eq1.coeff_monomial(z))))
    print("F =", printer(sm.simplify(denom * eq1.coeff_monomial(1))))

    return (
        sm.simplify(denom * eq1.coeff_monomial(x ** 2)),
        sm.simplify(denom * eq1.coeff_monomial(x * z)),
        sm.simplify(denom * eq1.coeff_monomial(z ** 2)),
        sm.simplify(denom * eq1.coeff_monomial(x)),
        sm.simplify(denom * eq1.coeff_monomial(z)),
        sm.simplify(denom * eq1.coeff_monomial(1)),
    )
```

```python
get_quadratic_eqs(printer=sm.latex)
print()
print()
get_quadratic_eqs(circular=True, printer=sm.latex)
print()
print()
get_quadratic_eqs(wcase=True, printer=sm.latex)
print()
print()
get_quadratic_eqs(edge=True, printer=sm.latex);
```

    x0 = x \cos{\left (\omega \right )} + \frac{z \sin{\left (\omega \right )}}{\sin{\left (i \right )}}
    y0 = - x \sin{\left (\omega \right )} + \frac{z \cos{\left (\omega \right )}}{\sin{\left (i \right )}}
    z0 = 0

    A = e^{2} \cos^{2}{\left (\omega \right )} - 1
    B = \frac{2 e^{2} \sin{\left (\omega \right )} \cos{\left (\omega \right )}}{\sin{\left (i \right )}}
    C = \frac{e^{2} \sin^{2}{\left (\omega \right )} - 1}{\sin^{2}{\left (i \right )}}
    D = 2 a e \left(- e^{2} + 1\right) \cos{\left (\omega \right )}
    E = - \frac{2 a e \left(e^{2} - 1\right) \sin{\left (\omega \right )}}{\sin{\left (i \right )}}
    F = a^{2} \left(e^{2} - 1\right)^{2}

    x0 = x \cos{\left (\omega \right )} + \frac{z \sin{\left (\omega \right )}}{\sin{\left (i \right )}}
    y0 = - x \sin{\left (\omega \right )} + \frac{z \cos{\left (\omega \right )}}{\sin{\left (i \right )}}
    z0 = 0

    A = -1
    B = 0
    C = - \frac{1}{\sin^{2}{\left (i \right )}}
    D = 0
    E = 0
    F = a^{2}

    x0 = x
    y0 = \frac{z}{\sin{\left (i \right )}}
    z0 = 0

    A = e^{2} - 1
    B = 0
    C = - \frac{1}{\sin^{2}{\left (i \right )}}
    D = 2 a e \left(- e^{2} + 1\right)
    E = 0
    F = a^{2} \left(e^{2} - 1\right)^{2}

    x0 = x \cos{\left (\omega \right )} + z \sin{\left (\omega \right )}
    y0 = - x \sin{\left (\omega \right )} + z \cos{\left (\omega \right )}
    z0 = 0

    A = e^{2} \cos^{2}{\left (\omega \right )} - 1
    B = 2 e^{2} \sin{\left (\omega \right )} \cos{\left (\omega \right )}
    C = e^{2} \sin^{2}{\left (\omega \right )} - 1
    D = 2 a e \left(- e^{2} + 1\right) \cos{\left (\omega \right )}
    E = 2 a e \left(- e^{2} + 1\right) \sin{\left (\omega \right )}
    F = a^{2} \left(e^{2} - 1\right)^{2}

```python
def get_quartic_expr(circular=False, edge=False, printer=None, wcase=False):
    if printer is None:
        printer = lambda x: x

    A, B, C, D, E, F, T, L, x = sm.symbols("A, B, C, D, E, F, T, L, x", real=True)
    if edge:
        A, B, C, D, E, F = get_quadratic_eqs(edge=True)

    p0 = T
    p1 = 0
    p2 = x ** 2 - L ** 2

    q0 = C
    q1 = B * x + E
    q2 = A * x ** 2 + D * x + F

    quartic = sm.Poly(
        (p0 * q2 - p2 * q0) ** 2 - (p0 * q1 - p1 * q0) * (p1 * q2 - p2 * q1), x
    )

    if circular:
        args = {A: -1, B: 0, D: 0, E: 0}
    elif wcase:
        args = {B: 0, E: 0}
        quartic = sm.factor(quartic.subs(args))
        print(quartic)
        return
    else:
        args = {}

    for i in range(5):
        print(
            "a_{0} =".format(i),
            printer(sm.factor(sm.simplify(quartic.coeff_monomial(x ** i).subs(args)))),
        )
```

```python
get_quartic_expr(printer=sm.latex)
print()
print()
get_quartic_expr(circular=True, printer=sm.latex)
print()
print()
get_quartic_expr(wcase=True, printer=sm.latex)
```

    a_0 = C^{2} L^{4} + 2 C F L^{2} T - E^{2} L^{2} T + F^{2} T^{2}
    a_1 = - 2 T \left(B E L^{2} - C D L^{2} - D F T\right)
    a_2 = 2 A C L^{2} T + 2 A F T^{2} - B^{2} L^{2} T - 2 C^{2} L^{2} - 2 C F T + D^{2} T^{2} + E^{2} T
    a_3 = 2 T \left(A D T + B E - C D\right)
    a_4 = A^{2} T^{2} - 2 A C T + B^{2} T + C^{2}

    a_0 = \left(C L^{2} + F T\right)^{2}
    a_1 = 0
    a_2 = - 2 \left(C + T\right) \left(C L^{2} + F T\right)
    a_3 = 0
    a_4 = \left(C + T\right)^{2}

    (A*T*x**2 + C*L**2 - C*x**2 + D*T*x + F*T)**2

```python

```

```python

```

```python
def balance_companion_matrix(companion_matrix):
    diag = np.array(np.diag(companion_matrix))
    companion_matrix[np.diag_indices_from(companion_matrix)] = 0.0
    degree = len(diag)

    # gamma <= 1 controls how much a change in the scaling has to
    # lower the 1-norm of the companion matrix to be accepted.
    #
    # gamma = 1 seems to lead to cycles (numerical issues?), so
    # we set it slightly lower.
    gamma = 0.9

    scaling_has_changed = True
    while scaling_has_changed:
        scaling_has_changed = False

        for i in range(degree):
            row_norm = np.sum(np.abs(companion_matrix[i]))
            col_norm = np.sum(np.abs(companion_matrix[:, i]))

            # Decompose row_norm/col_norm into mantissa * 2^exponent,
            # where 0.5 <= mantissa < 1. Discard mantissa (return value
            # of frexp), as only the exponent is needed.
            _, exponent = np.frexp(row_norm / col_norm)
            exponent = exponent // 2

            if exponent != 0:
                scaled_col_norm = np.ldexp(col_norm, exponent)
                scaled_row_norm = np.ldexp(row_norm, -exponent)
                if scaled_col_norm + scaled_row_norm < gamma * (col_norm + row_norm):
                    # Accept the new scaling. (Multiplication by powers of 2 should not
                    # introduce rounding errors (ignoring non-normalized numbers and
                    # over- or underflow))
                    scaling_has_changed = True
                    companion_matrix[i] *= np.ldexp(1.0, -exponent)
                    companion_matrix[:, i] *= np.ldexp(1.0, exponent)

    companion_matrix[np.diag_indices_from(companion_matrix)] = diag
    return companion_matrix

def solve_companion_matrix(poly):
    poly = np.atleast_1d(poly)
    comp = np.eye(len(poly) - 1, k=-1)
    comp[:, -1] = -poly[:-1] / poly[-1]
    return np.linalg.eigvals(balance_companion_matrix(comp))

def _get_quadratic(a, e, cosw, sinw, cosi, sini):
    e2 = e * e
    e2mo = e2 - 1
    return (
        (e2 * cosw * cosw - 1),
        2 * e2 * sinw * cosw / sini,
        (e2mo - e2 * cosw * cosw) / (sini * sini),
        -2 * a * e * e2mo * cosw,
        -2 * a * e * e2mo * sinw / sini,
        a ** 2 * e2mo * e2mo,
    )

def _get_quartic(A, B, C, D, E, F, T, L):
    A2 = A * A
    B2 = B * B
    C2 = C * C
    D2 = D * D
    E2 = E * E
    F2 = F * F
    T2 = T * T
    L2 = L * L
    return (
        C2 * L2 * L2 + 2 * C * F * L2 * T - E2 * L2 * T + F2 * T2,
        -2 * T * (B * E * L2 - C * D * L2 - D * F * T),
        2 * A * C * L2 * T
        + 2 * A * F * T2
        - B2 * L2 * T
        - 2 * C2 * L2
        - 2 * C * F * T
        + D2 * T2
        + E2 * T,
        2 * T * (A * D * T + B * E - C * D),
        A2 * T2 - 2 * A * C * T + B2 * T + C2,
    )

def _get_roots_general(a, e, omega, i, L, tol=1e-8):
    cosw = np.cos(omega)
    sinw = np.sin(omega)
    cosi = np.cos(i)
    sini = np.sin(i)

    f0 = 2 * np.arctan2(cosw, 1 + sinw)

    quad = _get_quadratic(a, e, cosw, sinw, cosi, sini)
    A, B, C, D, E, F = quad
    T = cosi / sini
    T *= T
    quartic = _get_quartic(A, B, C, D, E, F, T, L)

    roots = solve_companion_matrix(quartic)
    roots = roots[np.argsort(np.real(roots))]

    # Deal with multiplicity
    roots[0] = roots[:2][np.argmin(np.abs(np.imag(roots[:2])))]
    roots[1] = roots[2:][::-1][np.argmin(np.abs(np.imag(roots[2:])[::-1]))]
    roots = roots[:2]

    # Only select real roots
    roots = np.clip(np.real(roots[np.abs(np.imag(roots)) < tol]), -L, L)
    if len(roots) < 2:
        return np.empty(0)

    angles = []
    for x in roots:
        b0 = A * x * x + D * x + F
        b1 = B * x + E
        b2 = C
        z1 = -0.5 * b1 / b2
        arg = b1 * b1 - 4 * b0 * b2
        if arg < 0:
            continue
        z2 = 0.5 * np.sqrt(arg) / b2
        for sgn in [-1, 1]:
            z = z1 + sgn * z2
            if z > 0:
                continue
            y = z * cosi / sini

            x0 = x * cosw + z * sinw / sini
            y0 = -x * sinw + z * cosw / sini
            angle = np.arctan2(y0, x0) - np.pi
            if angle < -np.pi:
                angle += 2 * np.pi
            angles.append(angle - f0)

    angles = np.sort(angles)

    # Wrap the roots properly to span the transit
    if len(angles) == 2:
        if np.all(angles > 0):
            angles = np.array([angles[1] - 2 * np.pi, angles[0]])
        if np.all(angles < 0):
            angles = np.array([angles[1], angles[0] + 2 * np.pi])
    else:
        angles = np.array([-np.pi, np.pi])

    return angles + f0

def check_roots(a, e, omega, i, L, tol=1e-8):
    L /= a
    a = 1.0
    roots = _get_roots_general(a, e, omega, i, L, tol=tol)

    for f in roots:
        b2 = (
            a ** 2
            * (e ** 2 - 1) ** 2
            * (
                np.cos(i) ** 2
                * (np.cos(omega) * np.sin(f) + np.sin(omega) * np.cos(f)) ** 2
                + (np.cos(omega) * np.cos(f) - np.sin(omega) * np.sin(f)) ** 2
            )
            / (e * np.cos(f) + 1) ** 2
        )
        print("b2 = ", b2, " L2 = ", L ** 2)

    print(roots)
```

```python
check_roots(10.0, 0.5, -0.15, 0.5 * np.pi - 0.01, 1.0)
```

    b2 =  0.010014687059509328  L2 =  0.010000000000000002
    b2 =  0.010019159137027812  L2 =  0.010000000000000002
    [1.58853526 1.83656275]

```python
check_roots(100.0, 0.0, np.pi, 0.5 * np.pi, 1.5)
```

    b2 =  0.00022499999999999343  L2 =  0.000225
    b2 =  0.000225000000000003  L2 =  0.000225
    [-1.58579689 -1.55579576]

```python
get_quadratic_eqs()
print()
get_quartic_expr();
```

    x0 = x*cos(omega) + z*sin(omega)/sin(i)
    y0 = -x*sin(omega) + z*cos(omega)/sin(i)
    z0 = 0

    A = e**2*cos(omega)**2 - 1
    B = 2*e**2*sin(omega)*cos(omega)/sin(i)
    C = (e**2*sin(omega)**2 - 1)/sin(i)**2
    D = 2*a*e*(-e**2 + 1)*cos(omega)
    E = -2*a*e*(e**2 - 1)*sin(omega)/sin(i)
    F = a**2*(e**2 - 1)**2

    a_0 = C**2*L**4 + 2*C*F*L**2*T - E**2*L**2*T + F**2*T**2
    a_1 = -2*T*(B*E*L**2 - C*D*L**2 - D*F*T)
    a_2 = 2*A*C*L**2*T + 2*A*F*T**2 - B**2*L**2*T - 2*C**2*L**2 - 2*C*F*T + D**2*T**2 + E**2*T
    a_3 = 2*T*(A*D*T + B*E - C*D)
    a_4 = A**2*T**2 - 2*A*C*T + B**2*T + C**2

```python
get_quadratic_eqs(edge=True)
print()
print()
get_quadratic_eqs(circular=True, printer=sm.latex, edge=True);
```

    x0 = x*cos(omega) + z*sin(omega)
    y0 = -x*sin(omega) + z*cos(omega)
    z0 = -y

    b_0 = (a**2*e**4 - 2*a**2*e**2 + a**2 - 2*a*e**3*x*cos(omega) + 2*a*e*x*cos(omega) + e**2*x**2*cos(omega)**2 - x**2)/(e**2 - 1)
    b_1 = (-2*a*e**3*sin(omega) + 2*a*e*sin(omega) + 2*e**2*x*sin(omega)*cos(omega))/(e**2 - 1)
    b_2 = (e**2*sin(omega)**2 - 1)/(e**2 - 1)

    x0 = x
    y0 = z
    z0 = - y

    b_0 = - a^{2} + x^{2}
    b_1 = 0
    b_2 = 1

```python

```


```python
from sympy import *
import math
from decimal import *

```

```python
x=Symbol('x')
answer=solve(x**2-x*2+1,x)
print(answer)
```

```python
def f(x):
    return 64*x**6 - 480*x**4 + 720*x**2 - 120
def df(x):
    return 384*x**5 - 1920*x**3 + 1440*x
def dx(f,x):
    return abs(0-f(x))
def newtows(f,df,x0,e):
    delta=dx(f,x0)
    while delta>e:
        x0=x0-f(x0)/df(x0)
        delta=dx(f,x0)
    print('ROOT:%.10f'%x0)
    print('f(x):%.10f'%f(x0))
    print('Delta:%.10f'%delta)

```

```python

newtows(f,df,0.4,1e-6)

```

    ROOT:0.4360774119
    f(x):0.0000000000
    Delta:0.0000000000

```python
x=symbols("x")
y=1/(x*x)
diff(y,x)
```

    -2/x**3

```python
import numpy.polynomial.legendre
legendre(6,x)
```

    231*x**6/16 - 315*x**4/16 + 105*x**2/16 - 5/16

```python
import numpy.polynomial.laguerre
laguerre(5,x)
```

    -x**5/120 + 5*x**4/24 - 5*x**3/3 + 5*x**2 - 5*x + 1

```python
import numpy.polynomial.hermite
hermite(6,x)
```

    64*x**6 - 480*x**4 + 720*x**2 - 120

```python

```


```python
%pylab inline
%config InlineBackend.figure_format = 'retina'
from ipywidgets import interact
```

    Populating the interactive namespace from numpy and matplotlib

# Question 1
The Lagrange interpolating polynomial is
$$ p(x) = \sum_{j=0}^{\infty}y_j L_j(x).$$
Show that the identity ,
$$ \sum_{j=0}^{n} L_j(x) = 1,$$
is true for all $x$.

**Hint: The answer requires no algebra. Use the fact that $f(x) = 1$ is a polynomial of degree zero and a Lagrange polynomial.**

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

By the existence uniqueness theorem (week 4 Wednesday slides), there is only one Lagrange interpolating polynomial that interpolates the set of points $(x_j, 1)$. Note that $L_0(x) = 1$ is a Lagrange polynomial of degree zero (you can check that it satisfies the definition), and it interpolates the points $(x_j, 1)$. Hence any Lagrange interpolating polynomial $p_n(x)$ for the set of points $(x_j, 1)$ must satisfy $p_n(x) = L_0(x) = 1$ for all $x$.

# Question 2

## A.
Write a function for computing the barycentric weights
$$w_j = \left[\prod_{\substack{i=0 \\\\ i\neq j}}^{n}(x_j - x_i)\right]^{-1}.$$
Your function should take as input a vector containing the nodes $x_j$ and output the weights $w_j$ . Call your function `baryfit`. Write another function for evaluating the barycentric interpolant
$p(x).$
This function should take as input a vector containing the nodes $x_j$, a vector containing the corresponding barycentric weights $w_j$ (generated from your `baryfit` function), a vector containing the corresponding function values $y_j = f(x_j)$, and the location (or a vector of locations) of where the interpolant should be evaluated. The output of the function should be the value of the interpolating polynomial at all the evaluation points. Call this function `baryeval`.

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

See code below.

## B.
Using your `baryfit` function from part a, generate the barycentric weights for the following two sets of nodes:
  1. $x_j = -1 + \frac{2j}{8}$, $j=0,1,\ldots, 8$
  2. $x_j = -\cos(\frac{j\pi}{8})$, $j=0,1,\ldots, 8$

Plot the values of the weights versus the corresponding values of the nodes (i.e plot ($x_j$ , $w_j$ )) for each of the node sets. Comment on the results.

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

See code and figure below. The first set of nodes (uniform nodes) tend have higher magnitude than the second, particulary near the center. The magnitude of the second set (Chebyshev) is constant except at the ends.

## C.
For the two node sets from part B, use your `baryval` function to evaluate the 8th degree polynomial interpolant of the function $f(x) = \vert x \vert$ at 101 equally spaced points between $[-1, 1]$. Plot the error ($p(x) − \vert x \vert$) in the polynomial interpolant at these evaluation points for each of the two node sets. Which node set seems to produce the best result? What criteria did you use to determine what ‘best’ means?

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

See code and figure below. The second set seems to be the best. The accuracy is better based on lower error over most of the interval $[-1, 1]$.

## D.
For certain sets of nodes $x_j$, it is possible to give explicit formulas for the barycentric weights $w_j$. The easiest case is when the nodes are equally spaced between $[−1,1]$, (i.e., $x_j =−1+\frac{2j}{n}$, $j=0,1,...,n$). Show that for these nodes
$$w_j = \frac{\left(\frac{n}{2}\right)^n(-1)^{n-j}}{n!}\binom{n}{j}$$
Note that since $w_j$ appear both in the numerator and denominator of the barycentric formula for $p(x)$, any factors common to all $w_j$ can be factored out. Thus, we can reduce the above expression for the barycentric weights to $w_j =(−1)^j\binom{n}{j}$.

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

\begin{align}
\prod_{\substack{i=0 \\\\ i\neq j}}^{n}(x_j - x_i) &=
\prod_{\substack{i=0 \\\\ i\neq j}}^{n}\left(\frac{2}{n}(j - i)\right) \\
& = \left(\frac{2}{n}\right)^n\prod_{i=0}^{j-1}(j - i)\prod_{i=j+1}^{n}(j - i) \\
& = \left(\frac{2}{n}\right)^nj!\prod_{i=j+1}^{n}(j - i) \\
& = \left(\frac{2}{n}\right)^nj! (-1)^{n-j}\prod_{i=j+1}^{n}(i - j) \\
& = \left(\frac{2}{n}\right)^nj! (-1)^{n-j} (n-j)! \\
& =   \frac{n!}{\left(\frac{n}{2}\right)^n(-1)^{n-j}\binom{n}{j}}
\end{align}
Hence,
$$ w_j = \frac{1}{\displaystyle \prod_{\substack{i=0 \\\\ i\neq j}}^{n}(x_j - x_i)}
= \frac{\left(\frac{n}{2}\right)^n(-1)^{n-j}\binom{n}{j}}{n!}.
$$

```python
## Q2 Part A

def baryfit(xnodes):
    n = xnodes.size - 1
    w = zeros(n+1) # empty array to store weights
    for j in arange(n+1): # j = 0, 1, ..., n
        denominator = 1.
        for i in arange(n+1):# i = 0, 1, ..., n
            if i == j:
                continue ## this command will skip to the next iteration of the loop
            denominator = denominator*(xnodes[j] - xnodes[i])
        w[j] = 1/denominator
    return w
def baryeval(x, xnodes, ynodes, weights):
    n = xnodes.size - 1
    m = x.size
    numerator = zeros(m)
    denominator = zeros(m)
    for k in arange(m): ## loop over each evaluation point in `x`
        for j in arange(n+1): # j = 0, 1, ..., n
            if x[k] == xnodes[j]: # check if x = x_j
                numerator[k] = ynodes[j] ## p(x_j) = y_j
                denominator[k] = 1.
                break ## this will end the inner-most loop (skip to the next value of k)
            numerator[k] = numerator[k] + ynodes[j]*weights[j]/(x[k] - xnodes[j])
            denominator[k] = denominator[k] + weights[j]/(x[k] - xnodes[j])
    p_of_x = numerator/denominator
    return p_of_x

## vectorize versions: This is not required for the problem. I am including them as
## an example of writting more efficient code that minimizes the use of loops
def baryfit_vectorized(xnodes):
    w = 1/(
        xnodes[None, :]
        - xnodes[:, None]
        + eye(xnodes.size) # identity matrix, eliminates the i = j case
    ).prod(axis=0)
    return w
def baryeval_vectorized(x, xnodes, ynodes, weights):
    ## this version does not check if x = x_j
    ####################
    # check that nodes and weights have the same number of elements
    assert xnodes.size == ynodes.size == weights.size
    # check that all arrays are 1D arrays
    assert xnodes,ndim == ynodes.ndim == weights.ndim == 1
    bary = weights[:, None]/(x[None, :] - xnodes[:, None])
    y = (bary*ynodes[:, None]).sum(axis=0)/bary.sum(axis=0)
    return y
```

```python
## Q2 Part B
j = arange(8+1)
x_set1 = -1 + 2*j/8.
x_set2 = -cos(j*pi/8.)

w_set1 = baryfit(x_set1)
w_set2 = baryfit(x_set2)

figure(1, [7, 4])
plot(x_set1, w_set1, '-o', label='uniform')
plot(x_set2, w_set2, '-o', label='Chebyshev')
xlabel(r'$x_j$', fontsize=24)
ylabel(r'$w_j$', fontsize=24);
legend(fontsize=18);
```

```python
## Q2 Part C
y_set1 = absolute(x_set1)
y_set2 = absolute(x_set2)
x = linspace(-1, 1, 101) # 101 equally spaced points betweein -1 and 1
p_set1 = baryeval(x, x_set1, y_set1, w_set1)
p_set2 = baryeval(x, x_set2, y_set2, w_set2)
yexact = absolute(x)

figure(1, [7, 4])
plot(x, yexact, 'k', label='exact')
plot(x_set1, y_set1, 'ko')
plot(x_set2, y_set2, 'k*')
plot(x, p_set1, label='uniform')
plot(x, p_set2, label='Chebyshev')
xlabel(r'$x$', fontsize=24)
ylabel(r'$y$', fontsize=24);
legend(fontsize=18);

figure(2, [7, 4])
plot(x, (p_set1 - yexact), label='uniform')
plot(x, (p_set2 - yexact), label='Chebyshev')
xlabel(r'$x$', fontsize=24)
ylabel(r'$p(x) - |x|$', fontsize=24);
legend(fontsize=18);
```

# Question 3
Suppose you are given the following experimental measurements:
\begin{align}
	x_i &\qquad f(x_i) \\\\
	0.08 &\qquad 0.6739 \\\\
	0.46 &\qquad 2.4306 \\\\
	1.00 &\qquad 0.0000 \\\\
	1.51 &\qquad -1.0621 \\\\
	2.05 &\qquad 0.0986
\end{align}

## A.
Write a python script (using the code from Q2) that approximates $f(0.75)$ using the following Lagrange polynomials:
\begin{align}
	&\text{$P_1$ using $x_0 = 0.46$, and $x_1 = 1.00$;} \\\\
	&\text{$P_2$ using $x_0 = 0.46$, $x_1 = 1.00$, and $x_2 = 1.51$;} \\\\
	&\text{$P_3$ using $x_0 = 0.08$, $x_1 = 0.46$, $x_2 = 1.00$, and $x_3 = 1.51$;} \\\\
	&\text{$P_4$} \text{ using all five points}.
\end{align}

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

See code below.

## B.
The information in the above table corresponds to the function
\begin{equation}
f(x) = \sin(\pi x)e^{\cos(x)}.
\end{equation}

What is the absolute error of each of your approximations $P_i(0.75)$? Which Lagrange polynomial was the most accurate? Is this the result you expected?

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

The Lagrange polynomial $P_3(0.75)$ was the most accurate, since the absolute error associated with this interpolant was the smallest. This *is not* the result I expected! I would have assumed a higher-order interpolant ($P_4$) would be more accurate, yielding a smaller absolute error, but this assumption was wrong.

Just for fun, you can modify the `print()` statement in the above code to see that the relative error associated with $P_3(0.75)$ is *also* the smallest -- within 3% of $f(0.75)$. By both these measures, $P_3(0.75)$ is the most accurate Lagrange interpolant of $f(0.75)$.

```python
# ====== Question 3 solution ======
# translated from original MatLab solution
x =[0.08,   0.46,   1.00, 1.51,    2.05] # x points
fx=[0.6739, 2.4306, 0,    -1.0621, 0.0986] # f(x) data

# ------ initialize space ------
P = zeros(4) #storage for P_0, P_1, ...
start = [1, 1, 0, 0] #list of indices in x[] indicating what is x0 for each P_i
                     #these serve as an offset factor since we'll build all 4
                     #polynomial approximations in a set of nested loops

# ------ get some important information ------
n=len(P)
X = 0.75 # value we're interested in
F = sin(pi*X)*exp(cos(X))

for i in arange(n): #outer loop builds P_i
    nterms = i+2 #"first" iteration (i=0) there is (i+1)+1 terms in the Lagrange polynomial
    x0 = start[i] #index corresponding to which starting x0 to use
    for j in arange(nterms): #each iteration adds one term to P_i
        L=1 #initialize an empty product
        k=0
        while k<nterms:
            if k==j:
                k+=1 #skip (X - x_k)/(x_j - x_k) = NaN
            else:
                L *= (X-x[x0+k])/(x[x0+j]-x[x0+k]) #multiply another term into L
                k+=1
        P[i] += fx[x0+j]*L #add the newly completed term to P_i
    print('P_',i+1,'(',X,') =',round(P[i],4),'and |f(',X,') - P_',i+1,'(',X,')| =',round(abs(F-P[i]),4))
```

    P_ 1 ( 0.75 ) = 1.1253 and |f( 0.75 ) - P_ 1 ( 0.75 )| = 0.3445
    P_ 2 ( 0.75 ) = 0.9583 and |f( 0.75 ) - P_ 2 ( 0.75 )| = 0.5115
    P_ 3 ( 0.75 ) = 1.4292 and |f( 0.75 ) - P_ 3 ( 0.75 )| = 0.0406
    P_ 4 ( 0.75 ) = 1.2894 and |f( 0.75 ) - P_ 4 ( 0.75 )| = 0.1804

```python

```


```python
from sympy.interactive import init_printing
init_printing()
from sympy import sqrt, sin, cos, pi, var, integrate, Symbol, S, Integral, symbols, solve
def rpoisson(f, r):
    return -(r**2*f.diff(r)).diff(r)/r**2/(4*pi)
def rpoisson_int(nr, r):
    # Boundary conditions:
    # r_c^2*v'(r_c) = -1       (1)
    # v(r_c) = 1/r_c           (2)
    first = ((-4*pi*nr)*r**2).integrate(r)
    first = first - first.subs(r, r_c) + (-1)     # Impose (1)
    second = (first/r**2).integrate(r)
    second = second - second.subs(r, r_c) + 1/r_c # Impose (2)
    return second
def check_gr_vr(gr, vr, r, r_c):
    # Some checks:
    # density is zero at r=r_c
    assert gr.subs(r, r_c).simplify() == 0
    # density is normalized to 1
    assert integrate(4*pi*r**2*gr, (r, 0, r_c)).simplify() == 1
    # vr is the potential corresponding to the density gr
    assert (gr-rpoisson(vr, r)).simplify() == 0
    # gr is the density corresponding to vr
    assert (vr-rpoisson_int(gr, r)).simplify() == 0
```

```python
var("r r_c")
gr = -21*(r-r_c)**3*(6*r**2+3*r*r_c+r_c**2)/(5*pi*r_c**8)
vr = (9*r**7 - 30*r**6*r_c + 28*r**5*r_c**2 - 14*r**2*r_c**5 + 12*r_c**7)/(5*r_c**8)
```

```python
check_gr_vr(gr, vr, r, r_c)
```

```python
Ig = integrate(4*pi*r**2*gr*(1/r-vr), (r, 0, r_c))
assert Ig == 10976/(17875*r_c)
Ig
```

    10976/(17875*r_c)

```python
Isph = integrate(4*pi*r**2*(1/r-vr), (r, 0, r_c))
assert Isph == 14*pi*r_c**2/75
Isph
```

    14*pi*r_c**2/75

# Example 1

```python
#gr = cos(pi*r/r_c/2) /( 8*r_c**3*(-8 + pi**2)/pi**2 )
gr = 1-(r/r_c)**2
gr = gr / integrate(4*pi*r**2*gr, (r, 0, r_c))
gr = gr.simplify()
gr
```

    15*(-r**2 + r_c**2)/(8*pi*r_c**5)

```python
vr = rpoisson_int(gr, r).simplify()
vr
```

    (3*r**4 - 10*r**2*r_c**2 + 15*r_c**4)/(8*r_c**5)

```python
check_gr_vr(gr, vr, r, r_c)
```

```python
Ig = integrate(4*pi*r**2*gr*(1/r-vr), (r, 0, r_c))
Ig
```

    25/(56*r_c)

```python
Isph = integrate(4*pi*r**2*(1/r-vr), (r, 0, r_c))
Isph
```

    2*pi*r_c**2/7

```python
print "Ig =", Ig
print "Isph =", Isph
print "v0 =", vr.limit(r, 0)
print
print "gr =", gr
```

    Ig = 25/(56*r_c)
    Isph = 2*pi*r_c**2/7
    v0 = 15/(8*r_c)

    gr = 15*(-r**2 + r_c**2)/(8*pi*r_c**5)

# Example 2

```python
n = 10
p = r**(n-1)
syms = symbols("a0:%d" % (n-1))
for i in range(n-1):
    p += syms[i]*r**i
f = (1-r)**n * p
eqs = [f.diff(r, i).subs(r, 0) for i in range(1, n)]
d = solve(eqs, syms)
f = f.subs(d)
gr = f.subs(r, r/r_c)
gr = gr / integrate(4*pi*r**2*gr, (r, 0, r_c))
gr = gr.simplify()
gr
```

    21*(r - r_c)**10*(48620*r**9 + 24310*r**8*r_c + 11440*r**7*r_c**2 + 5005*r**6*r_c**3 + 2002*r**5*r_c**4 + 715*r**4*r_c**5 + 220*r**3*r_c**6 + 55*r**2*r_c**7 + 10*r*r_c**8 + r_c**9)/(4*pi*r_c**22)

```python
vr = rpoisson_int(gr, r).simplify()
vr
```

    (-8840*r**21 + 92378*r**20*r_c - 432432*r**19*r_c**2 + 1191190*r**18*r_c**3 - 2130128*r**17*r_c**4 + 2567565*r**16*r_c**5 - 2089164*r**15*r_c**6 + 1108536*r**14*r_c**7 - 348840*r**13*r_c**8 + 49742*r**12*r_c**9 - 14*r**2*r_c**19 + 11*r_c**21)/(4*r_c**22)

```python
check_gr_vr(gr, vr, r, r_c)
```

```python
Ig = integrate(4*pi*r**2*gr*(1/r-vr), (r, 0, r_c))
Ig
```

    517399110137/(809268832200*r_c)

```python
Isph = integrate(4*pi*r**2*(1/r-vr), (r, 0, r_c))
Isph
```

    91*pi*r_c**2/690

```python
print "Ig =", Ig
print "Isph =", Isph
print "v0 =", vr.limit(r, 0)
print
print "gr =", gr
```

    Ig = 517399110137/(809268832200*r_c)
    Isph = 91*pi*r_c**2/690
    v0 = 11/(4*r_c)

    gr = 21*(r - r_c)**10*(48620*r**9 + 24310*r**8*r_c + 11440*r**7*r_c**2 + 5005*r**6*r_c**3 + 2002*r**5*r_c**4 + 715*r**4*r_c**5 + 220*r**3*r_c**6 + 55*r**2*r_c**7 + 10*r*r_c**8 + r_c**9)/(4*pi*r_c**22)

```python
print "Ig =", str(Ig).replace("r_c", "rc")
print "Isph =", str(Isph).replace("r_c", "rc")
print "v0 =", str(vr.limit(r, 0)).replace("r_c", "rc")
print
print "gr =", str(gr).replace("r_c", "rc")
```

    Ig = 517399110137/(809268832200*rc)
    Isph = 91*pi*rc**2/690
    v0 = 11/(4*rc)

    gr = 21*(r - rc)**10*(48620*r**9 + 24310*r**8*rc + 11440*r**7*rc**2 + 5005*r**6*rc**3 + 2002*r**5*rc**4 + 715*r**4*rc**5 + 220*r**3*rc**6 + 55*r**2*rc**7 + 10*r*rc**8 + rc**9)/(4*pi*rc**22)

# Example 3

```python
from scipy.integrate import cumtrapz
from scipy.special import erf
from numpy import linspace, trapz, pi, sin, empty, size, exp, sqrt

def integrate(R, f):
    return cumtrapz(f, R, initial=0)

def rpoisson_int(gr, R):
    rc = 1
    first = integrate(R, (-4*pi*gr)*R**2)
    first = first - first[-1] + (-1)
    second = integrate(R, first/R**2)
    second = second - second[-1] + 1/rc
    return second
```

```python
alpha = 37
C = 1/(pi**(3./2) * erf(sqrt(alpha)) / alpha**(3./2) - 2*pi*exp(-alpha)/alpha)

for i in range(3, 7):
    R = linspace(1e-5, 1, 10**i)
    gr = C * exp(-37*R**2)

    print "gr norm:", 1/trapz(4*pi*gr*R**2, R)

    vr = rpoisson_int(gr, R)

    Ig = trapz(4*pi*R**2*gr*(1/R-vr), R)
    Isph = trapz(4*pi*R**2*(1/R-vr), R)
    v0 = vr[0]

    print "Ig = %.14f_dp / rc" % Ig
    print "Isph = %.14f_dp * rc**2" % Isph
    print "v0 = %.14f_dp / rc" % v0
    print
```

    gr norm: 1.00000000085
    Ig = 2.01027837029584_dp / rc
    Isph = 0.08490520777895_dp * rc**2
    v0 = 6.86830681054508_dp / rc

    gr norm: 1.00000000001
    Ig = 2.01031976486788_dp / rc
    Isph = 0.08490788196539_dp * rc**2
    v0 = 6.86367256211640_dp / rc

    gr norm: 1.0
    Ig = 2.01032017805483_dp / rc
    Isph = 0.08490790865855_dp * rc**2
    v0 = 6.86366259004854_dp / rc

    gr norm: 1.0
    Ig = 2.01032018218137_dp / rc
    Isph = 0.08490790892400_dp * rc**2
    v0 = 6.86366256490661_dp / rc


# The Euler identity

#### Proof of the Euler identity using SymPy

We begin by importing some stuff and defining our functions:

```python
import numpy as np
import sympy
print("Using sympy version", sympy.__version__)
sympy.init_session(quiet=True)
```

    Using sympy version 1.3

We can cut to the chase by evaluating the equality with `sympy`:

```python
exp(I * pi) + 1 == 0
```

    True

We are done! ■


```python
import numpy as np
from sympy import *
init_printing()
```

```python
x, y = symbols('x y', real=True)
```

```python
a = 0.7190027e-2
b = 0.3956443e-5
c = -0.1842722e-8
d = 0.3471851e-12
e = -0.2616792e-16
k = -0.234471
```

```python
# Eq(a*y + b*(y**2) + c*(y**3) + d*(y**4) + e*(y**5) + k -x)
# solve(Eq(a*y + b*(y**2) + c*(y**3) + d*(y**4) + e*(y**5) + k -x),y)
# solveset(Eq(a*y + b*(y**2) + c*(y**3) + k -x),y, domain=S.Reals)
# solve(Eq(a*y + b*(y**2) + c*(y**3) + d*(y**4)+ k -x),y)
equation = sympify(a*y + b*(y**2) + c*(y**3) + d*(y**4)+e*(y**5)+ k -x)
print(equation)
```

    -x - 2.616792e-17*y**5 + 3.471851e-13*y**4 - 1.842722e-9*y**3 + 3.956443e-6*y**2 + 0.007190027*y - 0.234471

```python
# equat = Eq(equation)
Eq(equation)
```

```python
T = np.arange(32,)
```

```python

```


```python
from sympy import *
init_printing(use_unicode=True)
```

```python
from typing import List, Dict
```

# Guía 1

```python
def coordenadas_de_un_vector_respecto_a_una_base_B(v: Matrix, coordenadas: Dict[Symbol, Matrix]):
    '''
    Devuelve las coordenadas de un vector respecto de una base B = {v1, v2, ..., vn}

    v: Es un vector escrito con símbolos (v1, v2, etc)
    coordenadas: es un diccionario que indica cómo traducir cada vector en coordenadas de la base B

    Ejemplo de un ejercicio completo:
    v1, v2, v3 = symbols('v1 v2 v3')

    B = [v1, v2, v3]

    coordenadas = {
        v1: Matrix([1, 0, 0]),
        v2: Matrix([0, 1, 0]),
        v3: Matrix([0, 0, 1]),
    }

    w1 = 0*v1 + 0*v2 + 0*v3
    w2 = v1 - v2
    w3 = 2*v2 - v3

    w2_B = coordenadas_de_un_vector_respecto_a_una_base_B(w2, coordenadas)
    w3_B = coordenadas_de_un_vector_respecto_a_una_base_B(w3, coordenadas)

    G_B = Matrix([[60, 30, 20], [30, 20, 15], [20, 15, 12]])

    G = matriz_de_gram(producto_interno_por_definicion, G_B, w2_B, w3_B)

    area_de_un_triangulo(G)
    '''
    w = v
    for vector in coordenadas:
        w = w.subs(vector, coordenadas[vector])
    return w

def encontrar_a(B: List[Matrix], a: Symbol):
    ''' MATRICES CUADRADAS
    Halla el o los valores que puede tener a para que la base dada sea una base efectivamente
    (para que los vectores sean linealmente independientes)

    Ejemplo:
    a = Symbol('a')
    B = [Matrix([a, 1, 2]), Matrix([3, 2, 3]), Matrix([1, -a, 1])]
    encontrar_a(B, a)
    '''
    return solve(det(Matrix.hstack(*B)), a)

def algoritmo_espacio_columna(S: List[Matrix]):
    '''
    Encuentra el espacio columna de un subespacio
    Devuelve una tupla con una lista de los vectores que generan el espacio columna
    y los índices de los pivotes.

    S: es una lista de los vectores que generan al subespacio S
    '''
    B_S = Matrix.hstack(*S)
    _, pivotes_S = B_S.rref()
    S_li = []
    for pivote in pivotes_S:
        S_li.append(S[pivote])
    return S_li, pivotes_S

def dos_subespacios_generan_el_mismo_subespacio(S1: List[Matrix], S2: List[Matrix]):
    '''
    Verifica si dos subespacios son el mismo subespacio devolviendo True para ese
    caso y False en caso contrario.

    Ejemplo:
    S1 = [Matrix([-4, -5, -3]), Matrix([26, 41, 23])]
    bien = [Matrix([11, 18, 10]), Matrix([15, 23, 13])]
    mal_1 = [Matrix([-3, 10, 8]), Matrix([-6, 11, 10])]
    mal_2 = [Matrix([7, 12, 2]), Matrix([9, 14, 1])]
    mal_3 = [Matrix([7, 2, 12]), Matrix([9, 1, 14]), 2 * Matrix([9, 1, 14])]

    dos_subespacios_generan_el_mismo_subespacio(S1, bien), dos_subespacios_generan_el_mismo_subespacio(S1, mal_1), dos_subespacios_generan_el_mismo_subespacio(S1, mal_2), dos_subespacios_generan_el_mismo_subespacio(S1, mal_3)
    '''
    S1_li, pivotes_S1 = algoritmo_espacio_columna(S1)
    S2_li, pivotes_S2 = algoritmo_espacio_columna(S2)

    B_S1_li = Matrix.hstack(*S1_li)
    B_S2_li = Matrix.hstack(*S2_li)

    B = Matrix.hstack(B_S1_li, B_S2_li)
    _, pivotes_B = B.rref()

    return len(pivotes_B) == len(pivotes_S1) and len(pivotes_B) == len(pivotes_S2)

''' OBTENER LOS 4 FANTÁSTICOS
A.nullspace() -> devuelve el espacio nulo de A
A.columnspace() -> devuelve el espacio columna de A
A.T.columnspace() -> devuelve el espacio columna de A traspuesta
A.T.nullspace() -> devuelve el espacio nulo de A de a traspuesta
'''

#Función que calcula el Wronskiano
#(existe wronskian() directamente de sympy, pero no devuelve la matriz wronskiana, sino sólo el valor del wronskiano)

def Wronskiano(B, x, return_matrix = True):
    """
    Calcula el Wronskiano del conjunto de funciones
    contenidas en B en función de la variable x.

    Args:
        B (lista de funciones):
            El conjunto de funciones sobre el cual
            calcular el Wronskiano en función de la
            variable x.
        x (objeto tipo Symbol):
            Variable independiente.
        return_matrix (bool, opcional):
            Si es cierto, devuelve la matriz Wronskiana
            además del Wronskiano.

    Returns:
        w (función):
            Wronskiano del conjunto B.
        W (objeto tipo Matrix, opcional):
            Si return_matrix es True, devuelve la matriz
            wronskiana.

    Ejemplo:
    #Para ver si un conjunto es linealmente independiente en su correspondiente espacio vectorial, podemos emplear el Wronskiano.
    x = Symbol('x') # Definición de la variable simbólica
    F = [1, sinh(x), cosh(x)] #Conjunto de funciones

    W, M_W = Wronskiano(F,x,return_matrix=True) #W es el Wronskiano, M_W es la matriz wronskiana
    print('La matriz wronskiana es: '); display(M_W)
    print('y el Wronskiano resulta: '); display(W)

    #W.subs(x,0) #Si quisiera evaluar el Wronskiano en algún valor de x, por ejemplo en este caso x=0
    """

    n = len(B) # Cantidad de vectores

    # Calculo la matrix wronskiana
    W = zeros(n,n)
    for i in range(n):
        for j, f in enumerate(B):
            W[i,j] = diff(f,x,i)

    if return_matrix:
        return W.det(), W

    return W.det()
```

    ' OBTENER LOS 4 FANTÁSTICOS\nA.nullspace() -> devuelve el espacio nulo de A \nA.columnspace() -> devuelve el espacio columna de A\nA.T.columnspace() -> devuelve el espacio columna de A traspuesta\nA.T.nullspace() -> devuelve el espacio nulo de A de a traspuesta\n'

# Guía 2

```python
def matriz_de_transformacion_lineal(B: List[Matrix], img_B: List[Matrix]):
    '''
    Halla la matriz de una transformación lineal T
    Si T(x) = A * x, esta función encuentra A dada una base B y la imágen
    por T de esa base B, es decir T(B).

    B: es una lista de vectores que conforman la base B
    img_B: es una lista de vectores que corresponden a las imágenes de los vectores de la base B

    Ejemplo:
    B = Matrix([[2, 2, 1], [1, -2, 2], [-2, 1, 2]])
    img_B = Matrix([[0, -1, -1], [1, 0, -1], [1, 0, -1], [1, 1, 0]])
    y = Matrix([2, 5, 5, 3])
    '''
    return Matrix.hstack(*img_B) * Matrix.hstack(*B).inv()

def imagen_por_T_de_un_subespacio(B: List[Matrix], img_B: List[Matrix], S: List[Matrix]):
    '''
    Halla la imágen por T de un subespacio S dada una base B y la imágen por T de B

    B: es una lista de vectores que conforman la base B
    img_B: es una lista de vectores que corresponden a las imágenes de los vectores de la base B
    S: es una lista de vectores que generan el subespacio S
    '''
    img_S = matriz_de_transformacion_lineal(B, img_B) * Matrix.hstack(*S)
    return img_S

def preimagen_subespacio(B,img_B,Au):
    """
    Dada una transformación lineal definida por una base B y las imágenes img_B
    de cada uno de los vectores de B, la función devuelve una base del subespacio
    T^(-1)(U), donde U = Nul(Au).
    """
    B_matrix = Matrix.hstack(*B)
    Eb, pivots = B_matrix.rref(pivots=True)

    img_B_matrix = Matrix.hstack(*img_B)
    sol = (Au * img_B_matrix).nullspace()
    B_T_inv_U = [B_matrix * v for v in sol]
    return B_T_inv_U

# TODO: EN CONSTRUCCIÓN
def nucleo_de_operador_diferencial(L):
    '''
    '''
    x = Symbol('x')
    Nu_L = set()
    raices = str(L).split('*')
    for raiz in raices:
        try:
            l = -int(raiz[raiz.index('-')+1])
        except:
            l = int(raiz[raiz.index('+')+1])

        if '**' in raiz:
            k = int(raiz.split('**')[1])
        else:
            k = 1

        for i in range(k):
            Nu_L.add((x**(i))*exp(-l*x))
    return Nu_L

def solucion_particular(Nu_L, Nu_A):
    Nu_AL = Nu_L | Nu_A
    yp_expr = ''
    for i, e in enumerate(Nu_AL - Nu_L):
        yp_expr += f'a{i+1}*{e}+'
    return yp_expr[:-1]

D, I = symbols('D I')
L = (D-2*I)*(D-4*I)*(D+3*I)**2

Nu_L = nucleo_de_operador_diferencial(L)
Nu_A = nucleo_de_operador_diferencial('(D+3I)^6')

solucion_particular(Nu_L, Nu_A)
```

# Guía 3

## Productos Internos

```python
'''
Para los productos internos en general

A: es una matriz
B: es una matriz armada con los vectores que conforman una base B

x, y: son vectores
G: es la matriz de Gram
'''

def producto_interno_matrices_3_x_3(A: Matrix, B: Matrix, G: Matrix=None):
    return 1/2 * (B.T * A).trace()

def producto_interno_por_definicion(x: Matrix, y: Matrix, G: Matrix):
    return (y.T * G * x)[0]

def producto_interno_polinomios_integral(p, q, x, limite_inferior=0, limite_superior=1, a=1):
    return integrate(p * q * a, (x, limite_inferior, limite_superior))

def producto_interno_canonico(x, y, G):
    return (y.T * x)[0]
```

## Matriz de Gram

```python
def matriz_de_gram(producto_interno, G_B, *vectores_ordenados):
    '''
    Dado un producto interno, una matriz de Gram que define ese producto interno (si es de matrices
    enviar cualquier cosa) y una lista de vectores ordenados devuelve la matriz de Gram de esos vectores

    producto_interno: es el nombre de una función definida
    G_B: es la matriz de Gram del producto interno
    *vectores_ordenados: es una lista de vectores que se tienen que mandar ordenados

    Ejemplo:

    '''
    G = eye(len(vectores_ordenados))
    for i in range(len(vectores_ordenados)):
        for j, vector in enumerate(vectores_ordenados):
            G[i,j] = producto_interno(vectores_ordenados[i], vectores_ordenados[j], G_B)

    return G

```

## Norma, Ángulo y Distancia

```python
def norma(x, producto_interno, G):
    '''
    Dado un vector, una matriz de producto interno y un producto interno, se computa la norma de dicho vector.

    x, y: son vectores
    producto_interno: es el nombre de una función definida
    G_B: es la matriz de Gram del producto interno
    '''
    return sqrt(producto_interno(x, x, G))

def angulo(x: Matrix, y: Matrix, G: Matrix, producto_interno):
    '''
    Dados dos vectores, una matriz de producto interno y un producto interno, se computa el ángulo entre
    dichos vectores.

    x, y: son vectores
    producto_interno: es el nombre de una función definida
    G_B: es la matriz de Gram del producto interno
    '''
    return acos(producto_interno(x, y, G) / (norma(x, producto_interno, G) * norma(y, producto_interno, G)))

def distancia_de_un_vector_a_un_subespacio(v, B, G, producto_interno):
    '''AGARRAR CON PINZAS'''
    _v_tilda = v_tilda(v, B, eye(4), producto_interno)
    return sqrt(producto_interno(v, v, G) - (_v_tilda.T * G.inv() * _v_tilda)[0])
```

## Áreas (triángulo y paralelogramo)

```python
def area_de_un_paralelogramo(G: Matrix):
    '''
    Computa el área de un paralelogramo dada una matriz de Gram de los vértices de dicho triángulo

    G: es la matriz de Gram de los vectores que forman el paralelogramo cuya área se quiere calcular
    '''
    return G.det()

def area_de_un_triangulo(G: Matrix):
    '''
    Computa el área de un triángulo dada una matriz de Gram de los vértices de dicho triángulo

    G: es la matriz de Gram de los vectores que forman el triángulo cuya área se quiere calcular
    '''
    return 1/2 * sqrt(area_de_un_paralelogramo(G))

def resolver_area_de_un_triangulo_en_el_origen(G_B: Matrix, v1: Matrix, v2: Matrix, producto_interno):
    '''
    Calcula el área del triángulo de vértices v1 y v2 (siendo nunguno el vector nulo)

    Ejemplo:
    v1 = Matrix([1, 1, 0])
    v2 = Matrix([1, 0, 1])
    G_B = Matrix([[1, 1, 1], [1, 2, 2], [1, 2, 3]])
    resolver_area_de_un_triangulo_con_uno_de_los_vertices_en_el_origen(G_B, v1, v2, producto_interno_por_definicion)
    '''
    G_v1_v2 = matriz_de_gram(producto_interno, G_B, v1, v2)
    return area_de_un_triangulo(G_v1_v2)

def resolver_area_de_un_triangulo_corrido_del_origen(G_B: Matrix, v1: Matrix, v2: Matrix, v3: Matrix, producto_interno):
    '''
    Calcula el área del triángulo de vértices v1, v2, y v3 (siendo nunguno el vector nulo)

    Ejemplo:
    v1 = Matrix([3, 1, 2])
    v2 = Matrix([4, 2, 2])
    v3 = Matrix([4, 1, 3])
    G_B = Matrix([[1, 1, 1], [1, 2, 2], [1, 2, 3]])
    resolver_area_de_un_triangulo_con_uno_de_los_vertices_en_el_origen(G_B, v1, v2, v3, producto_interno_por_definicion)
    '''
    w1, w2, w3 = v1-v1, v2-v1, v3-v1
    G_w2_w3 = matriz_de_gram(producto_interno, G_B, w2, w3)
    return area_de_un_triangulo(G_v1_v2)
```

## Proyecciones y Simetrías

```python
def v_tilda(x: Matrix, B: List[Matrix], G: Matrix, producto_interno):
    return Matrix([producto_interno(x, B[i], G) for i in range(len(B))])

def proyeccion_ortogonal_de_un_vector_a_un_subespacio(x: Matrix, B: list, G: Matrix, producto_interno):
    return G.inv() * v_tilda(x, B, G, producto_interno)

def matriz_de_proyeccion_sobre_S1_en_direccion_de_S2_en_coordenadas_canonicas(S1: List[Matrix], S2: List[Matrix]):
    '''
    Arma la matriz de proyección sobre un subespacio S1 en dirección de otro subespacio S2 en coordenadas
    canónicas dadas dos listas con vectores generadores de cada subespacio en particular.

    S1: lista de vectores que generan al subespacio S1 (sobre el que se quiere proyectar)
    S1: lista de vectores que generan al subespacio S2 (la dirección)
    '''
    B_S1 = Matrix.hstack(*S1)
    B_S2 = Matrix.hstack(*S2)
    B = Matrix.hstack(B_S1, B_S2)

    P_BB = eye(B.rank())
    for i in range(B_S1.rank(), B_S1.rank()-1, -1):
        P_BB[i, i] = 0

    return B * P_BB * B.inv()

def matriz_de_simetria_sobre_S1_en_direccion_de_S2_en_coordenadas_canonicas(S1: List[Matrix], S2: List[Matrix]):
    '''
    Arma la matriz de simetríá sobre un subespacio S1 en dirección de otro subespacio S2 en coordenadas
    canónicas dadas dos listas con vectores generadores de cada subespacio en particular.

    S1: lista de vectores que generan al subespacio S1 (sobre el que se quiere hacer la simetría)
    S1: lista de vectores que generan al subespacio S2 (la dirección)
    '''
    B_S1 = Matrix.hstack(*S1)
    B_S2 = Matrix.hstack(*S2)
    B = Matrix.hstack(B_S1, B_S2)

    S_BB = eye(B.rank())
    for i in range(B_S1.rank(), B_S1.rank()-1, -1):
        S_BB[i, i] = -1

    return B * S_BB * B.inv()
```

## QR

```python
'''
Q, R = A.QRDecomposition()
'''
```

notebooks_varios/Funciones para ejercicios de guias 1-3.ipynb


# Maximum Likelihood and Maximum A Posterior

* We looked at the regularization term as a *penalty* term in the objective function.  There is another way to interpret the regularization term as well.  Specifically, there is a *Bayesian* interpretation.

\begin{eqnarray}
\min E^{\ast}(\mathbf{w}) &=& \max -E^{\ast}(\mathbf{w})\\
& =& \max \exp \left\{ -E^{\ast}(\mathbf{w})\right\}\\
&=& \max \exp \left\{ -\frac{1}{2}\sum_{n=1}^N \left( y(x_n, \mathbf{w}) - t_n \right)^2 - \frac{\lambda}{2}\left\| \mathbf{w} \right\|^2_2 \right\}\\
&=& \max \exp \left\{ -\frac{1}{2}\sum_{n=1}^N \left( y(x_n, \mathbf{w}) - t_n \right)^2 \right\}\exp\left\{-\frac{1}{2}\lambda\left\| \mathbf{w} \right\|^2_2\right\}\\
&=& \max \prod_{n=1}^N \exp \left\{ -\frac{1}{2} \left( y(x_n, \mathbf{w}) - t_n \right)^2 \right\}\exp\left\{-\frac{1}{2}\lambda\left\| \mathbf{w} \right\|^2_2\right\}
\end{eqnarray}

* So, this is a maximization of the *data likelihood* with a *prior*: $p(\mathbf{X}|\mathbf{w})p(\mathbf{w})$

* *Method of Maximum Likelihood:*
    * A *data likelihood* is how likely the data is given the parameter set
    * So, if we want to maximize how likely the data is to have come from the model we fit, we should find the parameters that maximize the likelihood
    * A common trick to maximizing the likelihood is to maximize the log likelihood.  Often makes the math much easier.  *Why can we maximize the log likelihood instead of the likelihood and still get the same answer?*
    * Consider: $\max \ln \exp \left\{ -\frac{1}{2}\left(y(x_n, \mathbf{w}) - t_n\right)^2\right\}$ We go back to our original objective.

* *Method of Maximum A Posteriori (MAP):*
    * Bayes Rule: $p(Y|X) = \frac{p(X|Y)p(Y)}{p(X)}$
    * Consider: $p(\mathbf{w}|\mathscr{D}) = \frac{p(\mathscr{D}|\mathbf{w})p(\mathbf{w})}{p(\mathscr{D})}$, i.e., posterior $\propto$ likelihood $\times$ prior

## Maximum Likelihood vs. Maximum A Posteriori (MAP)

* Lets look at this in terms of binary variables, e.g., Flipping a coin:  $X =1$ is heads, $X=0$ is tails
* Let $\mu$ be the probability of heads.  If we know $\mu$, then: $P(x = 1 |\mu) = \mu$ and $P(x = 0|\mu) = 1-\mu$
\begin{eqnarray}
P(x|\mu) = \mu^x(1-\mu)^{1-x} = \left\{\begin{array}{c c}\mu & \text{ if } x=1 \\ 1-\mu & \text{ if } x = 0 \end{array}\right.
\end{eqnarray}

* This is called the *Bernoulli* distribution.  The mean and variance of a Bernoulli distribution is:
\begin{equation}
E[x] = \mu
\end{equation}
\begin{equation}
E\left[(x-\mu)^2\right] = \mu(1-\mu)
\end{equation}
* So, suppose we conducted many Bernoulli trials (e.g., coin flips) and we want to estimate $\mu$

### Method: Maximum Likelihood
\begin{eqnarray}
p(\mathscr{D}|\mu) &=& \prod_{n=1}^N p(x_n|\mu) \\
&=& \prod_{n=1}^N \mu^{x_n}(1-\mu)^{1-x_n}
\end{eqnarray}

* Maximize : (*What trick should we use?*)
\begin{eqnarray}
\mathscr{L} = \sum_{n=1}^N x_n \ln \mu + (1-x_n)\ln(1-\mu)
\end{eqnarray}

\begin{eqnarray}
\frac{\partial \mathscr{L}}{\partial \mu} =  0 &=& \frac{1}{\mu}\sum_{n=1}^N x_n - \frac{1}{1-\mu }\sum_{n=1}^N (1 - x_n)\\
0 &=& \frac{(1-\mu) \sum_{n=1}^N x_n - \mu \sum_{n=1}^N (1- x_n)}{\mu(1-\mu)}\\
0 &=& \sum_{n=1}^N x_n - \mu \sum_{n=1}^N x_n - \mu \sum_{n=1}^N 1 + \mu \sum_{n=1}^N x_n\\
0 &=& \sum_{n=1}^N x_n - \mu N\\
\mu &=& \frac{1}{N}\sum_{n=1}^N x_n = \frac{m}{N}
\end{eqnarray}
where $m$ is the number of successful trials.

* So, if we flip a coin 1 time and get heads, then $\mu = 1$ and probability of getting tails is 0.  *Would you believe that? We need a prior!*

### Method: Maximum A Posteriori:

* Look at several independent trials.  Consider N = 3 and m = 2 (N is number of trials, m is number of successes) and look at all ways to get 2 H and 1 T:
     * H H T  $\rightarrow \mu \mu (1-\mu) = \mu^2(1-\mu)$
     * H T H  $\rightarrow \mu  (1-\mu) \mu  = \mu^2(1-\mu)$
     * T H H $\rightarrow (1-\mu) \mu   \mu  = \mu^2(1-\mu)$

* $\left(\begin{array}{c} 3 \\ 2 \end{array}\right) \mu^2(1-\mu) \rightarrow \left(\begin{array}{c} N \\ m \end{array}\right) \mu^m(1-\mu)^{N-m} = \frac{N!}{(N-m)!m!}\mu^m(1-\mu)^{N-m} $
* This is the Binomial Distribution, gives the probability of $m$ observations of $x=1$ out of N independent trails
* So, what we saw is that we need a prior.  We want to incorporate our prior belief. Let us place a prior on $\mu$
\begin{equation}
Beta(\mu|a,b) = \frac{\Gamma(a + b)}{\Gamma(a)\Gamma(b)}\mu^{a-1}(1-\mu)^{b-1}
\end{equation}
\begin{equation}
E[\mu] = \frac{a}{a + b}
\end{equation}
\begin{equation}
Var[\mu] = \frac{ab}{(a+b)^2(a+b+1)}
\end{equation}
* Note: $\Gamma(x) = \int_0^\infty u^{x-1}e^{-u} du$ and when $x$ is an integer, then it simplifies to $(x-1)!$
* Calculation of the posterior, Take $N = m + l$ observations:
\begin{eqnarray}
p(\mu | m, l, a, b) &\propto& Bin(m,l|\mu)Beta(\mu|a,b) \\
&\propto& \mu^m(1-\mu)^l\mu^{a-1}(1-\mu)^{b-1}\\
&=& \mu^{m+a-1}(1-\mu)^{l+b-1}
\end{eqnarray}
* What does this look like?  Beta: $a \leftarrow m+a$, $b \leftarrow l+b$
* So, what's the posterior?
\begin{equation}
p(\mu | m, l, a, b) = \frac{\Gamma(m+a+l+b)}{\Gamma(m+a)\Gamma(l+b)}\mu^{m+a-1}(1-\mu)^{l+b-1}
\end{equation}
* *Conjugate Prior Relationship:* When the posterior is the same form as the prior
* Now we can maximize the (log of the) posterior:
\begin{eqnarray}
\max_\mu ((m+a-1) \ln \mu + (l+b-1) \ln (1-\mu))
\end{eqnarray}
\begin{eqnarray}
\frac{\partial \mathscr{L}}{\partial \mu} =  0&=& \frac{m + a -1}{\mu} - \frac{l + b - 1}{1-\mu}\\
&=& (1-\mu)(m+a-1) - \mu(l+b-1)\\
&=& (m+a-1) - \mu(m+a-1) - \mu(l+b-1)\\
\mu &=& \frac{m+a-1}{m+a+l+b-2}
\end{eqnarray}
* This is the MAP solution.  *So, what happens now when you flip one heads, two heads, etc.?*
* Discuss online updating of the prior.  Eventually the data takes over the prior.

```python
import numpy as np
import matplotlib.pyplot as plt
import math
%matplotlib inline

priorA = 2
priorB = 2
def plotBeta(a=priorA,b=priorB):
	'''plotBeta(a=1,b=1): Plot plot beta distribution with parameters a and b'''
	xrange = np.arange(0,1,0.001)  #get equally spaced points in the xrange
	normconst = math.gamma(a+b)/(math.gamma(a)*math.gamma(b))
	beta = normconst*xrange**(a-1)*(1-xrange)**(b-1)
	fig = plt.figure()
	p1 = plt.plot(xrange,beta, 'g')
	plt.show()

#Beta Distribution
# plotBeta(priorA,priorB);

trueMu = 0.5
numFlips = 100
flipResult = []
for flip in range(numFlips):
    flipResult.append(np.random.binomial(1,trueMu,1)[0])
    print(flipResult)
    print('Frequentist/Maximum Likelihood Probability of Heads:' + str(sum(flipResult)/len(flipResult)))
    print('Bayesian/MAP Probability of Heads:' + str((sum(flipResult)+priorA-1)/(len(flipResult)+priorA+priorB-2)))
    if (input("Hit enter to continue, or q to quit...\n") == "q"):
        print("quitting...\n")
        break
```

## The Gaussian Distribution:
* Consider a univariate Gaussian distribution:
\begin{equation}
\mathscr{N}(x|\mu, \sigma^2) = \frac{1}{\sqrt{2\pi \sigma^2}}\exp\left\{ -\frac{1}{2}\frac{(x-\mu)^2}{\sigma^2} \right\}
\end{equation}
* $\sigma^2$ is the variance OR $\frac{1}{\sigma^2}$ is the *precision*
* So, as $\lambda$ gets big, variance gets smaller/tighter.  As $\lambda$ gets small, variance gets larger/wider.
* The Gaussian distribution is also called the *Normal* distribution.
* We will often write $N(x|\mu, \sigma^2)$ to refer to a Gaussian with mean $\mu$ and variance $\sigma^2$.
* *What is the multi-variate Gaussian distribution?*

* What is the expected value of $x$ for the Gaussian distribution?
\begin{eqnarray}
E[x] &=& \int x p(x) dx \\
     &=& \int x \frac{1}{\sqrt{2\pi \sigma^2}}\exp\left\{ -\frac{1}{2}\frac{(x-\mu)^2}{\sigma^2} \right\} dx
\end{eqnarray}
* *Change of variables:*  Let
\begin{eqnarray}
y &=& \frac{x-\mu}{\sigma} \rightarrow x = \sigma y + \mu\\
dy &=& \frac{1}{\sigma} dx \rightarrow dx = \sigma dy
\end{eqnarray}
* Plugging this into the expectation:
\begin{eqnarray}
E[x] &=& \int \left(\sigma y + \mu  \right)\frac{1}{\sqrt{2\pi}\sigma} \exp\left\{ - \frac{1}{2} y^2 \right\} \sigma dy \\
&=& \int \frac{\sigma y}{\sqrt{2\pi}} \exp\left\{ - \frac{1}{2} y^2 \right\} dy + \int \frac{\mu}{\sqrt{2\pi}} \exp\left\{ - \frac{1}{2} y^2 \right\} dy
\end{eqnarray}
* The first term is an odd function: $f(-y) = -f(y)$  So, $E[x] = 0 + \mu = \mu$

## MLE of Mean of Gaussian

*  Let $\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{x}_N$ be samples from a multi-variance Normal distribution with known covariance matrix and an unknown mean.  Given this data, obtain the ML estimate of the mean vector.
	\begin{equation}
	p(\mathbf{x}_k| {{\mu}}) = \frac{1}{(2\pi)^{\frac{l}{2}}\left| \Sigma \right|^{\frac{1}{2}}}\exp\left( -\frac{1}{2}(\mathbf{x}_k - {{\mu}})^T\Sigma^{-1}(\mathbf{x}_k - {\mu})\right)
	\end{equation}
* We can define our likelihood given the $N$ data points.  We are assuming these data points are drawn independently but from an identical distribution (i.i.d.):
	\begin{equation}
	\prod_{n=1}^N p(\mathbf{x}_n| {{\mu}}) = \prod_{n=1}^N \frac{1}{(2\pi)^{\frac{l}{2}}\left| \Sigma \right|^{\frac{1}{2}}}\exp\left( -\frac{1}{2}(\mathbf{x}_n - {{\mu}})^T\Sigma^{-1}(\mathbf{x}_n - {\mu})\right)
	\end{equation}
*  We can apply our "trick" to simplify
	\begin{eqnarray}
	\mathscr{L} &=& \ln \prod_{n=1}^N p(\mathbf{x}_n| {{\mu}}) = \ln \prod_{n=1}^N \frac{1}{(2\pi)^{\frac{l}{2}}\left| \Sigma \right|^{\frac{1}{2}}}\exp\left( -\frac{1}{2}(\mathbf{x}_n - {{\mu}})^T\Sigma^{-1}(\mathbf{x}_n - {\mu})\right)\\
	&=& \sum_{n=1}^N  \ln \frac{1}{(2\pi)^{\frac{l}{2}}\left| \Sigma \right|^{\frac{1}{2}}}\exp\left( -\frac{1}{2}(\mathbf{x}_n - {{\mu}})^T\Sigma^{-1}(\mathbf{x}_n - {\mu})\right)\\
	&=& \sum_{n=1}^N  \left( \ln \frac{1}{(2\pi)^{\frac{l}{2}}\left| \Sigma \right|^{\frac{1}{2}}} + \left( -\frac{1}{2}(\mathbf{x}_n - {{\mu}})^T\Sigma^{-1}(\mathbf{x}_n - {\mu})\right) \right) \\
	&=&  - N \ln (2\pi)^{\frac{l}{2}}\left| \Sigma \right|^{\frac{1}{2}} + \sum_{n=1}^N  \left( -\frac{1}{2}(\mathbf{x}_n - {{\mu}})^T\Sigma^{-1}(\mathbf{x}_n - {\mu}) \right)
	\end{eqnarray}
* Now, lets maximize:
	\begin{eqnarray}
	\frac{\partial \mathscr{L}}{\partial \mu} &=& \frac{\partial}{\partial \mu} \left[- N \ln (2\pi)^{\frac{l}{2}}\left| \Sigma \right|^{\frac{1}{2}} + \sum_{n=1}^N  \left( -\frac{1}{2}(\mathbf{x}_n - {{\mu}})^T\Sigma^{-1}(\mathbf{x}_n - {\mu}) \right)\right] = 0 \\
	&\rightarrow& \sum_{n=1}^N  \Sigma^{-1}(\mathbf{x}_n - {\mu}) = 0\\
	&\rightarrow& \sum_{n=1}^N  \Sigma^{-1}\mathbf{x}_n  = \sum_{n=1}^N  \Sigma^{-1} {\mu}\\
	&\rightarrow& \Sigma^{-1} \sum_{n=1}^N \mathbf{x}_n  = \Sigma^{-1} {\mu} N\\
	&\rightarrow& \sum_{n=1}^N \mathbf{x}_n  = {\mu} N\\
	&\rightarrow& \frac{\sum_{n=1}^N \mathbf{x}_n}{N} = {\mu}\\
	\end{eqnarray}
* So, the ML estimate of $\mu$ is the sample mean!

## MAP of Mean of Gaussian

* To get a MAP estimate of the mean of a Gaussian, we apply a prior distribution and maximize the posterior.
* Lets use a Gaussian prior on the mean (because it has a *conjugate prior* relationship)

\begin{eqnarray}
p(\mu|X, \mu_0, \sigma_0^2, \sigma^2) &\propto& \mathscr{N}(X|\mu, \sigma^2)\mathscr{N}(\mu|\mu_0, \sigma_0^2)\\
&=& \prod_{n=1}^N \left(\frac{1}{\sqrt{2\pi \sigma^2}} \exp\left\{-\frac{1}{2\sigma^2}\left(x_n-\mu\right)^2 \right\}\right)\frac{1}{\sqrt{2\pi \sigma_0^2}} \exp\left\{-\frac{1}{2\sigma_0^2}\left(\mu-\mu_0\right)^2 \right\}\nonumber\\
\mathscr{L} &=& -\frac{N}{2}\ln(2\pi\sigma^2) - \frac{1}{2\sigma^2}\sum_{n=1}^N(x_n-\mu)^2 - \frac{N}{2}\ln(2\pi \sigma_0^2) - \frac{N}{2\sigma_0^2}(\mu - \mu_0)^2\\
\frac{\partial \mathscr{L}}{\partial \mu} &=&  - \frac{N}{\sigma^2}\mu - \frac{N}{\sigma_0^2}\mu + \frac{N}{\sigma_0^2}\mu_0 + \frac{1}{\sigma^2}\sum_{n=1}^N x_n = 0\\
N\mu\left(\frac{\sigma_0^2 + \sigma^2}{\sigma^2\sigma_0^2} \right) &=& \frac{1}{\sigma^2}\sum_{n=1}^N x_n + \frac{1}{\sigma_0^2}\mu_0 \\
\mu_{MAP} &=& \frac{\sigma_0^2}{N\sigma_0^2 + N\sigma^2}\sum_{n=1}^N x_n + \frac{\mu_0\sigma^2}{N\sigma_0^2 + N\sigma^2}
\end{eqnarray}

* *Does this result make sense?*

```python

```


```python
import numpy as np
import sympy as sy
import matplotlib.pyplot as plt
from openmm import unit
```

# Double well potential

Our double well potential is described by the following expression:

$$
V(x,y,z)=E_{0}\left[ \left(\frac{x}{a}\right)^4 -2\left(\frac{x}{a}\right)^2 \right]-\frac{b}{a}x + \frac{1}{2}k\left( y^2 + z^2 \right)
$$ (potential)

This potential can be split in two summands, the first one with the potential for the $X$ axis, and a second term with the potential for the $Y$ and $Z$ axes:

$$
V(x,y,z)=V_{x}(x)+V_{y,z}(y,z)
$$ (potential-decomposed)

## A double well potential along $X$

The one dimensional potential $V_{x}(x)$ is a function of $x$ as well as three parameters: $E_{0}$, $a$, $b$.

$$
V_{x}(x)=E_{0}\left[ \left(\frac{x}{a}\right)^4 -2\left(\frac{x}{a}\right)^2 \right]-\frac{b}{a}x + \frac{1}{2}k\left( y^2 + z^2 \right)
$$ (x_potential)

Let's see the meaning of these constants. For this purpose the following method is defined to evaluate the contribution of the potential energy terms corresponding to the double well in $X$.

```python
def double_well_potential_1D(x,Eo,a,b):
    return Eo*((x/a)**4-2*(x/a)**2)-(b/a)*x
```

### Symmetric case ($b=0$)

A symmetric double well potential can be represented by the former mathematical expression when $b=0$. In this situation, the double well minima are placed in $x=-a$ and $x=a$. And $E_{0}$ is the value of the barrier height (equal for both basins). Lets verify these statements with an example:

```python
Eo=3.0 * unit.kilocalories_per_mole # barrier height when the double well is symmetric.
a=1.0 * unit.nanometers # Absolute value of the coordinates of minima (the potential is an even function).
b=0.0 * unit.kilocalories_per_mole # No need to explanation at this moment

x_serie = np.arange(-5., 5., 0.05) * unit.nanometers

plt.plot(x_serie, double_well_potential_1D(x_serie,Eo,a,b), 'r-')
plt.ylim(-4,1)
plt.xlim(-2,2)
plt.grid()
plt.xlabel("X ({})".format(unit.nanometers))
plt.ylabel("Energy ({})".format(unit.kilocalories_per_mole))
plt.title("Symmetric Double Well")

plt.hlines(-3, -1.7, -1, color='gray', linestyle='--')
plt.hlines(0, -1.7, 0, color='gray', linestyle='--')
plt.vlines(-1.6, -3, 0, color='gray', linestyle='--')
plt.vlines(1, -4, -3, color='gray', linestyle='--')
plt.text(-1.85, -1.5, 'Eo', fontsize=12)
plt.text(1.1, -3.6, 'a', fontsize=12)

plt.show()
```

You can play with the last cell, changing the $E_{0}$ and $a$ values, to check the consistency of their description. Or you can instead work with the first derivative of the potential to do the same analytically:

```python
x, Eo, a = sy.symbols('x Eo a')
f = Eo*((x/a)**4-2*(x/a)**2)

g=sy.diff(f,x) # Primera derivada con respecto a x de la funcion f
```

```python
g
```

$\displaystyle Eo \left(- \frac{4 x}{a^{2}} + \frac{4 x^{3}}{a^{4}}\right)$

The first derivative can be factorized to unveil the value of its three roots: $x=0$, the barrier position, and $x=a$ y $x=-a$, the minima positions.

```python
sy.factor(g)
```

$\displaystyle \frac{4 Eo x \left(- a + x\right) \left(a + x\right)}{a^{4}}$

The height of the barrier, from the bottom of the basins to its top, can then be calculated:

$$
V_{x}(0)-V_{x}(c)=0-E_{0}\left[ 1-2 \right]=E_{0}
$$ (x_barrier)

#### Frequency of small oscillations around the minima

Now that the role of $E_{0}$ and $a$ is clear for the symmetric double well, we are interested in the frequency of the small oscillations at the energy minima. This frequency will be our time of reference to choose an appropriate integration time step of at the time of simulate the dynamics of a particle in this potential. This can be done attending to the second derivative of the potential.

The value of any mathematical function close enough to a minimum can be approximated by the value of the function at the minimum plus the resulting contribution of an harmonic potential. The stiffness of this harmonic potential is equal the to value of the second derivative of the function at the minimum. This is what is known as the Taylor expansion of any function (truncated in the third grade):

$$
f(x) \approx f(x_{0}) + f'(x_{0})(x-x_{0}) + \frac{1}{2} f''(x_{0})(x-x_{0})^{2}
$$ (taylor)

And by definition of minimum:

$$
f'(x_{0})=0
$$ (minimum_definition)

So:

$$
f(x) \approx f(x_{0}) + \frac{1}{2} f''(x_{0})(x-x_{0})^{2}
$$ (taylor_2)

At this point lets make a break to talk about the harmonic potential. We all know the hooks law to describe the force  suffered by a mass attached to an ideal spring around the equilibrium position:

$$
F(x) = -k(x-x_{0})
$$ (hook)

Where $k$ is the stiffness of the spring and $x_{0}$ is the equilibrium position. The potential energy $V(x)$ is now deduced given that:

$$
F(x) = -\frac{d V(x)}{dx}
$$ (grad_potential)

So, the spring force is the result of the first harmonic potential derivative:

$$
V(x) = \frac{1}{2} k (x-x_{0})^{2}
$$ (x_potential_2)

And the frequency of oscillation of the spring, or a particle goberned by the former potential, is:

$$
\omega = \sqrt{\frac{k}{m}}
$$ (omega)

Where $m$ is the mass of the particle. This way the potential can also be written as:

$$
V(x) = \frac{1}{2} k (x-x_{0})^{2} = \frac{1}{2} m \omega^{2} (x-x_{0})^{2}
$$ (potential_omega)

Now, going back to our Taylor expansion of any mathematical function $f(x)$. If the shape of the $f(x)$ around a minimum $x_{0}$ can be approximated with an harmonic potential, this means that the characteristic frequency $\omega$ of the oscillations around the near surroundings of the minimum is -by comparison with the potential behind the Hooke's law-:

$$
\omega = \sqrt{\frac{f''(x_{0})}{m}}
$$ (omega_2)

This way the frequency of the small oscillations of a particle with mass $m$ around a minimum can then by obtained from the value of the second derivative at the minimum:

```python
x, Eo, a = sy.symbols('x Eo a')
f = Eo*((x/a)**4-2*(x/a)**2)

gg=sy.diff(f,x,x) # Second derivative of f with respect x

gg
```

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

Let's look at the minimum at $x=a$:

```python
gg.subs({x:a})
```

$\displaystyle \frac{8 Eo}{a^{2}}$

In this case the frequency of the oscillations of a particle with mass $m$ is:

$$
\omega = \sqrt{\frac{8E_{0}}{ma^{2}}}
$$ (omega_example)

And the period is:

$$
T = \frac{2\pi}{\omega} = 2\pi \sqrt{\frac{ma^{2}}{8E_{0}}}
$$ (period_example)

We can see graphically how the Taylor expansion is a good aproximation when close enough to a minimum:

```python
def harmonic_well_potential_1D(x,k,a,Eo):
    return 0.5*k*(x-a)**2-Eo

Eo=3.0 * unit.kilocalories_per_mole # barrier height when the double well is symmetric.
a=1.0 * unit.nanometers # Absolute value of the coordinates of minima (the potential is an even function).
b=0.0 * unit.kilocalories_per_mole # No need to explanation at this moment
k=(8*Eo)/a**2 # harmonic stiffness

x_serie = np.arange(-5., 5., 0.05) * unit.nanometers

plt.plot(x_serie, double_well_potential_1D(x_serie,Eo,a,b), 'r-')
plt.plot(x_serie, harmonic_well_potential_1D(x_serie,k,a,Eo), color='k', linestyle='--')
plt.ylim(-4,1)
plt.xlim(-2,2)
plt.grid()
plt.xlabel("X ({})".format(unit.nanometers))
plt.ylabel("Energy ({})".format(unit.kilocalories_per_mole))
plt.title("Symmetric Double Well and Harmonic Potential")
plt.show()

x_serie = np.arange(-0.5, 1.5, 0.005) * unit.nanometers

plt.plot(x_serie, double_well_potential_1D(x_serie,Eo,a,b), 'r-')
plt.plot(x_serie, harmonic_well_potential_1D(x_serie,k,a,Eo), color='k', linestyle='--')
plt.ylim(-3.05,-2.8)
plt.xlim(0.9,1.1)
plt.grid()
plt.xlabel("X ({})".format(unit.nanometers))
plt.ylabel("Energy ({})".format(unit.kilocalories_per_mole))
plt.title("Harmonic approximation nearby a minimum")
plt.show()
```

### Assymetric case ($b\neq0$)

In the case of $b\neq 0$, our double well turns in to an assymetric potential. In this situation $E_{0}$ and $a$ have **approximately** the same interpretation, and $b$ can be **approximately** understood as the amount energy that basins shift up or down, depending on the relative position to $x=0$. In our double well, the left basin raises and the right basin drops when $b>0$. Lets see this in a plot:

```python
Eo=3.0 * unit.kilocalories_per_mole # barrier height when the double well is symmetric.
a=1.0 * unit.nanometers # Absolute value of the coordinates of minima (the potential is an even function).
b=1.0 * unit.kilocalories_per_mole # vertical shift of basins (approx.)

x_serie = np.arange(-5., 5., 0.05) * unit.nanometers

plt.plot(x_serie, double_well_potential_1D(x_serie,Eo,a,b), 'r-')
plt.ylim(-4,1)
plt.xlim(-2,2)
plt.grid()
plt.xlabel("X ({})".format(unit.nanometers))
plt.ylabel("Energy ({})".format(unit.kilocalories_per_mole))
plt.title("Asymmetric Double Well")

plt.hlines(-2, -1.45, -1, color='gray', linestyle='--')
plt.hlines(0.05, -1.45, 0, color='gray', linestyle='--')
plt.vlines(-1.4, -2, 0.05, color='gray', linestyle='--')
plt.vlines(-0.95, -4, -2, color='gray', linestyle='--')
plt.text(-1.75, -2.4, r'$\approx Eo-b$', fontsize=12)
plt.text(-0.9, -3.6, r'$\approx a$', fontsize=12)

plt.show()
```

The value of the energy barrier from the left minimum is $\approx E_{0}+b$, while $\approx E_{0}-b$ accounts for the barrier from the right minimum.

### Frequency of small oscillations around the minima.

Given that $b$ is included as a linear factor in the potential, its effect vanishes in the second derivative. As such, the harmonic approximation described for the symmetric double well is exact also for this case. But notice that the positions of the minima, and the position of the barrier, are slightly shifted with respect to those for the symmetric double well.

## An harmonic potential along $Y$ and $Z$

The behaviour of a particle in the double well potential along $X$ is independent of what happens along $Y$ and $Z$. We could keep the three dimensional potential equal to $V_{x}(x)$, but in this case the particle would diffuse freely in the subspace $Y-Z$. To avoid these, for aesthetic purposes only, let's add an harmonic well in those two axes:

$$
V_{yz}(y,z)= \frac{1}{2}k\left( y^2 + z^2 \right)
$$ (xy_potential)

The value of the elastic constant $k$ should be lower, or at least equal, to the harmonic approximation of $V_{x}(x)$ around the minima. Otherwise we should be aware of $V_{yz}$ limiting the integration time step. It's then suggested that:

$$
k\le \frac{8E_{0}}{a^2}
$$ (k_limit)


+ This notebook is part of lecture 17 *Orthogonal matrices and Gram-Scmidt* in the OCW MIT course 18.06 by Prof Gilbert Strang [1]
+ Created by me, Dr Juan H Klopper
    + Head of Acute Care Surgery
    + Groote Schuur Hospital
    + University Cape Town
    + Email me with your thoughts, comments, suggestions and corrections

Linear Algebra OCW MIT18.06 IPython notebook [2] study notes by Dr Juan H Klopper is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.

+ [1] OCW MIT 18.06
+ [2] Fernando Pérez, Brian E. Granger, IPython: A System for Interactive Scientific Computing, Computing in Science and Engineering, vol. 9, no. 3, pp. 21-29, May/June 2007, doi:10.1109/MCSE.2007.53. URL: http://ipython.org

```python
from IPython.core.display import HTML, Image
css_file = 'style.css'
HTML(open(css_file, 'r').read())
```

```python
from sympy import init_printing, symbols, Matrix, sin, cos, sqrt, Rational, GramSchmidt
from warnings import filterwarnings
```

```python
init_printing(use_latex = 'mathjax')
filterwarnings('ignore')
```

```python
theta = symbols('theta')
```

# Orthogonal basis
# Orthogonal matrix
# Gram-Schmidt

## Orthogonal basis

* Here we mean vectors *q*1,*q*2,...,*q*n

* We actually mean orthonormal vectors (for orthogonal or perpendicular and of unit length / normalized)
* Vectors that are orthogonal have a dot product equal to zero
    * If they are orthogonal
$$ {q}_{i}^{T}{q}_{j}={0} $$
    * If they are not
$$ {q}_{i}^{T}{q}_{j}\neq{0} $$

## Orthogonal matrix

* We can now put these (column) basis vectors into a matrix Q

* This brings about
$$ {Q}^{T}{Q}={I} $$

* In the case of the matrix Q being square the word *orthogonal matrix* is used
* When it is square we can calculate the inverse making
$$ {Q}^{T}={Q}^{-1} $$

* Consider the following permutation matrix with orthonormal column vectors

```python
Q = Matrix([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
Q, Q.transpose()
```

* In this example the transpose also contains orthonormal column vectors
* Multiplication gives the identity matrix

```python
Q.transpose() * Q
```

* Consider this example

```python
Q = Matrix([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])
Q, Q.transpose()
```

* The two column vectors are orthogonal and the length of each column vector is 1
* It is thus an orthogonal matrix

```python
Q.transpose() * Q
```

* The example below certainly has orthogonal column vectors, but they are not of unit length
$$ {Q}=\begin{bmatrix} 1 & 1 \\ 1 & {-1} \end{bmatrix} $$

* Well, we can change them into unit vectors by dividing each component by the length of that vector
$$ \sqrt { { \left( 1 \right)  }^{ 2 }+{ \left( 1 \right)  }^{ 2 } } =\sqrt { 2 } \\ \sqrt { { \left( 1 \right)  }^{ 2 }+{ \left( -1 \right)  }^{ 2 } } =\sqrt { 2 }  $$
$$ Q=\frac { 1 }{ \sqrt { 2 }  } \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} $$

* As it stands QTQ is not the identity matrix

```python
Q = Matrix([[1, 1], [1, -1]])
Q.transpose() * Q
```

* Turning it into an orthogonal matrix

```python
Q = (1 / sqrt(2)) * Matrix([[1, 1], [1, -1]])
Q
```

```python
Q.transpose() * Q
```

* Consider this example with orthogonal (but not orthonormal) column vectors

```python
Q = Matrix([[1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1]])
Q
```

* Again, as it stands QTQ is not the identity matrix

```python
Q.transpose() * Q
```

* But turning it into an orthogonal matrix works

```python
Q = Rational(1, 2) * Matrix([[1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1]])
# Rational() creates a mathematical fraction instead of a decimal
Q
```

```python
Q.transpose() * Q
```

* Consider this matrix Q with orthogonal column vectors, but that is not square

```python
Q = Rational(1, 3) * Matrix([[1, -2], [2, -1], [2, 2]])
Q
```

* We now have a matrix with two column vectors that are normalized and orthogonal to each other and they form a basis for a plane (subspace) in ℝ3

* There must be a third column matrix of unit length, orthogonal to the other two so we end up with an orthogonal matrix

```python
Q = Rational(1, 3) * Matrix([[1, -2, 2], [2, -1, -2], [2, 2, 1]])
Q
```

```python
Q.transpose() * Q
```

* Let's make use of these matrices with orthonormal columns (which we will always denote with a letter Q) and project them onto their columnspace
* What would the projection matrix be?
$$ Q\underline{x}={b} \\ {P}={Q}{\left({Q}^{T}{Q}\right)}^{-1}{Q}^{T} $$

* Remember, though that for matrices with orthonormal column vectors we have QTQ is the identity matrix and we have
$$ {P}={Q}{Q}^{T} $$

* If additionally, Q is square, then we have independent columns and the columnspace contain the whole space ℝn and the projection matrix is the identity matrix in *n*
    * Remember QT = Q-1 in these cases making it easy to see that we get the identity matrix
    * Remember also that the projection matrix is symmetric
    * Lastly the projection matrix has the property of squaring it leaves us in the same spot, so here we will have (QQT)2=QQT

* All of this has the final consequence that
$$ {Q}^{T}Q\hat{x}={Q}^{T}\underline{b} \\ \hat{x}={Q}^{T}\underline{b} \\ \hat{x}_{i}={q}_{i}^{T}{b} $$

## Gram-Schmidt

* All of the above makes things quite easy, so we should try and create orthogonal matrices

* Good, let's start with two independent vectors **a** and **b** and try and create two orthogonal vectors **A** and **B** and then create two orthonormal vectors
$$ { q }_{ 1 }=\frac { A }{ \left\| A \right\|  } \\ { q }_{ 2 }=\frac { B }{ \left\| B \right\|  }  $$

* We can choose one of them as our initial vector, say **a** = **A**, so we have to get an orthogonal projection (to **a**) for **B**
* This is what we previously called the error vector **e**
$$ \underline{e}=\underline{b}-\underline{p} $$
* Remembering how to get **p** we have the following
$$ B=\underline { b } -\frac { { A }^{ T }\underline { b }  }{ { A }^{ T }A } A $$

* Let's do an example

```python
a = Matrix([1, 1, 1])
b = Matrix([1, 0, 2])
a, b
```

```python
A = a
A
```

```python
A.transpose() * b
```

```python
A.transpose() * A
```

```python
B = b - A
B
```

* Checking that they are perpendicular

```python
A.transpose() * B
```

* Now we have to create Q by turning **A** and **B** into unit vectors and place them in the same matrix

```python
A.normalized() # Easy way no normalize a matrix
```

```python
B.normalized()
```

```python
Q = Matrix([[sqrt(3) / 3, 0], [sqrt(3) / 3, -sqrt(2) / 2], [sqrt(3) / 3, sqrt(2) / 2]])
Q
```

* The columnspace of the original matrix (of two column vectors) and Q are the same

* In python™ we can use the following code

```python
# The column matrices (independant orthogonal column vectors) are entered indivisually inside square bracket []
A = [Matrix([1, 1, 1]), Matrix([1, 0, 2])]
A
```

```python
Q = GramSchmidt(A, True) # The True argument normalizes the columns
Q
```

## Example problems

### Example problem 1

* Create an orthogonal matrix from the following matrix
$$ \begin{bmatrix} 1 & 2 & 4 \\ 0 & 0 & 5 \\ 0 & 3 & 6 \end{bmatrix} $$

#### Solution

```python
A = [Matrix([1, 0, 0]), Matrix([2, 0, 3]), Matrix([4, 5, 6])]
A
```

```python
Q = GramSchmidt(A, True)
Q
```

* We can also consider QR-factorization

```python
from sympy.mpmath import matrix, qr
```

```python
A = matrix([[1, 2, 4], [0, 0, 5], [0, 3, 6]])
print(A)
```

```python
Q, R = qr(A)
```

```python
print(Q)
```

```python
print(R)
```

```python

```