from sys import *
from sympy import *
import numpy as np
#from sympy import Symbol, solve
from sympy import init_printing
from sympy.solvers.solveset import linsolve
from sympy.polys.polyfuncs import horner

def symbolCIP(nOrder):
    # Simple check
    if nOrder % 2 == 0:
        sys.exit("Order might be odd more than 3,\n"
                 "for issue 3, 5, 7, 9, etc.")

    # Set values
    coeff = symbols('a0:%d' % (nOrder + 1))
    xi, h = symbols('xi, h')
    ucval = symbols('uC:%d' % (nOrder - 1))
    urval = symbols('uR:%d' % (nOrder - 1))
    ulval = symbols('uL:%d' % (nOrder - 1))

    # Set interpolation polynomials
    fpoly = sum(coeff[k] * xi ** k for k in range(nOrder+1))
    dfpoly = []
    for k in range(np.int((nOrder + 1)/2)):
        dfpoly.append(fpoly.diff(xi, k))

    # Set constrains
    eqs_left = []
    eqs_right = []
    for k in range(np.int((nOrder + 1)/2)):
        eqs_right.append(dfpoly[k].subs(xi, 0))
        eqs_right.append(dfpoly[k].subs(xi,-h))
        eqs_left.append(dfpoly[k].subs(xi, 0))
        eqs_left.append(dfpoly[k].subs(xi, h))

    # Solve system under constrains
    # Right wave
    vals_right = ucval + ulval
    sys_eqns_right = [eqs_right[k] - vals_right[k] for k in range((nOrder+1))]
    sol_right = linsolve(sys_eqns_right, coeff)
    sol_right_list = list(sol_right)
    print('Right wave:\n', sol_right_list[0])
    # Left wave
    vals_left = ucval + urval
    sys_eqns_left = [eqs_left[k] - vals_left[k] for k in range((nOrder+1))]
    sol_left = linsolve(sys_eqns_left, coeff)
    sol_left_list = list(sol_left)
    print('Left wave:\n', sol_left_list[0])
    # print('LaTeX form:\n', latex(sol_left_list[0]))
    # print('Mathematica form:\n', mathematica_code(sol_left_list[0]))
    # print('C form:\n', ccode(sol_left_list[0]))
    # print('Octave form:\n', octave_code(sol_left_list[0]))

def symboldeltaP3():
    x, a, h, t, nu, c1, c2, c3, c4, ui, ui1, ri, ri1, uxx, u3x, rx, rxx, _uni \
        = symbols('x, a, h, t, nu, c1, c2, c3, c4, ui, ui1, ri, ri1, uxx, u3x, rx, rxx, _uni')
    # Set interpolants
    u = c1 + c2 * x + c3 * x ** 2 + c4 * x ** 3
    du = u.diff(x)
    print(du)
    # Set constrains
    eq01 = u.subs(x, 0)
    eq02 = du.subs(x, 0)
    eq03 = u.subs(x, -h)
    eq04 = du.subs(x, -h)
    # Solve system under constrains
    sol = linsolve([eq01 - ui, eq02 - ri, eq03 - ui1, eq04 - ri1], (c1, c2, c3, c4))
    # Get coefficients
    sol_get = next(iter(sol))
    c1_expr = sol_get[0].subs(ri, ri / h)
    c2_expr = sol_get[1].subs(ri, ri / h)
    c3_expr = sol_get[2].subs(ri, ri / h).subs(ri1, ri1 / h)
    c4_expr = sol_get[3].subs(ri, ri / h).subs(ri1, ri1 / h)
    print(("c1 = {0}".format(c1_expr)))
    print(("c2 = {0}".format(c2_expr)))
    print(("c3 = {0}".format(c3_expr)))
    print(("c4 = {0}".format(c4_expr)))

    # print("u = {0}".format(u))
    # nu = a * t / h
    # Set constrains
    ui = u.subs(x, 0)
    ui1 = u.subs(x, -h)

    ux = u.diff(x)
    uxi = ux.subs(x, 0)

    uxx = ux.diff(x)
    uxxi = uxx.subs(x, 0)

    u3x = uxx.diff(x)
    u3xi = u3x.subs(x, 0)

    r = h * u.diff(x)
    ri = r.subs(x, 0)

    rx = r.diff(x)
    rxi = rx.subs(x, 0)

    rxx = rx.diff(x)
    rxxi = rxx.subs(x, 0)

    # uxx = ux.diff(x)
    # u3x = uxx.diff(x)

    # print("ui = {0}, ui1 = {1}, ri = {2}, ri1 = {3}".format(simplify(ui), simplify(ui1), ri, ri1))
    #
    # c1 = ui
    # c2 = ri / h
    #
    # c3 = (3 * (ui1 - ui) + 2 * ri + ri1) / h**2
    # c4 = (2 * (ui1 - ui) + ri + ri1) / h**3

    # uxx = 2 * c3
    # u3x = 6 * c4
    #
    # rx = h * 2 * c3
    # rxx = h * 6 * c4

    uni = ui - a * t * uxi + a**2 * t**2 / 2 * uxxi - a**3 * t**3 / 6 * u3xi
    # uni = simplify(uni)
    _uni = uni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(a * t / h, nu)
    _uni = collect(expand(_uni), nu)
    print(_uni)

    rni = simplify(ri - a * t * rxi + a**2 * t**2 / 2 * rxxi)
    _rni = rni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(a * t / h, nu)
    print(collect(expand(_rni), nu))

def symbolDeltaP():
    # # Simple check
    # if nOrder % 2 == 0:
    #     sys.exit("Order might be odd more than 3,\n"
    #              "for issue 3, 5, 7, 9, etc.")
    nOrder = 3
    # Set values
    coeff = symbols('с1:%d' % (nOrder + 2))
    print(coeff)
    x, a, h, t, nu, c1, c2, c3, c4, ui, ui1, ri, ri1, uxx, u3x, rx, rxx, _uni \
        = symbols('x, a, h, t, nu, c1, c2, c3, c4, ui, ui1, ri, ri1, uxx, u3x, rx, rxx, _uni')
    # Set interpolants
    u = c1 + c2 * x + c3 * x ** 2 + c4 * x ** 3
    print(u)
    u = sum(coeff[k] * x ** k for k in range(nOrder + 1))
    du = u.diff(x)
    print(u)
    # Set constrains
    eq01 = u.subs(x, 0)
    eq02 = du.subs(x, 0)
    eq03 = u.subs(x, -h)
    eq04 = du.subs(x, -h)
    # Solve system under constrains
    sol = linsolve([eq01 - ui, eq02 - ri, eq03 - ui1, eq04 - ri1], (c1, c2, c3, c4))
    # Get coefficients
    sol_get = next(iter(sol))
    c1_expr = sol_get[0].subs(ri, ri / h)
    c2_expr = sol_get[1].subs(ri, ri / h)
    c3_expr = sol_get[2].subs(ri, ri / h).subs(ri1, ri1 / h)
    c4_expr = sol_get[3].subs(ri, ri / h).subs(ri1, ri1 / h)
    print(("c1 = {0}".format(c1_expr)))
    print(("c2 = {0}".format(c2_expr)))
    print(("c3 = {0}".format(c3_expr)))
    print(("c4 = {0}".format(c4_expr)))

    # print("u = {0}".format(u))
    # nu = a * t / h
    # Set constrains
    ui = u.subs(x, 0)
    ui1 = u.subs(x, -h)

    ux = u.diff(x)
    uxi = ux.subs(x, 0)

    uxx = ux.diff(x)
    uxxi = uxx.subs(x, 0)

    u3x = uxx.diff(x)
    u3xi = u3x.subs(x, 0)

    r = h * u.diff(x)
    ri = r.subs(x, 0)

    rx = r.diff(x)
    rxi = rx.subs(x, 0)

    rxx = rx.diff(x)
    rxxi = rxx.subs(x, 0)

    # uxx = ux.diff(x)
    # u3x = uxx.diff(x)

    # print("ui = {0}, ui1 = {1}, ri = {2}, ri1 = {3}".format(simplify(ui), simplify(ui1), ri, ri1))
    #
    # c1 = ui
    # c2 = ri / h
    #
    # c3 = (3 * (ui1 - ui) + 2 * ri + ri1) / h**2
    # c4 = (2 * (ui1 - ui) + ri + ri1) / h**3

    # uxx = 2 * c3
    # u3x = 6 * c4
    #
    # rx = h * 2 * c3
    # rxx = h * 6 * c4

    uni = ui - a * t * uxi + a**2 * t**2 / 2 * uxxi - a**3 * t**3 / 6 * u3xi
    # uni = simplify(uni)
    _uni = uni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(a * t / h, nu)
    _uni = collect(expand(_uni), nu)
    print(_uni)

    rni = simplify(ri - a * t * rxi + a**2 * t**2 / 2 * rxxi)
    _rni = rni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(a * t / h, nu)
    print(collect(expand(_rni), nu))

# def symboldeltaP5():
#     x, a, h, tau, nu, ui, ui1, ri, ri1, u3x, u4x, u5x, rxi, rxi1, rxx, r3x, r4x \
#         = symbols('x, a, h, tau, nu, ui, ui1, ri, ri1, u3x, u4x, u5x, rxi, rxi1, rxx, r3x, r4x')
#
#     c1, c2, c3, c4, c5, c6 = symbols('c1, c2, c3, c4, c5, c6')
#
#     _uxi, _uxxi, _ux1i, _uxx1i = symbols('_uxi, _uxxi, _ux1i, _uxx1i')
#
#     u = c1 + c2 * x + c3 * x ** 2 + c4 * x ** 3 + c5 * x ** 4 + c6 * x ** 5
#
#     du = u.diff(x)
#     ddu = du.diff(x)
#     eq01 = u.subs(x, 0)
#     eq02 = du.subs(x, 0)
#     eq03 = u.subs(x, -h)
#     eq04 = du.subs(x, -h)
#     eq05 = ddu.subs(x, 0)
#     eq06 = ddu.subs(x, -h)
#     # Solve system under constrains
#     sol = linsolve([eq01 - ui, eq02 - _uxi, eq03 - ui1, eq04 - _ux1i, eq05 - _uxxi, eq06 - _uxx1i],
#                    (c1, c2, c3, c4, c5, c6))
#
#     sol_get = next(iter(sol))
#     # c1_expr = sol_get[0].subs(ri, ri / h).subs(ri1, ri1 / h).subs(rxi, rxi / h**2).subs(rxi1, rxi1 / h**2)
#     # c2_expr = sol_get[1].subs(ri, ri / h).subs(ri1, ri1 / h).subs(rxi, rxi / h**2).subs(rxi1, rxi1 / h**2)
#     # c3_expr = sol_get[2].subs(ri, ri / h).subs(ri1, ri1 / h).subs(rxi, rxi / h**2).subs(rxi1, rxi1 / h**2)
#     # c4_expr = sol_get[3].subs(ri, ri / h).subs(ri1, ri1 / h).subs(rxi, rxi / h**2).subs(rxi1, rxi1 / h**2)
#     # c5_expr = sol_get[4].subs(ri, ri / h).subs(ri1, ri1 / h).subs(rxi, rxi / h**2).subs(rxi1, rxi1 / h**2)
#     # c6_expr = sol_get[5].subs(ri, ri / h).subs(ri1, ri1 / h).subs(rxi, rxi / h**2).subs(rxi1, rxi1 / h**2)
#     c1_expr = sol_get[0]
#     c2_expr = sol_get[1].subs(_uxi, ri / h)
#     c3_expr = sol_get[2].subs(_uxxi, rxi / h ** 2)
#     c4_expr = sol_get[3].subs(h * _uxi, ri).subs(h * _ux1i, ri1).subs(h ** 2 * _uxxi, rxi).subs(h ** 2 * _uxx1i, rxi1)
#     c5_expr = sol_get[4].subs(h * _uxi, ri).subs(h * _ux1i, ri1).subs(h ** 2 * _uxxi, rxi).subs(h ** 2 * _uxx1i, rxi1)
#     c6_expr = sol_get[5].subs(h * _uxi, ri).subs(h * _ux1i, ri1).subs(h ** 2 * _uxxi, rxi).subs(h ** 2 * _uxx1i, rxi1)
#     print("c1 = {0}".format(c1_expr))
#     print("c2 = {0}".format(c2_expr))
#     print("c3 = {0}".format(c3_expr))
#     print("c4 = {0}".format(c4_expr))
#     print("c5 = {0}".format(c5_expr))
#     print("c6 = {0}".format(c6_expr))
#
#     # print("u = {0}".format(u))
#     # Set constrains
#     ui = u.subs(x, 0)
#
#     ux = u.diff(x)
#     uxi = ux.subs(x, 0)
#
#     uxx = ux.diff(x)
#     uxxi = uxx.subs(x, 0)
#
#     u3x = uxx.diff(x)
#     u3xi = u3x.subs(x, 0)
#
#     u4x = u3x.diff(x)
#     u4xi = u4x.subs(x, 0)
#
#     u5x = u4x.diff(x)
#     u5xi = u5x.subs(x, 0)
#
#     r = h * ux
#     ri = r.subs(x, 0)
#
#     rx = r.diff(x)
#     rxi = rx.subs(x, 0)
#
#     rxx = rx.diff(x)
#     rxxi = rxx.subs(x, 0)
#
#     r3x = rxx.diff(x)
#     r3xi = r3x.subs(x, 0)
#
#     r4x = r3x.diff(x)
#     r4xi = r4x.subs(x, 0)
#
#     # uxx = ux.diff(x)
#     # u3x = uxx.diff(x)
#
#     # print("ui = {0}, ui1 = {1}, ri = {2}, ri1 = {3}".format(simplify(ui), simplify(ui1), ri, ri1))
#     #
#     # c1 = ui
#     # c2 = ri / h
#     #
#     # c3 = (3 * (ui1 - ui) + 2 * ri + ri1) / h**2
#     # c4 = (2 * (ui1 - ui) + ri + ri1) / h**3
#
#     # uxx = 2 * c3
#     # u3x = 6 * c4
#     #
#     # rx = h * 2 * c3
#     # rxx = h * 6 * c4
#
#     uni = (ui - a * tau * uxi + a**2 * tau**2 / 2 * uxxi - a**3 * tau**3 / 6
#            * u3xi + a**4 * tau**4 / 24 * u4xi - a ** 5 * tau ** 5 / 120 * u5xi)
#
#     _uni = uni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr)\
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(a * tau / h, nu)
#
#     rni = ri - a * tau * rxi + a**2 * tau**2 / 2 * rxxi - a**3 * tau**3 / 6 * r3xi + a**4 * tau**4 / 24 * r4xi
#
#     _rni = rni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr)\
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(a * tau / h, nu)
#
#     rxni = rxi - a * tau * rxxi + a**2 * tau**2 / 2 * r3xi - a**3 * tau**3 / 6 * r4xi
#
#     _rxni = rxni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr)\
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(a * tau / h, nu) * h
#
#     print(simplify(_uni))
#     print(simplify(_rni))
#     print(simplify(_rxni))

# def symboldeltaP7():
#     x, a, h, tau, nu, c1, c2, c3, c4, c5, c6, c7, c8 = symbols('x, a, h, tau, nu, c1, c2, c3, c4, c5, c6, c7, c8')
#
#     ui, ui1, uxi, uxi1, uxxi, uxxi1, u3xi, u3xi1 = symbols('ui, ui1, ri, ri1, uxxi, uxx1i, u3xi, u3x1i')
#
#     ri, ri1, rxi, rxi1, rxxi, rxxi1 = symbols('ri, ri1, rxi, rxi1, rxxi, rxxi1')
#
#     u = c1 + c2 * x + c3 * x ** 2 + c4 * x ** 3 + c5 * x ** 4 + c6 * x ** 5 + c7 * x ** 6 + c8 * x ** 7
#
#     du = u.diff(x)
#     ddu = du.diff(x)
#     dddu = ddu.diff(x)
#     eq01 = u.subs(x, 0)
#     eq02 = du.subs(x, 0)
#     eq03 = u.subs(x, -h)
#     eq04 = du.subs(x, -h)
#     eq05 = ddu.subs(x, 0)
#     eq06 = ddu.subs(x, -h)
#     eq07 = dddu.subs(x, 0)
#     eq08 = dddu.subs(x, -h)
#     # Solve system under constrains
#     sol = linsolve([eq01 - ui, eq02 - uxi, eq03 - ui1, eq04 - uxi1, eq05 - uxxi, eq06 - uxxi1, eq07 - u3xi, eq08 - u3xi1],
#                    (c1, c2, c3, c4, c5, c6, c7, c8))
#     sol_get = next(iter(sol))
#     c1_expr = sol_get[0]
#     c2_expr = sol_get[1].subs(uxi, ri / h)
#     c3_expr = sol_get[2].subs(uxxi, rxi / h ** 2)
#     c4_expr = sol_get[3].subs(u3xi, rxxi / h ** 3)
#     c5_expr = sol_get[4].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi)\
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1)
#     c6_expr = sol_get[5].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi)\
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1)
#     c7_expr = sol_get[6].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi)\
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1)
#     c8_expr = sol_get[7].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi)\
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1)
#
#     print("c1 = {0}".format(c1_expr))
#     print("c2 = {0}".format(c2_expr))
#     print("c3 = {0}".format(c3_expr))
#     print("c4 = {0}".format(c4_expr))
#     print("c5 = {0}".format(c5_expr))
#     print("c6 = {0}".format(c6_expr))
#     print("c7 = {0}".format(c7_expr))
#     print("c8 = {0}".format(c8_expr))
#
#
#     # Set constrains
#     ui = u.subs(x, 0)
#
#     ux = u.diff(x)
#     uxi = ux.subs(x, 0)
#
#     uxx = ux.diff(x)
#     uxxi = uxx.subs(x, 0)
#
#     u3x = uxx.diff(x)
#     u3xi = u3x.subs(x, 0)
#
#     u4x = u3x.diff(x)
#     u4xi = u4x.subs(x, 0)
#
#     u5x = u4x.diff(x)
#     u5xi = u5x.subs(x, 0)
#
#     u6x = u5x.diff(x)
#     u6xi = u6x.subs(x, 0)
#
#     u7x = u6x.diff(x)
#     u7xi = u7x.subs(x, 0)
#
#     r = h * u.diff(x)
#     ri = r.subs(x, 0)
#
#     rx = r.diff(x)
#     rxi = rx.subs(x, 0)
#
#     rxx = rx.diff(x)
#     rxxi = rxx.subs(x, 0)
#
#     r3x = rxx.diff(x)
#     r3xi = r3x.subs(x, 0)
#
#     r4x = r3x.diff(x)
#     r4xi = r4x.subs(x, 0)
#
#     r5x = r4x.diff(x)
#     r5xi = r5x.subs(x, 0)
#
#     r6x = r5x.diff(x)
#     r6xi = r6x.subs(x, 0)
#
#     # uxx = ux.diff(x)
#     # u3x = uxx.diff(x)
#
#     # print("ui = {0}, ui1 = {1}, ri = {2}, ri1 = {3}".format(simplify(ui), simplify(ui1), ri, ri1))
#     #
#     # c1 = ui
#     # c2 = ri / h
#     #
#     # c3 = (3 * (ui1 - ui) + 2 * ri + ri1) / h**2
#     # c4 = (2 * (ui1 - ui) + ri + ri1) / h**3
#
#     # uxx = 2 * c3
#     # u3x = 6 * c4
#     #
#     # rx = h * 2 * c3
#     # rxx = h * 6 * c4
#
#     uni = ui - a * tau * uxi + a**2 * tau**2 / 2 * uxxi - a**3 * tau**3 / 6 * u3xi + a**4 * tau**4 / 24 * u4xi \
#           - a ** 5 * tau ** 5 / 120 * u5xi + a ** 6 * tau ** 6 / 720 * u6xi - a ** 7 * tau ** 7 / 5040 * u7xi
#
#     rni = ri - a * tau * rxi + a**2 * tau**2 / 2 * rxxi - a**3 * tau**3 / 6 * r3xi + a**4 * tau**4 / 24 * r4xi \
#           - a ** 5 * tau ** 5 / 120 * r5xi + a ** 6 * tau ** 6 / 720 * r6xi
#
#     rxni = rxi - a * tau * rxxi + a**2 * tau**2 / 2 * r3xi - a**3 * tau**3 / 6 * r4xi \
#            + a**4 * tau**4 / 24 * r5xi - a ** 5 * tau ** 5 / 120 * r6xi
#
#     rxxni = rxxi - a * tau * r3xi + a**2 * tau**2 / 2 * r4xi - a**3 * tau**3 / 6 * r5xi \
#             + a**4 * tau**4 / 24 * r6xi
#
#     uni = uni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr)\
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(a * tau / h, nu)
#
#     rni = rni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(a * tau / h, nu)
#
#     rxni = rxni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr)\
#         .subs(a * tau / h, nu) * h
#
#     rxxni = rxxni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr)\
#         .subs(a * tau / h, nu) * h ** 2
#
#     print("uni = {0}".format(simplify(uni)))
#     print("rni = {0}".format(simplify(rni)))
#     print("rxni = {0}".format(simplify(rxni)))
#     print("rxxni = {0}".format(simplify(rxxni)))
#
#     # print("rx = {0}, с2 = {1}, с3 = {2}, с4 = {3}, с5 = {4}".format(simplify(rx), simplify(c2), c3, c4, c5))

# def symboldeltaP9():
#     x, a, h, tau, nu, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10 \
#         = symbols('x, a, h, tau, nu, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10')
#
#     ui, ui1, uxi, uxi1, uxxi, uxxi1, u3xi, u3xi1, u4xi, u4xi1 \
#         = symbols('ui, ui1, ri, ri1, uxxi, uxx1i, u3xi, u3x1i, u4xi, u4xi1')
#
#     ri, ri1, rxi, rxi1, rxxi, rxxi1, r3xi, r3xi1 = symbols('ri, ri1, rxi, rxi1, rxxi, rxxi1, r3xi, r3xi1')
#
#     u = c1 + c2 * x + c3 * x ** 2 + c4 * x ** 3 + c5 * x ** 4 + c6 * x ** 5 + c7 * x ** 6 + c8 * x ** 7 \
#         + c9 * x ** 8 + c10 * x ** 9
#
#     du = u.diff(x)
#     ddu = du.diff(x)
#     d3u = ddu.diff(x)
#     d4u = d3u.diff(x)
#
#     eq01 = u.subs(x, 0)
#     eq02 = du.subs(x, 0)
#     eq03 = u.subs(x, -h)
#     eq04 = du.subs(x, -h)
#     eq05 = ddu.subs(x, 0)
#     eq06 = ddu.subs(x, -h)
#     eq07 = d3u.subs(x, 0)
#     eq08 = d3u.subs(x, -h)
#     eq09 = d4u.subs(x, 0)
#     eq10 = d4u.subs(x, -h)
#
#     # Solve system under constrains
#
#     sol = linsolve([eq01 - ui, eq02 - uxi, eq03 - ui1, eq04 - uxi1, eq05 - uxxi, eq06 - uxxi1, eq07 - u3xi,
#                     eq08 - u3xi1, eq09 - u4xi, eq10 - u4xi1], (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10))
#
#     sol_get = next(iter(sol))
#
#     c1_expr = sol_get[0]
#     c2_expr = sol_get[1].subs(uxi, ri / h)
#     c3_expr = sol_get[2].subs(uxxi, rxi / h ** 2)
#     c4_expr = sol_get[3].subs(u3xi, rxxi / h ** 3)
#     c5_expr = sol_get[4].subs(u4xi, r3xi / h ** 4)
#     c6_expr = sol_get[5].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi) \
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1).subs(h ** 4 * u4xi, r3xi)\
#         .subs(h ** 4 * u4xi1, r3xi1)
#     c7_expr = sol_get[6].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi) \
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1).subs(h ** 4 * u4xi, r3xi)\
#         .subs(h ** 4 * u4xi1, r3xi1)
#     c8_expr = sol_get[7].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi) \
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1).subs(h ** 4 * u4xi, r3xi)\
#         .subs(h ** 4 * u4xi1, r3xi1)
#     c9_expr = sol_get[8].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi) \
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1).subs(h ** 4 * u4xi, r3xi)\
#         .subs(h ** 4 * u4xi1, r3xi1)
#     c10_expr = sol_get[9].subs(h * uxi, ri).subs(h * uxi1, ri1).subs(h ** 2 * uxxi, rxi) \
#         .subs(h ** 2 * uxxi1, rxi1).subs(h ** 3 * u3xi, rxxi).subs(h ** 3 * u3xi1, rxxi1).subs(h ** 4 * u4xi, r3xi)\
#         .subs(h ** 4 * u4xi1, r3xi1)
#     print("c1 = {0}".format(c1_expr))
#     print("c2 = {0}".format(c2_expr))
#     print("c3 = {0}".format(c3_expr))
#     print("c4 = {0}".format(c4_expr))
#     print("c5 = {0}".format(c5_expr))
#     print("c6 = {0}".format(c6_expr))
#     print("c7 = {0}".format(c7_expr))
#     print("c8 = {0}".format(c8_expr))
#     print("c9 = {0}".format(c9_expr))
#     print("c10 = {0}\n".format(c10_expr))
#
#     # Set constrains
#     ui = u.subs(x, 0)
#
#     ux = u.diff(x)
#     uxi = ux.subs(x, 0)
#
#     uxx = ux.diff(x)
#     uxxi = uxx.subs(x, 0)
#
#     u3x = uxx.diff(x)
#     u3xi = u3x.subs(x, 0)
#
#     u4x = u3x.diff(x)
#     u4xi = u4x.subs(x, 0)
#
#     u5x = u4x.diff(x)
#     u5xi = u5x.subs(x, 0)
#
#     u6x = u5x.diff(x)
#     u6xi = u6x.subs(x, 0)
#
#     u7x = u6x.diff(x)
#     u7xi = u7x.subs(x, 0)
#
#     u8x = u7x.diff(x)
#     u8xi = u8x.subs(x, 0)
#
#     u9x = u8x.diff(x)
#     u9xi = u9x.subs(x, 0)
#
#     r = h * u.diff(x)
#     ri = r.subs(x, 0)
#
#     rx = r.diff(x)
#     rxi = rx.subs(x, 0)
#
#     rxx = rx.diff(x)
#     rxxi = rxx.subs(x, 0)
#
#     r3x = rxx.diff(x)
#     r3xi = r3x.subs(x, 0)
#
#     r4x = r3x.diff(x)
#     r4xi = r4x.subs(x, 0)
#
#     r5x = r4x.diff(x)
#     r5xi = r5x.subs(x, 0)
#
#     r6x = r5x.diff(x)
#     r6xi = r6x.subs(x, 0)
#
#     r7x = r6x.diff(x)
#     r7xi = r7x.subs(x, 0)
#
#     r8x = r7x.diff(x)
#     r8xi = r8x.subs(x, 0)
#
#     uni = ui - a * tau * uxi + a ** 2 * tau ** 2 / 2 * uxxi - a ** 3 * tau ** 3 / 6 * u3xi + a ** 4 * tau ** 4 / 24 * u4xi \
#           - a ** 5 * tau ** 5 / 120 * u5xi + a ** 6 * tau ** 6 / 720 * u6xi - a ** 7 * tau ** 7 / 5040 * u7xi \
#           + a ** 8 * tau ** 8 / 40320 * u8xi - a ** 9 * tau ** 9 / 362880 * u9xi
#
#     rni = ri - a * tau * rxi + a ** 2 * tau ** 2 / 2 * rxxi - a ** 3 * tau ** 3 / 6 * r3xi + a ** 4 * tau ** 4 / 24 * r4xi \
#           - a ** 5 * tau ** 5 / 120 * r5xi + a ** 6 * tau ** 6 / 720 * r6xi - a ** 7 * tau ** 7 / 5040 * r7xi \
#           + a ** 8 * tau ** 8 / 40320 * r8xi
#
#     rxni = rxi - a * tau * rxxi + a ** 2 * tau ** 2 / 2 * r3xi - a ** 3 * tau ** 3 / 6 * r4xi \
#            + a ** 4 * tau ** 4 / 24 * r5xi - a ** 5 * tau ** 5 / 120 * r6xi + a ** 6 * tau ** 6 / 720 * r7xi \
#            - a ** 7 * tau ** 7 / 5040 * r8xi
#
#     rxxni = rxxi - a * tau * r3xi + a ** 2 * tau ** 2 / 2 * r4xi - a ** 3 * tau ** 3 / 6 * r5xi \
#             + a ** 4 * tau ** 4 / 24 * r6xi - a ** 5 * tau ** 5 / 120 * r7xi + a ** 6 * tau ** 6 / 720 * r8xi
#
#     r3xni = r3xi - a * tau * r4xi + a ** 2 * tau ** 2 / 2 * r5xi - a ** 3 * tau ** 3 / 6 * r6xi \
#             + a ** 4 * tau ** 4 / 24 * r7xi - a ** 5 * tau ** 5 / 120 * r8xi
#
#     uni = uni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr)\
#         .subs(c10, c10_expr).subs(a * tau / h, nu)
#
#     rni = rni.subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr)\
#         .subs(c10, c10_expr).subs(a * tau / h, nu)
#
#     rxni = rxni.subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr)\
#         .subs(c10, c10_expr).subs(a * tau / h, nu) * h
#
#     rxxni = rxxni.subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr)\
#         .subs(c10, c10_expr).subs(a * tau / h, nu) * h ** 2
#
#     r3xni = r3xni.subs(c2, c2_expr).subs(c3, c3_expr) \
#         .subs(c4, c4_expr).subs(c5, c5_expr).subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr)\
#         .subs(c10, c10_expr).subs(a * tau / h, nu) * h ** 3
#
#     print("uni = {0}".format(simplify(uni)))
#     print("rni = {0}".format(simplify(rni)))
#     print("rxni = {0}".format(simplify(rxni)))
#     print("rxxni = {0}".format(simplify(rxxni)))
#     print("r3xni = {0}".format(simplify(r3xni)))

# def get_derivative():
#     t, x, y = symbols('t, x, y')
#
#     p = cos(2 * sqrt(2) * pi * t) * sin(2 * pi * x) * sin(2 * pi * y)
#
#     u = -1 / sqrt(2) * sin(2 * sqrt(2) * pi * t) * cos(2 * pi * x) * sin(2 * pi * y)
#
#     v = -1 / sqrt(2) * sin(2 * sqrt(2) * pi * t) * sin(2 * pi * x) * cos(2 * pi * y)
#
#     p_exp = exp(-(10 * (x ** 2 + (y + 0.7) ** 2)))
#
#     p_sin = 1 / pi * sin(pi * x) ** 2 * sin(pi * y) ** 2
#
#     rp = diff(p, x)
#     ru = diff(u, x)
#     rv = diff(v, x)
#
#     sp = diff(p, y)
#     su = diff(u, y)
#     sv = diff(v, y)
#
#     rp_exp = diff(p_exp, x)
#     sp_exp = diff(p_exp, y)
#
#     rp_sin = diff(p_sin, x)
#     sp_sin = diff(p_sin, y)
#
#     print("rp = {0}\n".format(rp))
#     print("ru = {0}\n".format(ru))
#     print("rv = {0}\n".format(rv))
#
#     print("sp = {0}\n".format(sp))
#     print("su = {0}\n".format(su))
#     print("sv = {0}\n".format(sv))
#
#     print("rp_exp = {0}\n".format(rp_exp))
#     print("sp_exp = {0}\n".format(sp_exp))
#
#     print("rp_sin = {0}\n".format(rp_sin))
#     print("sp_sin = {0}\n".format(sp_sin))

# def two_dim_deltaP3():
#     x, y, a, h, tau, nu, ui, ui1, ri, ri1, uxx, u3x, rx, rxx \
#         = symbols('x, y, a, h, tau, nu, ui, ui1, ri, ri1, uxx, u3x, rx, rxx')
#
#     c1, c2, c3, c4, c5, c6, c7, c8, c9, c10 = \
#         symbols('c1, c2, c3, c4, c5, c6, c7, c8, c9, c10')
#
#     phi_i_j1, phi_i1_j1, phi_i1_j, phi_i_j, r_i_j1, r_i_j, r_i1_j, s_i_j1, s_i_j, s_i1_j = \
#         symbols('phi_i_j1, phi_i1_j1, phi_i1_j, phi_i_j, r_i_j1, r_i_j, r_i1_j, s_i_j1, s_i_j, s_i1_j')
#
#     # p, u, v, rp, ru, rv, sp, su, sv = symbols('p, u, v, rp, ru, rv, sp, su, sv')
#
#     p = c1 + c2 * x + c3 * y + c4 * x * y + c5 * x ** 2 + c6 * y ** 2 + c7 * x ** 2 * y + c8 * x * y ** 2 \
#         + c9 * x ** 3 + c10 * y ** 3
#
#     u = c1 + c2 * x + c3 * y + c4 * x * y + c5 * x ** 2 + c6 * y ** 2 + c7 * x ** 2 * y + c8 * x * y ** 2 \
#         + c9 * x ** 3 + c10 * y ** 3
#
#     v = c1 + c2 * x + c3 * y + c4 * x * y + c5 * x ** 2 + c6 * y ** 2 + c7 * x ** 2 * y + c8 * x * y ** 2 \
#         + c9 * x ** 3 + c10 * y ** 3
#
#     # linear system of 10 equations
#
#     # eq01 = p.subs(x, 0).subs(y, h)
#     # eq02 = p.subs(x, h).subs(y, h)
#     # eq03 = p.subs(x, h).subs(y, 0)
#     # eq04 = p.subs(x, 0).subs(y, 0)
#     #
#     # eq05 = p.diff(x).subs(x, 0).subs(y, h)
#     # eq06 = p.diff(x).subs(x, h).subs(y, h)
#     # eq07 = p.diff(x).subs(x, h).subs(y, 0)
#     #
#     # eq08 = p.diff(y).subs(x, 0).subs(y, h)
#     # eq09 = p.diff(y).subs(x, h).subs(y, h)
#     # eq10 = p.diff(y).subs(x, h).subs(y, 0)
#
#     eq01 = p.subs(x, -h).subs(y, 0)
#     eq02 = p.subs(x, 0).subs(y, 0)
#     eq03 = p.subs(x, 0).subs(y, -h)
#     eq04 = p.subs(x, -h).subs(y, -h)
#
#     eq05 = p.diff(x).subs(x, -h).subs(y, 0)
#     eq06 = p.diff(x).subs(x, 0).subs(y, 0)
#     eq07 = p.diff(x).subs(x, 0).subs(y, -h)
#
#     eq08 = p.diff(y).subs(x, -h).subs(y, 0)
#     eq09 = p.diff(y).subs(x, 0).subs(y, 0)
#     eq10 = p.diff(y).subs(x, 0).subs(y, -h)
#
#     sol = linsolve([eq01 - phi_i1_j, eq02 - phi_i_j, eq03 - phi_i_j1, eq04 - phi_i1_j1, eq05 - r_i1_j, eq06 - r_i_j,
#                     eq07 - r_i_j1, eq08 - s_i1_j, eq09 - s_i_j, eq10 - s_i_j1],
#                    (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10))
#
#     sol_get = next(iter(sol))
#
#     c1_expr = sol_get[0] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#        # .subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c2_expr = sol_get[1] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#        # .subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c3_expr = sol_get[2] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#        # .subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c4_expr = sol_get[3] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#        # .subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c5_expr = sol_get[4] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#        # .subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c6_expr = sol_get[5] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#         #.subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c7_expr = sol_get[6] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#        # .subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c8_expr = sol_get[7] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#         #.subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c9_expr = sol_get[8] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#         #.subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     c10_expr = sol_get[9] #.subs(r_i_j1, r_i_j1 / h).subs(r_i1_j1, r_i1_j1 / h).subs(r_i1_j, r_i1_j / h)\
#         #.subs(s_i_j1, s_i_j1 / h).subs(s_i1_j1, s_i1_j1 / h).subs(s_i1_j, s_i1_j / h)
#
#     print("c1 = {0}".format(c1_expr))
#     print("c2 = {0}".format(c2_expr))
#     print("c3 = {0}".format(c3_expr))
#     print("c4 = {0}".format(c4_expr))
#     print("c5 = {0}".format(c5_expr))
#     print("c6 = {0}".format(c6_expr))
#     print("c7 = {0}".format(c7_expr))
#     print("c8 = {0}".format(c8_expr))
#     print("c9 = {0}".format(c9_expr))
#     print("c10 = {0}\n".format(c10_expr))
#
#     # first derivative in the x-direction
#
#     rp = p.diff(x)
#     ru = u.diff(x)
#     rv = v.diff(x)
#
#     # first derivative in the y-direction
#
#     sp = p.diff(y)
#     su = u.diff(y)
#     sv = v.diff(y)
#
#     # second and third derivative for p
#
#     p_xx = rp.diff(x)
#     p_xy = rp.diff(y)
#     p_yy = sp.diff(y)
#
#     p_3x = p_xx.diff(x)
#     p_xxy = p_xx.diff(y)
#     p_xyy = p_xy.diff(y)
#     p_3y = p_yy.diff(y)
#
#     # second and third derivative for u
#
#     u_xx = ru.diff(x)
#     u_xy = ru.diff(y)
#     u_yy = su.diff(y)
#
#     u_3x = u_xx.diff(x)
#     u_xxy = u_xx.diff(y)
#     u_xyy = u_xy.diff(y)
#     u_3y = u_yy.diff(y)
#
#     # second and third derivative for v
#
#     v_xx = rv.diff(x)
#     v_xy = rv.diff(y)
#     v_yy = sv.diff(y)
#
#     v_3x = v_xx.diff(x)
#     v_xxy = v_xx.diff(y)
#     v_xyy = v_xy.diff(y)
#     v_3y = v_yy.diff(y)
#
#     # parameters values at the discrete point i
#
#     pi = p.subs(x, 0).subs(y, 0)
#     ui = u.subs(x, 0).subs(y, 0)
#     vi = v.subs(x, 0).subs(y, 0)
#
#     rpi = rp.subs(x, 0).subs(y, 0)  # first derivatives in the x-direction
#     rui = ru.subs(x, 0).subs(y, 0)
#     rvi = rv.subs(x, 0).subs(y, 0)
#
#     spi = sp.subs(x, 0).subs(y, 0)  # first derivatives in the y-direction
#     sui = su.subs(x, 0).subs(y, 0)
#     svi = sv.subs(x, 0).subs(y, 0)
#
#     p_xxi = p_xx.subs(x, 0).subs(y, 0)  # second derivatives
#     print("p_xx = {0}\n".format(p_xxi.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr)))
#     p_xyi = p_xy.subs(x, 0).subs(y, 0)
#     print("p_xy = {0}\n".format(
#         p_xyi.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr) \
#         .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr)))
#     p_yyi = p_yy.subs(x, 0).subs(y, 0)
#
#     u_xxi = u_xx.subs(x, 0).subs(y, 0)
#     u_xyi = u_xy.subs(x, 0).subs(y, 0)
#     u_yyi = u_yy.subs(x, 0).subs(y, 0)
#
#     v_xxi = v_xx.subs(x, 0).subs(y, 0)
#     v_xyi = v_xy.subs(x, 0).subs(y, 0)
#     v_yyi = v_yy.subs(x, 0).subs(y, 0)
#
#     p_3xi = p_3x.subs(x, 0).subs(y, 0)  # third derivatives
#
#     p_xxyi = p_xxy.subs(x, 0).subs(y, 0)
#     p_xyyi = p_xyy.subs(x, 0).subs(y, 0)
#     p_3yi = p_3y.subs(x, 0).subs(y, 0)
#
#     u_3xi = u_3x.subs(x, 0).subs(y, 0)
#     u_xxyi = u_xxy.subs(x, 0).subs(y, 0)
#     u_xyyi = u_xyy.subs(x, 0).subs(y, 0)
#     u_3yi = u_3y.subs(x, 0).subs(y, 0)
#
#     v_3xi = v_3x.subs(x, 0).subs(y, 0)
#     v_xxyi = v_xxy.subs(x, 0).subs(y, 0)
#     v_xyyi = v_xyy.subs(x, 0).subs(y, 0)
#     v_3yi = v_3y.subs(x, 0).subs(y, 0)
#
#     pni = pi - tau * (rui + svi) + tau**2 / 2 * (p_xxi + p_yyi) - tau**3 / 6 * (u_3xi + u_xyyi + v_xxyi + v_3yi)
#
#     uni = ui - tau * rpi + tau**2 / 2 * (u_xxi + v_xyi) - tau**3 / 6 * (p_3xi + p_xyyi)
#
#     vni = vi - tau * spi + tau**2 / 2 * (u_xyi + v_yyi) - tau**3 / 6 * (p_xxyi + p_3yi)
#
#     # derivatives at the x-direction
#
#     rpni = rpi - tau * (u_xxi + v_xyi) + tau**2 / 2 * (p_3xi + p_xyyi)
#
#     runi = rui - tau * p_xxi + tau ** 2 / 2 * (u_3xi + v_xxyi)
#
#     rvni = rvi - tau * p_xyi + tau**2 / 2 * (u_xxyi + v_xyyi)
#
#     # derivatives at the y-direction
#
#     spni = spi - tau * (u_xyi + v_yyi) + tau**2 / 2 * (p_xxyi + p_3yi)
#
#     suni = sui - tau * p_xyi + tau**2 / 2 * (u_xxyi + v_xyyi)
#
#     svni = svi - tau * p_yyi + tau**2 / 2 * (u_xyyi + v_3yi)
#
#     _pni = pni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr)
#
#     _uni = uni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     _vni = vni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#         .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     _rpni = rpni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     _runi = runi.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     _rvni = rvni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#         .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     _spni = spni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     _suni = suni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     _svni = svni.subs(c1, c1_expr).subs(c2, c2_expr).subs(c3, c3_expr).subs(c4, c4_expr).subs(c5, c5_expr)\
#        .subs(c6, c6_expr).subs(c7, c7_expr).subs(c8, c8_expr).subs(c9, c9_expr).subs(c10, c10_expr).subs(tau / h, nu)
#
#     print(simplify(_pni))
#     # print(simplify(_uni))
#     # print(simplify(_vni))
#     print(simplify(_rpni))
#     # print(simplify(_runi))
#     # print(simplify(_rvni))
#     print(simplify(_spni))
#     # print(simplify(_suni))
#     # print(simplify(_svni))

# symbolCIP(3)
symbolDeltaP()


#!/usr/bin/env python

import numpy as np
from scipy.integrate import ode

#
# f[0] = d theta1/dt = omega1
# f[1] = d theata2/dt = omega2
# f[2] = d omega1/dt = -(g/l1)*theta1 - k*(theta1 - theta2)
# f[3] = d omega2/dt = -(g/l2)*theta2 - k*(theta2 - theta1)
#
# y[0] = theta1
# y[1] = theta2
# y[2] = omega1
# y[3] = omega2
#
def functions(t, y, l1, l2, k):
    g = 9.81
    return [
        y[2],
        y[3],
        -(g/l1)*y[0] - k*(y[0] - y[1]),
        -(g/l2)*y[1] - k*(y[1] - y[0])
    ]

def jacobian(t, y, l1, l2, k):
    g = 9.81
    return [
        [0.0,                  0.0,  1.0,   0.0],
        [0.0,                  0.0,  0.0,   1.0],
        [-(g/l1) - k,            k,  0.0,   0.0],
        [k,            -(g/l2) - k,  0.0,   0.0]
    ]

def init_integrator(theta0_1, theta0_2, t0=0.0, l1=1.0, l2=1.0, k=0.1):
    integrator = ode(functions, jacobian).set_integrator('dopri5',
                                                         atol=1.0e-6,
                                                         rtol=0.0)
    integrator.set_initial_value([theta0_1, theta0_2, 0.0, 0.0], t0)
    integrator.set_f_params(l1, l2, k)
    integrator.set_jac_params(l1, l2, k)
    return integrator

if __name__ == '__main__':
    from argparse import ArgumentParser
    arg_parser = ArgumentParser(description='solved coupled pendulums')
    arg_parser.add_argument('--theta0_1', type=float, default=0.0,
                            help='initial theta of first pendulum')
    arg_parser.add_argument('--theta0_2', type=float, default=0.0,
                            help='initial theta of second pendulum')
    arg_parser.add_argument('--l1', type=float, default=1.0,
                            help='length of first pendulum')
    arg_parser.add_argument('--l2', type=float, default=1.0,
                            help='length of second pendulum')
    arg_parser.add_argument('--k', type=float, default=0.5,
                            help="Hooke's constant")
    arg_parser.add_argument('--t_max', type=float, default=10*2*np.pi,
                            help='maximum time')
    arg_parser.add_argument('--delta_t', type=float, default=0.01,
                            help='delta t')
    options = arg_parser.parse_args()
    integrator = init_integrator(options.theta0_1, options.theta0_2, t0=0.0,
                                 l1=options.l1, l2=options.l2, k=options.k)
    while integrator.successful() and integrator.t < options.t_max:
        integrator.integrate(integrator.t + options.delta_t)
        print('{0:.3f}\t{1:.5f}\t{2:.5f}'.format(integrator.t,
                                                 integrator.y[0],
                                                 integrator.y[1]))


import numpy as np
import matplotlib.pyplot as plt

w = np.array([0.15, 0.6])
eta = 0.01

print(w, eta)

all_weights = []
energy = []

while (w[0]+w[1]) < 1 and w[0] > 0 and w[1] > 0 :
    f_w = - np.log(1 - w[0] - w[1]) - np.log(w[0]) - np.log(w[1])
    energy.append(f_w)

    all_weights.append(w)

    grad = np.array([(1 / (1-w[0]-w[1])) - (1 / w[0]), (1 / (1-w[0]-w[1])) - (1 / w[1])])

    delta_w = eta * grad
    new_w = w - delta_w

    if np.linalg.norm(w - new_w) < 0.001:
        break
    else:
        w = new_w

all_weights = np.array(all_weights)

plt.plot(all_weights[:, 0], all_weights[:, 1], '-o')
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Gradient Descent Weight Updates")
plt.show()

plt.plot(range(len(energy)), energy)
plt.xlabel("Number of Iterations")
plt.ylabel("Energy f(w)")
plt.title("Changes in Energy")
plt.show()

w = np.array([0.15, 0.6])
eta = 0.01

print(f"Initial Weight is: {w}\nLearning Rate is: {eta}")

all_weights = []
energy = []

H = np.empty((2, 2))

while (w[0]+w[1]) < 1 and w[0] > 0 and w[1] > 0 :
    f_w = - np.log(1 - w[0] - w[1]) - np.log(w[0]) - np.log(w[1])
    energy.append(f_w)

    all_weights.append(w)

    grad = np.array([(1 / (1-w[0]-w[1])) - (1 / w[0]), (1 / (1-w[0]-w[1])) - (1 / w[1])])

    H[0,:] = [(1 / (1-w[0]-w[1])**2) + (1 / w[0]**2),
              (1 / (1-w[0]-w[1])**2)]
    H[1,:] = [(1 / (1-w[0]-w[1])**2),
              (1 / (1-w[0]-w[1])**2) + (1 / w[1]**2)]

    delta_w = eta * np.matmul(np.linalg.inv(H), grad)
    new_w = w - delta_w

    if np.linalg.norm(w - new_w) < 0.001:
        break
    else:
        w = new_w

all_weights = np.array(all_weights)
plt.plot(all_weights[:, 0], all_weights[:, 1], '-o')
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Newton's Method Weight Updates")
plt.show()

plt.plot(range(len(energy)), energy)
plt.xlabel("Number of Iterations")
plt.ylabel("Energy f(w)")
plt.title("Changes in Energy for Newton's Method")
plt.show()


# -*- coding: utf-8 -*-
import numpy as np

def h(x, theta):
    return 1 / (1 + np.exp(-np.dot(x, theta)))

def cost(x, y, theta):
    _h = h(x, theta)
    if y == 1:
        return -np.log(_h)
    else:
        return -np.log(1 - _h)

def J(X, Y, theta):
    m, n = np.shape(X)
    loss = 0
    for k in range(m):
        loss += cost(X[k], Y[k], theta)
    return loss / m

def logistic_gd(X, Y, alpha=0.01, epsilon=1e-6, trace=True):
    m = len(X)
    _X = np.column_stack((np.ones(m), X))
    m, n = np.shape(_X)
    theta, j1, cnt = np.ones(n), 0, 0
    Xt = _X.T

    while True:
        loss = h(_X, theta) - Y
        gradient = np.dot(Xt, loss) / m
        theta -= alpha * gradient

        j = J(_X, Y, theta)

        if trace:
            print("[ Epoch {0} ] theta = {1}, loss = {2}, error = {3})".format(
                cnt, theta, loss, j))

        if abs(j - j1) < epsilon:
            break
        else:
            j1 = j

        cnt += 1
    return theta


import matplotlib.pyplot as plt
import numpy as np
import numpy.linalg as LA

def lin_lsq(x,y):
    """
    This function returns the coefficients of the linear regression AND the corresponding linear polyonomial
    of the given input data, x and y, in row or column vector form. It implements the linear version of the Method
    of Least Squares. It also displays a table where each row contains the x and y coordinates of the data, the
    linear function of the method of LSQ evaluated at that point and the absolute error of the best-fit linear
    function and the data. We also display the total squared error at the end.
    """
    n = np.prod(x.shape)
    x = x.reshape(n,1) #if given otherwise, we turn x and y vectors to column vectors
    y = y.reshape(n,1)
    s_x = np.sum(x); s_xx = np.sum(x**2); s_y = np.sum(y); s_xy = np.sum(x*y)
    S = np.array([[s_xx, s_x], [s_x, 4]], float)
    d = np.array([s_xy, s_y], float)
    try:
        S_inv = LA.inv(S) # if LA.det(S)=0 then a LinAlgError exception will be raised
    except LinAlgError:
        print("""With the given data, the system of normal equations of the Method of LSQ, does not have or
                 has infinite solutions because the coefficient matrix, S, is singular, i.e. it doesnt have an inverse.""")
        return None
    else:
        sol = LA.solve(S,d)
        a = sol[0]; b = sol[1]
        g = np.poly1d([a,b])
        g_x = np.polyval(g,x).reshape(n,1)
        err = y-g_x
        print('|    x    |    y    |   g(x)  |   y-g(x) | \n ----------------------------------------')
        table = np.concatenate((x, y, g_x, err), axis=1)
        for (x_i, y_i, g_xi, err_i) in table:
            print(f'|  {x_i:5.2f}  |  {y_i:5.2f}  |  {g_xi:5.2f}  |  {err_i:6.2f}  |')
        print(f"Also the total squared error is {sum(err**2)[0]:.2f} \n")
        return (a,b), g

x = np.array([1, 3, 5, 7], int)
y = np.array([2.5, 3.5, 6.35, 8.1], float)
(a,b), g = lin_lsq(x,y)
print(f"The coefficients of the linear polyonomial of the Method of LSQ are: a = {a:.2f} and b = {b:.2f}")
print(g)
t = np.linspace(0,10, num=1000)
g_t = g(t)
print(g_t)

# Creating the plot and editing some of its attributes for clarity. We will just copy and paste them for future use.
# We also assign every modification to our plot to a dummy/garbage collecting variable; '_' to prevent unwanted outputs

plt.figure(figsize=(10,5))
plt.scatter(x, y , marker='*', c='red', s=80, label='Our Data')
plt.plot(t, g_t, c='blue', linewidth='2.0', label=r'$g(x)=ax+b$')
plt.xlabel('x', fontsize=14)
plt.ylabel('y', fontsize=14)
plt.grid(True)
axes = plt.gca() #gca stands for get current axes
axes.set_xlim([-0.5,10])
axes.set_ylim([-0.5,10])
plt.rcParams['xtick.labelsize']=18
plt.rcParams['ytick.labelsize']=18
plt.legend(loc='best', fontsize=14) #Sets the legend box at the best location
plt.axhline(0, color='black', lw=2)
plt.axvline(0, color='black', lw=2)

plt.show()


# --------------
# Code starts here

import numpy as np

# Code starts here

# Adjacency matrix
adj_mat = np.array([[0,0,0,0,0,0,1/3,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                   [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                  [0,0,1/2,1/3,0,0,1/3,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/3,0]])

# Compute eigenvalues and eigencevectrs
eigenvalues,eigencevectors=np.linalg.eig(adj_mat)

# Eigen vector corresponding to 1
eigen_1=abs(eigencevectors[:,0])/(np.linalg.norm(eigencevectors[:,0],1))

print(eigen_1)
# most important page
page=int(np.where(eigen_1 == eigen_1.max())[0])+1
#page=eigen_1.where()
print(page)
# Code ends here

# --------------
# Code starts here

# Initialize stationary vector I
init_I=np.array([1,0,0,0,0,0,0,0]);
print(init_I)
for i in range(10):
    init_I=abs(np.dot(adj_mat, init_I))/(np.linalg.norm(init_I,1))
print(init_I)
power_page = np.where(np.max(init_I) == init_I)[0][0] + 1

print(power_page)
# Perform iterations for power method

# Code ends here

# --------------
# Code starts here

# New Adjancency matrix
# New Adjancency matrix
new_adj_mat = np.array([[0,0,0,0,0,0,0,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                  [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                   [0,0,1/2,1/3,0,0,1/2,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/2,0]])

# Initialize stationary vector I
new_init_I =np.array([1,0,0,0,0,0,0,0]);

# Perform iterations for power method
for i in range(10):
    new_init_I=abs(np.dot(new_adj_mat, new_init_I))/(np.linalg.norm(new_init_I,1))

print(new_init_I)

# Code ends here

# --------------
# Alpha value
alpha = 0.85

# Code starts here

# Modified adjancency matrix
n=len(new_adj_mat)
l=np.ones(new_adj_mat.shape).astype(float)
G=np.dot(alpha,new_adj_mat)+np.dot((1-alpha)*(1/n),l)
# Initialize stationary vector I
final_init_I=np.array([1,0,0,0,0,0,0,0])
# Perform iterations for power method
for i in range(1000):
    final_init_I=abs(np.dot(G, final_init_I))/(np.linalg.norm(final_init_I,1))
print(final_init_I)

# Code ends here


# Implementaion of k-Means Clustering algo for credit card fraud detection
# https://github.com/llSourcell/k_means_clustering
# Marker info - https://matplotlib.org/api/markers_api.html

import sys
from pprint import pprint
import numpy as np
import matplotlib.pyplot as plt

#np.random.seed(1)
datapath = 'k_means_clustering-master/durudataset.txt'
no_of_clusters = 2

def data_process(datapath):
    # preprocess the data
    data = np.loadtxt(datapath)
    num_rows, num_features = data.shape

    return data, num_rows, num_features

def plot(centroids, cluster_points, test_cluster={}):
    # Plot the clustered datapoints and their centroid
    colors = ['r', 'g', 'm', 'c', 'y', 'w']
    fig, ax = plt.subplots()

    for key in cluster_points.keys():
        for point in cluster_points[key]:
            ax.plot(point[0], point[1], (colors[key] + 'o'))

    if test_cluster:
        for key in test_cluster.keys():
            for point in test_cluster[key]:
                ax.plot(point[0], point[1], (colors[key] + 's'))

    for centroid in centroids:
        ax.plot(centroid[0], centroid[1], 'ko')     # black dot
    plt.show()

def euclidean(v):
    # returns norm of a vector/matrix
    return np.linalg.norm(v)

def clustering(data, belongs_to, k):
    # returns k clusters of data points
    cluster_points = {}
    for i in range(k):
        cluster_points[i] = []
    for i, value in enumerate(belongs_to):
        cluster_points[value].append(data[i])

    return cluster_points

def kmeans(data, num_rows, num_features, k):
    # k-Means algorithm
    rand_no = np.random.choice(num_rows, k)
    new_centroids = [data[i] for i in rand_no]
    belongs_to = list(np.zeros(num_rows))
    error = 1
    iterator = 0

    while error > 0:
        centroids = new_centroids
        for i, row in enumerate(data):
            distance = []
            for centroid in centroids:
                distance.append(euclidean(row - centroid))
            centroid_no = np.argmin(distance)
            belongs_to[i] = centroid_no

        # calculate no. of datapoints in each cluster
        # by calculating the frequency of items in belongs_to list
        d = {x: belongs_to.count(x) for x in belongs_to}
        cluster_counter = d.values()

        tmp_centroids = np.zeros((k, num_features))
        for i, row in enumerate(data):
            tmp_centroids[belongs_to[i]] += row
        tmp_centroids = [tmp_centroids[i]/float(cluster_counter[i]) for i in range(k)]

        new_centroids = tmp_centroids
        sub = np.subtract(new_centroids, centroids)
        error = euclidean(sub)
        iterator += 1
        new_centroids = [np.ndarray.tolist(i) for i in new_centroids]

    cluster_points = clustering(data, belongs_to, k)

    return new_centroids, cluster_points, iterator

def inference(test_data, centroids):
    # cluster new data points
    belongs_to = list(np.zeros(test_data.shape[0]))

    for i, data in enumerate(test_data):
        distance = []
        for centroid in centroids:
            distance.append(euclidean(data - centroid))
        belongs_to[i] = np.argmin(distance)

    return belongs_to

def main():
    data, num_rows, num_features = data_process(datapath)
    centroids, cluster_points, iterations = kmeans(data, num_rows, num_features, no_of_clusters)

    # Testing
    test_data = np.array([[1.8,1.8], [0.2,0.2], [0.25,1.5]])
    test_belongs = inference(test_data, centroids)
    test_cluster = clustering(test_data, test_belongs, no_of_clusters)

    # Graph plot
    print "Centroid of clusters: "
    pprint(centroids)
    print "No. of iterations: ", iterations
    plot(centroids, cluster_points, test_cluster)

if __name__ == '__main__':
    main()

'''
Result-
Centroid of clusters:
[[1.5805824656617171, 1.5689741160857174],
 [0.2233106749885314, 0.28960446247509586]]
No. of iterations:  3
'''


"""
                        Eigendecomposition
Eigenvalues and eigenvectors are easy to find with Python and NumPy. Remember,
an eigenvector of a square matrix  A  is a nozero vector  v  such that multiplication by  A
alters only the scale of  v
                                Av=λv
The scalar  λ  is known as the eigenvalue corresponding to this eigenvector.
 """

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.cm as cm

# find the eigenvalues and eigenvectors for a simple square matrix
A = np.diag(np.arange(1, 4))
"""
[[1 0 0]
 [0 2 0]
 [0 0 3]]
"""

eigenvalues, eigenvectors = np.linalg.eig(A)
"""
Eigenvalue's: [1. 2. 3.]
Eigenvectors:[[1. 0. 0.]
              [0. 1. 0.]
              [0. 0. 1.]]
"""

# the eigenvalue w[i] corresponds to the eigenvector v[:, i]
print('Eigenvalue: {}'.format(eigenvalues[1]))
print('Eigenvector: {}'.format(eigenvectors[:, 1]))
"""
Eigenvalue: 2.0
Eigenvector: [0. 1. 0.]
"""

# verify eigendecomposition - should return original matrix
matrix = np.matmul(np.diag(eigenvalues), np.linalg.inv(eigenvectors))
output = np.matmul(eigenvectors, matrix).astype(np.int)
"""
[[1 0 0]
 [0 2 0]
 [0 0 3]]
"""

# plot the eigenvectors
origin = [0,0,0]

fig = plt.figure(figsize=(18,10))
fig.suptitle('Effects of Eigenvalues and Eigenvectors')
ax1 = fig.add_subplot(121, projection='3d')

ax1.quiver(origin, origin, origin, eigenvectors[0, :], eigenvectors[1, :], eigenvectors[2, :], color = 'k')
ax1.set_xlim([-3, 3])
ax1.set_ylim([-3, 3])
ax1.set_zlim([-3, 3])
ax1.set_xlabel('X axis')
ax1.set_ylabel('Y axis')
ax1.set_zlabel('Z axis')
ax1.view_init(15, 30)
ax1.set_title("Before Multiplication")

# multiply original matrix by eigenvectors
new_eig = np.matmul(A, eigenvectors)
ax2 = plt.subplot(122, projection='3d')

# plot the new vectors
ax2.quiver(origin, origin, origin, new_eig[0, :], new_eig[1, :], new_eig[2, :], color = 'k')

# plot the eigenvalues for each vector (the amount the vector should be scaled by)
ax2.plot((eigenvalues[0]*eigenvectors[0]), (eigenvalues[1]*eigenvectors[1]), (eigenvalues[2]*eigenvectors[2]), 'rX')
ax2.set_title("After Multiplication")
ax2.set_xlim([-3, 3])
ax2.set_ylim([-3, 3])
ax2.set_zlim([-3, 3])
ax2.set_xlabel('X axis')
ax2.set_ylabel('Y axis')
ax2.set_zlabel('Z axis')
ax2.view_init(15, 30)

# check the png file for plot
plt.savefig('eigen_vectors.png')
plt.close(fig)


import numpy as np

#Importing the data
data = np.loadtxt('sample.txt', delimiter=",")
class LinearRegression(object):
    #Setting some of our initial parameters
    def __init__(self, alpha, theta):
        self.alpha = alpha
        self.theta = theta
    #Cleaning the data to make it ready for the training
    def Cleaningdata(self, data):
        ones = np.ones((len(data),1))
        #This needs to be modified before calibrated the method
        x = np.concatenate((ones, data[:,0].reshape(-1,1)), axis=1)
        y = data[:,1].reshape(-1,1)
        return (x, y)
    #Training the model using gradient descent
    def train(self,x , y, m):
        error = y.T - (self.theta @ x.T)
        J = m * (error @ error.T)
        dj = (m*-2) * ((error) @ x)
        self.theta = self.theta - self.alpha * dj
    #We can also train our model using the normal equation
    def normalEquation(self, x, y):
        self.theta = np.linalg.inv(x.T @ x) @ (x.T @ y)
    #We can query a specific answer
    def query(self, x):
        return self.theta @ x

#Example of a model
model = LinearRegression(0.00000001, [10,10])
x, y = model.Cleaningdata(data)

#Setting our training loop
iteration = 0
m = 1/len(data)
while iteration < 100000:
    iteration += 1
    model.train(x,y,m)
#Asking the model to answer a specific question
answer = model.query([100,23])

#Example of model traning using the normal equation
model.normalEquation(x,y)


import math
import sklearn.metrics
import pandas as pd
import numpy as  np
import matplotlib.pyplot as plt

def residuals(actual, predicted):
    return actual - predicted

def sse(actual, predicted):
    return (residuals(actual, predicted) **2).sum()

def mse(actual, predicted):
    n = actual.shape[0]
    return sse(actual, predicted) / n

def rmse(actual, predicted):
    return math.sqrt(mse(actual, predicted))

def ess(actual, predicted):
    return ((predicted - actual.mean()) ** 2).sum()

def tss(actual):
    return ((actual - actual.mean()) ** 2).sum()

def regression_errors(actual, predicted):
    return pd.Series({
        'sse': sse(actual, predicted),
        'ess': ess(actual, predicted),
        'tss': tss(actual),
        'mse': mse(actual, predicted),
        'rmse': rmse(actual, predicted),
    })

def baseline_mean_errors(actual):
    predicted = actual.mean()
    return {
        'sse': sse(actual, predicted),
        'mse': mse(actual, predicted),
        'rmse': rmse(actual, predicted),
    }

def better_than_baseline(actual, predicted):
    rmse_baseline = rmse(actual, actual.mean())
    rmse_model = rmse(actual, predicted)
    return rmse_model < rmse_baseline

def model_significance(ols_model):
    return {
        'r^2 -- variance explained': ols_model.rsquared,
        'p-value -- P(data|model == baseline)': ols_model.f_pvalue,
    }
def plot_residuals(actual, predicted):
    residuals = actual - predicted
    plt.hlines(0, actual.min(), actual.max(), ls=':')
    plt.scatter(actual, residuals)
    plt.ylabel('residual ($y - \hat{y}$)')
    plt.xlabel('actual value ($y$)')
    plt.title('Actual vs Residual')
    plt.show()


from statistics import mean
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import style

style.use('fivethirtyeight')

xs = np.array([1, 2, 3, 4, 5, 6], dtype=np.float64)
ys = np.array([5, 4, 6, 5, 6, 7], dtype=np.float64)

# plt.scatter(xs, ys)
# plt.show()

def best_fit_slope_and_intercept(xs, ys):
    # least square method
    m = ((mean(xs) * mean(ys)) - mean(xs*ys)) / (mean(xs)**2 - mean(xs**2))
    b = mean(ys) - m*mean(xs)
    return m, b

def squared_error(ys_orig, ys_line):
    # calculate squared error
    return sum((ys_line-ys_orig)**2)

def coefficient_of_determination(ys_orig, ys_line):
    y_mean_line = [mean(ys_orig) for y in ys_orig]
    squared_error_regr = squared_error(ys_orig, ys_line)
    squared_error_y_mean = squared_error(ys_orig, y_mean_line)
    return 1 - (squared_error_regr / squared_error_y_mean)

m, b = best_fit_slope_and_intercept(xs, ys)

# print (m, b)
regression_line = [(m*x)+b for x in xs]
# The line above is equal
# for x in xs:
#     regression_line.append((m*x)+b)

predict_x = 8
predict_y = (m*predict_x)+b

r_squared = coefficient_of_determination(ys, regression_line)
print (r_squared)

plt.scatter(xs, ys)
plt.scatter(predict_x, predict_y, color='g')
plt.plot(xs, regression_line)
plt.show()


# ------------- Machine Learning - Topic 2: Logistic Regression Multivariate

## Initialization
import numpy as np
from scipy.optimize import fmin, fmin_bfgs
import os, sys
sys.path.append(os.getcwd() + os.path.dirname('/ml/ex2/'))
from helpers import sigmoid, costFunctionReg, predict, plotData, plotDecisionBoundary, mapFeature

# Load Data
#  The first two columns contains the exam scores and the third column
#  contains the label.
data = np.loadtxt('ml/ex2/ex2data2.txt', delimiter=",")
X = data[:, :2]
y = data[:, 2]
plt, p1, p2 = plotData(X, y)

# # Labels and Legend
plt.xlabel('Microchip Test 1')
plt.ylabel('Microchip Test 2')
plt.legend((p1, p2), ('y = 1', 'y = 0'), numpoints=1, handlelength=0)
plt.show(block=False) # prevents having to close the graph to move forward
input('Program paused. Press enter to continue.\n')

## =========== Part 1: Regularized Logistic Regression ============
#  In this part, you are given a dataset with data points that are not
#  linearly separable. However, you would still like to use logistic
#  regression to classify the data points.
#
#  To do so, you introduce more features to use -- in particular, you add
#  polynomial features to our data matrix (similar to polynomial
#  regression).
#

# Add Polynomial Features

# Note that mapFeature also adds a column of ones for us, so the intercept
# term is handled
X = mapFeature(X[:,0], X[:,1])
m, n = X.shape

# Initialize fitting parameters
initial_theta = np.zeros((n, 1))

# Set regularization parameter lambda to 1
lambda_reg = 0.1

# Compute and display initial cost
# gradient is too large to display in this exercise
cost = costFunctionReg(initial_theta, X, y, lambda_reg)

print('Cost at initial theta (zeros): {:f}'.format(cost))
# print('Gradient at initial theta (zeros):')
# print(grad)

input('Program paused. Press enter to continue.\n')

## ============= Part 2: Regularization and Accuracies =============
#  Optional Exercise:
#  In this part, you will get to try different values of lambda and
#  see how regularization affects the decision coundart
#
#  Try the following values of lambda (0, 1, 10, 100).
#
#  How does the decision boundary change when you vary lambda? How does
#  the training set accuracy vary?

# Initialize fitting parameters
initial_theta = np.zeros((n, 1))

# Set regularization parameter lambda to 1 (you should vary this)
lambda_reg = 1

#  Run fmin_bfgs to obtain the optimal theta
#  This function returns theta and the cost
myargs=(X, y, lambda_reg)
theta = fmin_bfgs(costFunctionReg, x0=initial_theta, args=myargs)

# Plot Boundary
plotDecisionBoundary(theta, X, y)

# # Labels, title and Legend
plt.xlabel('Microchip Test 1')
plt.ylabel('Microchip Test 2')
plt.title('lambda = {:f}'.format(lambda_reg))

# % Compute accuracy on our training set
p = predict(theta, X)

print('Train Accuracy: {:f}'.format(np.mean(p == y) * 100))

input('Program paused. Press enter to continue.\n')


import numpy as np

def matrix_multiply(X, Y):
    """ Matrix multiplication
    Inputs:
      - X: A numpy array of shape (N, M)
      - Y: A numpy array of shape (M, K)
    Output:
      - out: A numpy array of shape (N, K)
    """
    out = np.dot(X, Y)
    return out

def matrix_rowmean(X, weights=np.empty(0)):
    """ Calculate mean of each row.
    In case of weights do weighted mean.
    For example, for matrix [[1, 2, 3]] and weights [0, 1, 2]
    weighted mean equals 2.6666 (while ordinary mean equals 2)
    Inputs:
      - X: A numpy array of shape (N, M)
      - weights: A numpy array of shape (M,) or an emty array of shape (0,)
    Output:
      - out: A numpy array of shape (N,)
    """
    if weights.size > 0:
        out = np.average(X, weights=weights, axis=1)
    else:
        out = np.average(X, axis=1)
    return out

def cosine_similarity(X, top_n=10, with_mean=True, with_std=True):
    """ Calculate cosine similarity between each pair of row.
    1. In case of with_mean: subtract mean of each row from row
    2. In case of with_std: divide each row on it's std
    3. Select top_n best elements in each row or set other to zero.
    4. Compute cosine similarity between each pair of rows.
    Inputs:
      - X: A numpy array of shape (N, M)
      - top_n: int, number of best elements in each row
      - with_mean: bool, in case of subtracting each row's mean
      - with_std: bool, in case of subtracting each row's std
    Output:
      - out: A numpy array of shape (N, N)

    Example (with top_n=1, with_mean=True, with_std=True):
        X = array([[1, 2], [4, 3]])
        after mean and std transform:
        X = array([[-1.,  1.], [ 1., -1.]])
        after top n choice
        X = array([[0.,  1.], [ 1., 0]])
        cosine similarity:
        X = array([[ 1.,  0.], [ 0.,  1.]])

    """
    if with_mean:
        X = X - np.mean(X, axis=1, keepdims=True)
    if with_std:
        X = X / np.std(X, axis=1, keepdims=True)
    if top_n is not None:
        X[np.arange(X.shape[0]).reshape(-1, 1),
          np.argsort(X)[:, :-top_n]] = 0
    out = np.dot(X, X.T) / (np.linalg.norm(X, axis=1, keepdims=True)
                            * np.linalg.norm(X, axis=1, keepdims=True).T)
    return out


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (20.0,10.0)

#Reading data

data = pd.read_csv('data.csv')
print(data.shape)

print(data.head())

#collecting the x and y values
X = data['experience'].values
Y = data['salary'].values

#to get the equation of the regression line y = mx + c ,
#we need to find the value of slope first
# m = Sum{ ([X-mean_x] * [Y - mean_y]) } / Sum { [X - mean_x]**2 }

#calculating the mean

mean_x =np.mean(X)
mean_y =np.mean(Y)

#collecting the total number of values so that we can minus the mean from everything

n = len(X)

# Lets consider m = numerator / denominator , we they each take the respective
#positions as mentioned in l20 equation

numerator, denominator = 0 , 0

for i in range(n):
	numerator += (X[i] - mean_x)*(Y[i] - mean_y)
	denominator += (X[i]-mean_x)**2

# lets get the value of m and c || y = mx + c
m = numerator/denominator
# from y = mx + c || c = y - mx
c = mean_y - (m*mean_x)

print(" m = {} and c = {} ".format(m,c))

#plotting Values and regression line

max_x = np.max(X) + 10
min_x = np.min(X) - 10

#calculating line values x and y

x = np.linspace(min_x, max_x,100)
y = m*x + c

#Rsquared method , did'nt understand this part

ss_t , ss_r= 0,0

for i in range(n):
	y_pred = m + X[i]*c
	ss_t += (Y[i] - mean_y)**2
	ss_r += (Y[i]- y_pred)**2

r2 = 1 -(ss_r/ss_t)
print("R2 = ",r2)

#plotting line

plt.plot(x,y, color ="#58b970", label= "Regression Line")

#plotting scatter points
plt.scatter(X, Y, c="#ef5423", label="Scatter Plots")

plt.xlabel('Experience')
plt.ylabel('Salary')
plt.legend()
plt.show()
from sklearn import model_selection,linear_model

exp_train , exp_test,salary_train , salary_test = model_selection.train_test_split(X,Y)
reg = linear_model.LinearRegression()
reg.fit(exp_train, salary_train)

print("From Calculation")
print("Slope = ", m)
print("Intercept = ", c)

print("From Linear Regression")
print("Slope = ", reg.coef_)
print("intercept  = ", reg.intercept_)


import numpy as np
import numpy.linalg as la
import matplotlib.pyplot as plt

v1 = np.array([1,1])
plt.plot([0, v1[0]], [0, v1[1]], 'b' , label='v', linewidth=2 )
plt.plot([1,0],[0,0], 'g', label='ihat')
plt.plot([0,0],[0,1], 'r', label='jhat')

plt.legend()
plt.axis('square')
plt.axis((-0.5,1.5,-0.5,1.5))
plt.grid()
plt.show()

TFM_Scale = np.array([[2,0], [0,3]])
v2 = TFM_Scale @ v1
print("Scale along diagonal transform matrix ")
print(TFM_Scale)
print(v2)

TFM_Rotate = np.array([[0,-1],[-1,0]])
v3=TFM_Rotate @ v1
print("90 degrees clockwise rotation: ")
print(TFM_Rotate)
print(v3)

TFM_Shear = np.array([[1,1],[0,1]])
v4 = TFM_Shear @ v1
print("Shear along x-axis: ")
print(TFM_Shear)
print(v4)

#Plot Transformation
plt.plot([0, v1[0]], [0, v1[1]], label='v', linewidth=2)
plt.plot([0, v2[0]], [0, v2[1]], label='Scale', linewidth=2)
plt.plot([0, v3[0]], [0, v3[1]], label="Rotate", linewidth=2)
plt.plot([0, v4[0]], [0, v4[1]], label='Shear',  linewidth= 2)

plt.legend()
plt.axis('square')
plt.axis((-3,4,-2,5))
plt.grid()
plt.show()


# Univariate Linear Regression
# Implementation using numpy, more efficient

import numpy as np
from sklearn import datasets

def partial_derivative(theta, trainX, trainY, trainSize):
    return (theta.dot(trainX) - trainY).dot(trainX.T) / trainSize

def fit(trainX, trainY, learningRate=0.1, threashold=1E-5):
    theta = np.array([0, 1], dtype=float) # Initialize theta
    trainSize = len(trainY)
    derivative = partial_derivative(theta, trainX, trainY, trainSize)
    while abs(derivative).max() > threashold: # While gradient doesn't converge
        theta -= learningRate * derivative # Perform gradient descent
        derivative = partial_derivative(theta, trainX, trainY, trainSize) # Update partial derivative
    return theta

def standard_fit(trainX, trainY):
    trainX = trainX[1, :] # Remove the extra bias term
    model = np.polyfit(trainX, trainY, deg=1) # Degree of 1 (linear model)
    return model

def accuracy(theta, testX, testY):
    # Returns the R Squared value of the model
    explained = theta.dot(testX)
    mean = np.sum(testY) / len(testY)
    totSumSquares = np.sum((testY - mean) ** 2)
    resSumSquares = np.sum((testY - explained) ** 2)
    accuracy = 1 - resSumSquares / totSumSquares
    return accuracy

dataset = datasets.load_diabetes()
data, target = dataset.data, dataset.target
dataSize = len(data)
data = data[:, 2] # Take only one feature
data = np.vstack((np.ones(dataSize), data)) # Add an extra bias term
separation = int(dataSize * 0.8) # Percentage of training data set and testing data set
trainX, trainY = data[:, :separation], target[:separation]
testX, testY = data[:, separation:], target[separation:]

hypothesis = standard_fit(trainX, trainY)
print('Fitted Model: y = {0}x {2} {1}'.format(hypothesis[0], abs(hypothesis[1]), ('-', '+')[hypothesis[1]>0]))
acc = accuracy(hypothesis[::-1], testX, testY)
print('Standard Accuracy: {:.4f}\n'.format(acc))

hypothesis = fit(trainX, trainY, 1, 1E-6)
print('Fitted Model: y = {1}x {2} {0}'.format(abs(hypothesis[0]), hypothesis[1], ('-', '+')[hypothesis[0]>0]))
acc = accuracy(hypothesis, testX, testY)
print('Fitted Accuracy: {:.4f}'.format(acc))


import numpy as np

def stmul(A,B) :
    # 에러 처리 부분은 생략하겠습니다. (e.g len(A)!=2^k)

    n=len(A)

    if 2<n :
        A11=A[0:n//2,0:n//2]
        A12=A[0:n//2,n//2:n]
        A21=A[n//2:n,0:n//2]
        A22=A[n//2:n,n//2:n]

        B11=B[0:n//2,0:n//2]
        B12=B[0:n//2,n//2:n]
        B21=B[n//2:n,0:n//2]
        B22=B[n//2:n,n//2:n]

        M1=stmul(A11+A22,B11+B22)
        M2=stmul(A21+A22,B11)
        M3=stmul(A11,B12-B22)
        M4=stmul(A22,B21-B11)
        M5=stmul(A11+A12,B22)
        M6=stmul(A21-A11,B11+B12)
        M7=stmul(A12-A22,B21+B22)

        C11=M1+M4-M5+M7
        C12=M3+M5
        C21=M2+M4
        C22=M1-M2+M3+M6

        C=np.zeros((n,n))

        C[0:n//2,0:n//2]=C11
        C[0:n//2,n//2:n]=C12
        C[n//2:n,0:n//2]=C21
        C[n//2:n,n//2:n]=C22

    else :

        A11=A[0,0]
        A12=A[0,1]
        A21=A[1,0]
        A22=A[1,1]

        B11=B[0,0]
        B12=B[0,1]
        B21=B[1,0]
        B22=B[1,1]

        M1=(A11+A22)*(B11+B22)
        M2=(A21+A22)*(B11)
        M3=(A11)*(B12-B22)
        M4=(A22)*(B21-B11)
        M5=(A11+A12)*(B22)
        M6=(A21-A11)*(B11+B12)
        M7=(A12-A22)*(B21+B22)

        C11=M1+M4-M5+M7
        C12=M3+M5
        C21=M2+M4
        C22=M1-M2+M3+M6

        C=np.zeros((n,n))

        C[0,0]=C11
        C[0,1]=C12
        C[1,0]=C21
        C[1,1]=C22

    return C

if __name__ == '__main__':
    A=np.array([[1,0,2,1],[4,1,1,0],[0,1,3,0],[5,0,2,1]])
    B=np.array([[0,1,0,1],[2,1,1,4],[2,0,1,1],[1,3,5,0]])

    C=stmul(A,B)
    print(C)

    D=np.matmul(A,B)
    print(D) #numpy 내장함수를 통해 행렬 A,B를 곱한 것이 strassen formula를 통해 구한 것과 동일함을 알 수 있다.


# 0452-qp-ls-lr-poly3.pg
# Impelement QP for LS in the case of LR for POLY3
# Sparisoma Viridi | https://github.com/dudung/bug
# 20220215 Create this program.

# abbreviations
# QP quaratic programming
# LS least square
# LR linear regression
# POLY3 polynomial function of 3rd order

# import numpy
import os, sys
import numpy as np

# open file, read lines and store them in a list, close file
fn = '0452-a.txt'
print('file:', fn)
with open(os.path.join(sys.path[0], fn), 'r') as f:
    lines = f.readlines()

# create empty list
x = [[]]
y = [[]]
A = []

# iterate through lines but ommit the first element
for l in lines[1:]:
    s = l.rstrip('\n')
    s = s.split('\t')
    xi = float(s[0])
    yi = float(s[1])

    x[0].append(xi)
    y[0].append(yi)

    p0 = 1
    p1 = xi
    p2 = xi**2
    p3 = xi**3
    Ai = [p0, p1, p2, p3]
    A.append(Ai)

# create numpy array
x = np.transpose(np.array(x))
y = np.transpose(np.array(y))
A = np.array(A)

# find c via some temporary variables
AT = np.transpose(A)
ATA = np.matmul(AT, A)
ATA_1 = np.linalg.inv(ATA)
ATA_1AT = np.matmul(ATA_1, AT)
c = np.matmul(ATA_1AT, y)

# print dimension of matrices
print()
print('y:', y.shape)
print('A:', A.shape)
print('AT:', AT.shape)
print('ATA:', ATA.shape)
print('ATA_1:', ATA_1.shape)
print('ATA_1AT:', ATA_1AT.shape)
print('c: ', c.shape)

# print results
print()
print('c = ')
print(c)


import numpy as np

def rmse(predictions, targets):
    return np.sqrt(((predictions - targets) ** 2).mean())

def polar_to_cartesian(z,theta):
    return z*np.exp(1j*np.deg2rad(theta))

def cartesian_to_polar(Z):
    x = Z.real
    y = Z.imag
    z = np.sqrt(x**2 + y**2)
    theta = np.arctan2(y, x)
    return z,theta

def MAPE(predictions, targets):
    targets, predictions = np.array(targets), np.array(predictions)
    return np.mean(np.abs((targets - predictions) / targets)) * 100


#encoding=utf-8

"""
1. Take the whole dataset consisting of dd-dimensional samples ignoring the class labels
2. Compute the dd-dimensional mean vector (i.e., the means for every dimension of the whole dataset)
3. Compute the scatter matrix (alternatively, the covariance matrix) of the whole data set
4. Compute eigenvectors (ee1,ee2,...,eedee1,ee2,...,eed) and corresponding eigenvalues (λλ1,λλ2,...,λλdλλ1,λλ2,...,λλd)
5. Sort the eigenvectors by decreasing eigenvalues and choose kk eigenvectors with the largest eigenvalues to form a d×kd×k dimensional
matrix WWWW(where every column represents an eigenvector)
6. Use this d×kd×k eigenvector matrix to transform the samples onto the new subspace. This can be summarized by the mathematical
equation: yy=WWT×xxyy=WWT×xx (where xxxx is a d×1d×1-dimensional vector representing one sample, and yyyy is the transformed
k×1k×1-dimensional sample in the new subspace.)
"""

import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d import proj3d

np.random.seed(12)

mu_vec1 = np.array([0,0,0])
cov_mat1 = np.array([[1,0,0],[0,1,0],[0,0,1]])
class1_sample = np.random.multivariate_normal(mu_vec1, cov_mat1, 20).T
assert class1_sample.shape == (3,20), "The matrix has not the dimensions 3x20"

mu_vec2 = np.array([1,1,1])
cov_mat2 = np.array([[1,0,0],[0,1,0],[0,0,1]])
class2_sample = np.random.multivariate_normal(mu_vec2, cov_mat2, 20).T
assert class2_sample.shape == (3,20), "The matrix has not the dimensions 3x20"

#
# fig = plt.figure(figsize=(8,8))
# ax = fig.add_subplot(111, projection='3d')
# plt.rcParams['legend.fontsize'] = 10
# ax.plot(class1_sample[0,:], class1_sample[1,:], class1_sample[2,:], 'o', markersize=8, color='blue', alpha=0.5, label='class1')
# ax.plot(class2_sample[0,:], class2_sample[1,:], class2_sample[2,:], '^', markersize=8, alpha=0.5, color='red', label='class2')
#
# plt.title('Samples for class 1 and class 2')
# ax.legend(loc='upper right')
#
# plt.show()

all_samples = np.concatenate((class1_sample, class2_sample), axis=1)
assert all_samples.shape == (3,40), "The matrix has not the dimensions 3x40"

mean_x = np.mean(all_samples[0,:])
mean_y = np.mean(all_samples[1,:])
mean_z = np.mean(all_samples[2,:])

mean_vector = np.array([[mean_x],[mean_y],[mean_z]])

print('Mean Vector:\n', mean_vector)

"""
The scatter matrix is computed by the following equation:
S=∑k=1n(xxk−mm)(xxk−mm)T
where mmmm is the mean vector
mm=1n∑k=1nxxkmm=1n∑k=1nxxk
"""

scatter_matrix = np.zeros((3,3))
for i in range(all_samples.shape[1]):
    scatter_matrix += (all_samples[:,i].reshape(3,1) - mean_vector).dot((all_samples[:,i].reshape(3,1) - mean_vector).T)
print('Scatter Matrix:\n', scatter_matrix)

cov_mat = np.cov([all_samples[0,:],all_samples[1,:],all_samples[2,:]])
print('Covariance Matrix:\n', cov_mat)

"""
4 --
"""
# eigenvectors and eigenvalues for the from the scatter matrix
eig_val_sc, eig_vec_sc = np.linalg.eig(scatter_matrix)

# eigenvectors and eigenvalues for the from the covariance matrix
eig_val_cov, eig_vec_cov = np.linalg.eig(cov_mat)

for i in range(len(eig_val_sc)):
    eigvec_sc = eig_vec_sc[:,i].reshape(1,3).T
    eigvec_cov = eig_vec_cov[:,i].reshape(1,3).T
    assert eigvec_sc.all() == eigvec_cov.all(), 'Eigenvectors are not identical'

    print('Eigenvector {}: \n{}'.format(i+1, eigvec_sc))
    print('Eigenvalue {} from scatter matrix: {}'.format(i+1, eig_val_sc[i]))
    print('Eigenvalue {} from covariance matrix: {}'.format(i+1, eig_val_cov[i]))
    print('Scaling factor: ', eig_val_sc[i]/eig_val_cov[i])
    print(40 * '-')

#
# from matplotlib import pyplot as plt
# from mpl_toolkits.mplot3d import Axes3D
# from mpl_toolkits.mplot3d import proj3d
# from matplotlib.patches import FancyArrowPatch
#
# class Arrow3D(FancyArrowPatch):
#     def __init__(self, xs, ys, zs, *args, **kwargs):
#         FancyArrowPatch.__init__(self, (0,0), (0,0), *args, **kwargs)
#         self._verts3d = xs, ys, zs
#
#     def draw(self, renderer):
#         xs3d, ys3d, zs3d = self._verts3d
#         xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M)
#         self.set_positions((xs[0],ys[0]),(xs[1],ys[1]))
#         FancyArrowPatch.draw(self, renderer)
#
# fig = plt.figure(figsize=(7,7))
# ax = fig.add_subplot(111, projection='3d')
#
# ax.plot(all_samples[0,:], all_samples[1,:], all_samples[2,:], 'o', markersize=8, color='green', alpha=0.2)
# ax.plot([mean_x], [mean_y], [mean_z], 'o', markersize=10, color='red', alpha=0.5)
# for v in eig_vec_sc.T:
#     a = Arrow3D([mean_x, v[0]], [mean_y, v[1]], [mean_z, v[2]], mutation_scale=20, lw=3, arrowstyle="-|>", color="r")
#     ax.add_artist(a)
# ax.set_xlabel('x_values')
# ax.set_ylabel('y_values')
# ax.set_zlabel('z_values')
#
# plt.title('Eigenvectors')
#
# plt.show()

# Make a list of (eigenvalue, eigenvector) tuples
eig_pairs = [(np.abs(eig_val_sc[i]), eig_vec_sc[:,i]) for i in range(len(eig_val_sc))]

# Sort the (eigenvalue, eigenvector) tuples from high to low
eig_pairs.sort(key=lambda x: x[0], reverse=True)

# Visually confirm that the list is correctly sorted by decreasing eigenvalues
for i in eig_pairs:
    print(i[0])

matrix_w = np.hstack((eig_pairs[0][1].reshape(3,1), eig_pairs[1][1].reshape(3,1)))
print('Matrix W:\n', matrix_w)

"""
6
"""
transformed = matrix_w.T.dot(all_samples)
assert transformed.shape == (2,40), "The matrix is not 2x40 dimensional."

plt.plot(transformed[0,0:20], transformed[1,0:20], 'o', markersize=7, color='blue', alpha=0.5, label='class1')
plt.plot(transformed[0,20:40], transformed[1,20:40], '^', markersize=7, color='red', alpha=0.5, label='class2')
plt.xlim([-4,4])
plt.ylim([-4,4])
plt.xlabel('x_values')
plt.ylabel('y_values')
plt.legend()
plt.title('Transformed samples with class labels')

plt.show()

from matplotlib.mlab import PCA as mlabPCA

mlab_pca = mlabPCA(all_samples.T)

print('PC axes in terms of the measurement axes scaled by the standard deviations:\n', mlab_pca.Wt)

plt.plot(mlab_pca.Y[0:20,0],mlab_pca.Y[0:20,1], 'o', markersize=7, color='blue', alpha=0.5, label='class1')
plt.plot(mlab_pca.Y[20:40,0], mlab_pca.Y[20:40,1], '^', markersize=7, color='red', alpha=0.5, label='class2')

plt.xlabel('x_values')
plt.ylabel('y_values')
plt.xlim([-4,4])
plt.ylim([-4,4])
plt.legend()
plt.title('Transformed samples with class labels from matplotlib.mlab.PCA()')

plt.show()

from sklearn.decomposition import PCA as sklearnPCA

sklearn_pca = sklearnPCA(n_components=2)
sklearn_transf = sklearn_pca.fit_transform(all_samples.T)

plt.plot(sklearn_transf[0:20,0],sklearn_transf[0:20,1], 'o', markersize=7, color='blue', alpha=0.5, label='class1')
plt.plot(sklearn_transf[20:40,0], sklearn_transf[20:40,1], '^', markersize=7, color='red', alpha=0.5, label='class2')

plt.xlabel('x_values')
plt.ylabel('y_values')
plt.xlim([-4,4])
plt.ylim([-4,4])
plt.legend()
plt.title('Transformed samples with class labels from matplotlib.mlab.PCA()')

plt.show()


__author__ = 'Randall'

"""
    DEMO: basisChebyshev
    This script provides a few examples on using the basis classes.
    Last updated: March 15, 2015.
    Copyright (C) 2014 Randall Romero-Aguilar
    Licensed under the MIT license, see LICENSE.txt
"""

from compecon import BasisChebyshev
import numpy as np
import matplotlib.pyplot as plt

"""
    EXAMPLE 1:
        Using BasisChebyshev to interpolate a 1-D function with a Chebyshev basis
        PROBLEM: Interpolate the function y = f(x) = 1 + sin(2*x) on the domain [0,pi], using 5 Gaussian nodes.
"""

# First, create the function f
f = lambda x: 1 + np.sin(2 * x)

# and the Chebyshev basis B. If the type of nodes is unspecified, Gaussian is computed by default
F = BasisChebyshev(5, 0, np.pi, f=f)

# Interpolation matrix and nodes: Obtain the interpolation matrix Phi, evaluated at the basis nodes.

# The basis nodes are:
xnodes = F.nodes

# and the basis coefficients are
c = F.c

# Next plot the function f and its approximation. To evaluate the function defined by the basis B and coefficients c
# at values xx we use the interpolation method:

# We also plot the residuals, showing the residuals at the interpolating nodes (zero by construction)
xx = np.linspace(F.a, F.b, 121)
f_approx = F(xx)
fxx = f(xx)

plt.figure()
plt.subplot(2, 1, 1)
plt.plot(xx, fxx)
plt.plot(xx, f_approx)
plt.legend(['f = 1 + sin(2x)','approx.'])
plt.title('Chebyshev approximation with 5 nodes')

plt.subplot(2, 1, 2)
plt.plot(xx, f_approx - fxx)
plt.title('Residuals using 5 nodes')

# Adjusting the number of nodes: to increase accuracy, we increase the number of nodes in B to 25.
F = BasisChebyshev(25, 0, np.pi, f=f)
plt.figure()
plt.subplot(2,1,1)
plt.plot(xx, f(xx))
plt.plot(xx, F(xx))
plt.legend(['f = 1 + sin(2x)','approx.'])
plt.title('Chebyshev approximation with 25 nodes')

plt.subplot(2,1,2)
plt.plot(xx, F(xx) - f(xx), 'r')
plt.title('Residuals using 25 nodes')

# With previous basis, now compute derivative.
df = lambda x: 2 * np.cos(2 * x)
plt.figure()
plt.subplot(2, 1, 1)
plt.plot(xx, df(xx))
plt.plot(xx, F(xx, 1))  # notice the 1 in Phi(xx,1) to compute first derivative
plt.legend(['df/dx = 2cos(2x)', 'approx.'])
plt.title('Chebyshev approximation with 25 nodes')

plt.subplot(2, 1, 2)
plt.plot(xx, F(xx, 1) - df(xx), 'r')
plt.title('Residuals using 25 nodes')

plt.show()


# THIS...
# IS.....
# TRIGGGGGGGGG!
print('''

CONSIDER THE FOLLOWING...

Imagine coordinates, x, and y

            |y
            |
            |
-----------------------------x
            |
            |
            |

What is the most glorious shape in the world?
Why, a circle of course!

I can't draw a circle with my keyboard (O, is that right??)
So let's plot it

OH, btw, we're making a circle with radius = 1

There's an EXTREMELY FANCY WAY to explain to your fellow mathematicians
that your circle has radius 1. You ready? The term is "UNIT".

It is a UNIT circle if it has radius 1.

''')

import matplotlib.pyplot as plt
import numpy as np

# Create the x axis, from -1 to 1, including 1 (linspace does this auto)

xs = np.linspace(-1,1,100)  # from -1 to 1, make 100 equally spaced points

ys = {'top':[], 'bot':[]}  # create a python dictionary holding the circle's ys

# top half

for x in xs:

    y_top = + np.sqrt( 1**2 - x**2)
    y_bot = - np.sqrt( 1**2 - x**2)

    ys['top'].append( y_top )
    ys['bot'].append( y_bot )

plt.plot(xs, ys['top'])
plt.plot(xs, ys['bot'])
plt.show()

print(
'''
Now, all that was a bit tedious wasn't it?

What if...

What if we could just say "I WANT A UNIT CIRCLE THAT GOES AROUND ONCE",
and it would just... WORK?

Hmmm...

In 'polar' coordinates, instead of using x and y as our "coordinates",
we can just use "r" and "theta", or a "radius" and an "angle"........

So maybe we can just "sweep" over angles, and plot the radii?

Something like...
'''

''' What is the circumference of a circle?

    ... I hope you remember...

    It's 2 * pi * radius.... Since our radius is 1, what's the circumference?

    2*pi. SO!

AHA!

 What if we just define our angles as fractions of the circumference?
 (instead of using degrees)

    So 0 degrees = 0 * 2*pi

    So 45 degrees = (2*pi / 8) =     (pi / 4) <--> 45 deg

    So 90 degrees = (2*pi / 4) =     (pi / 2) <--> 90 deg

    So 180 deg = (2*pi / 2)    =      (pi)    <--> 180 deg

    So 360 deg = (2*pi / 1)    =    (2*pi)    <--> 360 deg

    ... FYI these new fancy fractional angles are called 'radians'
'''
)

from numpy import pi

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

print('''

Okay, now how do to translate our new polar coordinates into xs and ys?
(We need to do this so we can plot...)

''')

# Here's how........

xs = np.cos(angles)  # cosine maps an angle to an X value on the unit circle
ys = np.sin(angles)  # sine maps an angle to a Y value on the unit circle

# tan = sin / cos ........ slope = y / x

plt.plot(xs, ys)
plt.show()

print(
''' Well... That was easy. But what the heck is cos and sine?

    The answer is best seen by drawing triangles inside of circles.

    I shall explain on the white board...!

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

    SOH : sin = opposite/hypotenuse
    CAH : cos = adjacent/hypotenuse
    TOA : tan = opposite/adjacent

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

    Basically,

    x = cos(angle) works because cosine = adjacent / hypotenuse = x / 1
    y = sin(angle) works because cosine = opposite / hypotenuse = y / 1

    if we draw the triangle like this:

          /|
    hyp  / |
        /  | opposite side
        ----
        adj

    where (angle) is measured between the adj and hypotenuse sides

    Notice also that:

        x**2 + y**2 = 1

                which means that

        cos(angle)**2 + sin(angle)**2 = 1    (for any angle)

'''
)

print(
'''

Congratulations!
That's basically all of the really important stuff about trigonometry!

If you ever want to get the ANGLE, and you know the LENGTH,

use angle = arccos(length) = acos(length), or
    angle = arcsin(length) = asin(length), or
    angle = arctan(length) = atan(length),

    which will all work for a certain range of lengths (not all lengths...)

Some extra tidbits:

    - There are these things called "trig identities"...
    - They are used like crazy in calculus and physics...
    - Basically, they boil down to equivalent ways to say exactly the same thing
    - If you want to see most of them, go here:
        https://en.wikipedia.org/wiki/List_of_trigonometric_identities

'''
)

Math You Will Actually Use/Week8_HowToTrig.py


# -*- coding: utf-8 -*-
"""AEOPDay3.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1EBz2EcAFR7wNNt5BtgNLWoT9kxSkqfy3

# HackerRank Problems Continued

Task 11: Mean, Var, and STD
"""

import numpy as np

a = np.array([[1, 2], [3, 4]])
print(np.mean(a, axis = 1))
print(np.var(a, axis=0))
print(np.std(a, axis=None))

"""Task 12 Dot and Cross"""

## The dot tool returns the dot product of two arrays
## The cross tool returns the cross product of two arrays.
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[1, 2], [3, 4]])
print(np.dot(A, B))
#dot is just the matrix

"""Task 13 Inner and Outer"""

## The inner tool returns the inner product of two arrays.

import numpy

A = numpy.array([0, 1])
B = numpy.array([3, 4])

print(numpy.inner(A, B))   #Output : 4
## The outer tool returns the outer product of two arrays.
import numpy

A = numpy.array([0, 1])
B = numpy.array([3, 4])

print(numpy.outer(A, B))    #Output : [[0 0]
                            #          [3 4]]

import numpy as np
A = np.array([0,1])
B = np.array([2, 3])

print(np.inner(A, B))
print(np.outer(A, B))

"""Task 14 Polynomials"""

import numpy as np
poly = np.array([1.1, 2, 3])
np.polyval((poly), 0)

"""The poly tool returns the coefficients of a polynomial with the given sequence of roots.
 - The polyint tool returns an antiderivative (indefinite integral) of a polynomial.
 - The roots tool returns the roots of a polynomial with the given coefficients.
 - The polyder tool returns the derivative of the specified order of a polynomial.
 - The polyval tool evaluates the polynomial at specific value. (used above)
"""
#print numpy.poly([-1, 1, 1, 10])
#Output : [  1 -11   9  11 -10]

#why are the outputs that way? are they different points of intersection

"""Task 15 Linear Algebra"""

import numpy as np

A = np.array([[1.1, 1.1], [1.1, 1.1]])
print(np.linalg.det(A))

"""# MatPlotLib

Video One: Creating + Customizing Plots
"""

import matplotlib

from matplotlib import pyplot as plt

# print(plt.style.available)
plt.style.use('fivethirtyeight')
# Median Developer Salaries by Age
ages_x = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]

dev_y = [38496, 42000, 46752, 49320, 53200,
         56000, 62316, 64928, 67317, 68748, 73752]

plt.plot(ages_x, dev_y, color='k', linestyle = '--' ,label = 'All Devs')

# Median Python Developer Salaries by Age
## can remobe to keep axis py_dev_x = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
py_dev_y = [45372, 48876, 53850, 57287, 63016,
            65998, 70003, 70000, 71496, 75370, 83640]

plt.plot(ages_x, py_dev_y, color = '#009000', marker = '*', linewidth=3, label = 'Python')

# Median JavaScript Developer Salaries by Age
js_dev_y = [37810, 43515, 46823, 49293, 53437,
            56373, 62375, 66674, 68745, 68746, 74583]

plt.plot(ages_x, js_dev_y, color = '#adad3b', marker = '.', label = 'JavaScript')

plt.xlabel('Ages')
plt.ylabel('Salary')
plt.title('Median Salary (USD) by Age')

# If you know the order of which color is which, or just add as new parameter in plt.plot
# plt.legend(['All Devs','Python']), just legend better bc updates automatically
plt.legend()
# format string fmt = '[marker][line][color]'

#plt.grid(True)
plt.tight_layout()

plt.savefig('plot.png')

plt.show(ages_x, dev_y)

"""Video 2: Bar Charts and Analyzing Data from CSV"""

import matplotlib
import numpy as np
from matplotlib import pyplot as plt

ages_x = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]

x_indexes = np.arange(len(ages_x))
width = 0.25

dev_y = [38496, 42000, 46752, 49320, 53200,
         56000, 62316, 64928, 67317, 68748, 73752]

plt.bar(x_indexes - width, dev_y, width=width, color = "#444444", label = "All Devs")

# Median Python Developer Salaries by Age
py_dev_y = [45372, 48876, 53850, 57287, 63016,
            65998, 70003, 70000, 71496, 75370, 83640]

plt.bar(x_indexes, py_dev_y, width=width, color = "#004400", label = "Python")

# Median JavaScript Developer Salaries by Age
js_dev_y = [37810, 43515, 46823, 49293, 53437,
            56373, 62375, 66674, 68745, 68746, 74583]

plt.bar(x_indexes + width, js_dev_y, width=width, color = "#440000", label = "JavaScript")

plt.xticks(ticks=x_indexes, labels = ages_x)

plt.legend()

import matplotlib
import csv
import pandas as pd #open files neater
import numpy as np
from collections import Counter
from matplotlib import pyplot as plt

'''with open('data.txt') as csv_file:
  csv_reader = csv.DictReader(csv_file)'''

data = pd.read_csv('data.txt')
ids = data['Responder_id']
lang_responses = data['LanguagesWorkedWith']

language_counter = Counter()

      #replace row with response after use of pandas and cvs_reader with lang_responses
for response in lang_responses:   #row also replaced
  language_counter.update(response.split(';'))
                      #and remove 'LanguagesWorkedWith idk why
languages =[]
popularity = []

for item in language_counter.most_common(15):
  languages.append(item[0])
  popularity.append(item[1])

#print(language_counter.most_common(15))
languages.reverse()
popularity.reverse()
plt.barh(languages, popularity)

"""Video 3 Pie Charts"""

import matplotlib
from matplotlib import pyplot as plt

# Language Popularity
slices = [59219, 55466, 47544, 36443, 35917]
labels = ['JavaScript', 'HTML/CSS', 'SQL', 'Python', 'Java']
explode = [0, 0, 0, 0.1, 0]
                                                                                #ask to define percentages
plt.pie(slices, labels = labels, explode = explode, shadow = True, startangle = 90, autopct = '%1.1f%%',colors = colors, wedgeprops = {'edgecolor' : 'black'})

plt.title("My Awesome Pie Chart")
plt.show()

"""Video 4 Stack Plots"""

import matplotlib
from matplotlib import pyplot as plt

plt.style.use("fivethirtyeight")

minutes = [1, 2, 3, 4, 5, 6, 7, 8, 9]

player1 = [8, 6, 5, 5, 4, 2, 1, 1, 0]
player2 = [0, 1, 2, 2, 2, 4, 4, 4, 4]
player3 = [0, 1, 1, 1, 2, 2, 3, 3, 4]

labels = ('Player 1', 'Player 2', 'Player 3')
plt.stackplot(minutes, player1, player2, player3, labels= labels)

plt.legend(loc = (0.07, 0.05))  #google matplotlib legend to adjust coordinates

plt.title("My Awesome Stack Plot")
plt.tight_layout()
plt.show()

"""Filling Area on Line Plots"""

import pandas as pd
import matplotlib
from matplotlib import pyplot as plt

data = pd.read_csv('data.csv')
ages = data['Age']
dev_salaries = data['All_Devs']
py_salaries = data['Python']
js_salaries = data['JavaScript']

plt.plot(ages, dev_salaries, color='#444444',
         linestyle='--', label='All Devs')

plt.plot(ages, py_salaries, label='Python')

overall_median = 57287
                                    #to change intensity of fill
plt.fill_between(ages, py_salaries, overall_median,
                 where = (py_salaries <= overall_median),
                 interpolate = True, alpha = 0.25, )
plt.fill_between(ages, py_salaries, overall_median,
                 where = (py_salaries > overall_median),
                 color = "green",alpha = 0.25)

plt.legend()

plt.title('Median Salary (USD) by Age')
plt.xlabel('Ages')
plt.ylabel('Median Salary (USD)')

plt.tight_layout()

plt.show()

## TIMESTAMP: 11:30


import sympy

x = sympy.symbols('x')

f = (x**2 - 2*x)*sympy.exp(3 - x)

fp = sympy.simplify(sympy.diff(f)) # (x*(2 - x) + 2*x - 2)*exp(3 - x)

fp2 = -(x**2 - 4*x + 2)*sympy.exp(3 - x)

sympy.simplify(fp2 - fp) == 0  # True

F = sympy.integrate(f, x)  # -x**2*exp(3 - x)


# -*- coding: utf-8 -*-
"""
Simulazione 8 Settembre 2020

Funzione e radici
"""
import matplotlib.pyplot as plt
import numpy as np
import sympy as sym
from scipy import optimize
import math

# L'unico metodo a convergenza quadratica è Newton
def newton(fname, fpname, x0, m,  tol, nMaxIt):
    xk = []
    it = 0
    if abs(fpname(x0)) <= np.spacing(1):
        print("Derivata nulla in x0")
        return [], 0, []
    else:
        d = (fname(x0)/ fpname(x0))
        x1 = x0 - d*m
        xk.append(x1)
        it += 1
        while it < nMaxIt and abs(fname(x1)) >= tol and abs(d) >= tol * abs(x1):
            x0 = x1
            if abs(fpname(x0)) <= np.spacing(1):
                print("Derivata nulla in x0")
                return x1, it, xk
            d = (fname(x0)/ fpname(x0))
            x1 = x0 - d*m
            xk.append(x1)
            it += 1

        if it == nMaxIt:
            print("Numero massimo di iterazioni raggiunto")
        return x1, it, xk

# Definisco Funzione e derivata
x = sym.Symbol("x")
xx = np.linspace(1, 3, 100)
fx = x - (2 * sym.sqrt(x - 1))
dfx = sym.diff(fx, x, 1)

f = sym.utilities.lambdify(x, fx, np)
d = sym.utilities.lambdify(x, dfx, np)

# Plotto la funzione per cercare le radici
plt.plot(xx, f(xx))
plt.title("Funzione")
plt.show()

# Noto che la funzione vicino al 2 vale 0
zero = optimize.fsolve(f, 2.0)
print(zero)

x1, it, xks = newton(f, d, 3.0, 2.0, 1.e-12, 500)
print("La derivata nel punto è ", d(x1))
# Stimo l'ordine di convergenza del metodo
numO = math.log(abs(xks[-2] - xks[-1]) / abs(xks[-3] - xks[-2]))
denO = math.log(abs(xks[-3] - xks[-2]) / abs(xks[-4] - xks[-3]))
ordine = numO / denO
print(f"L'ordine di convergenza del metodo è {ordine}")

plt.semilogy(np.arange(it), xks)
plt.title("Iterazioni")
plt.show()

"""
    Osservazioni: il metodo di Newton inizialmente con la radice "2" aveva un ordine
    di convergenza pari a 1, quindi la radice ha molteplicità m, provando poi ad
    implementare la versione modificata del metodo ho ottenuto un ordine di convergenza
    pari a 2, utilizzando m = 2 dunque la radice ha molteplicità pari a 2
    Inoltre il metodo non converge usando '1' come valore di innesco in quanto quest'ultimo
    ha una derivata prossima allo 0
"""


# Necessary import(s)
import numpy as np

# The helper methods used by the learning methods above are implemented below:

def compute_error_vector(y, tx, w):
    """
    Computes the error vector that is defined as y - tx . w
    Args:
        y: labels
        tx: features
        w: weight vector
    Returns:
        error_vector: the error vector defined as y - tx.dot(w)
    """
    return y - tx.dot(w)

def compute_mse(error_vector):
    """
    Computes the mean squared error for a given error vector.
    Args:
        error_vector: error vector computed for a specific dataset and model
    Returns:
        mse: numeric value of the mean squared error
    """
    return np.mean(error_vector ** 2) / 2

def compute_gradient(tx, error_vector):
    """
    Computes the gradient for the mean squared error loss function.
    Args:
        y: labels
        error_vector: error vector computed for a specific data set and model
    Returns:
        gradient: the gradient vector computed according to its definition
    """
    return - tx.T.dot(error_vector) / error_vector.size

def build_polynomial(x, degree):
    """
    Extends the feature matrix, x, by adding a polynomial basis of the given degree.
    Args:
        x: features
        degree: degree of the polynomial basis
    Returns:
        augmented_x: expanded features based on a polynomial basis
    """
    num_cols = x.shape[1] if len(x.shape) > 1 else 1
    augmented_x = np.ones((len(x), 1))
    for col in range(num_cols):
        for degree in range(1, degree + 1):
            if num_cols > 1:
                augmented_x = np.c_[augmented_x, np.power(x[ :, col], degree)]
            else:
                augmented_x = np.c_[augmented_x, np.power(x, degree)]
        if num_cols > 1 and col != num_cols - 1:
            augmented_x = np.c_[augmented_x, np.ones((len(x), 1))]
    return augmented_x

def compute_rmse(loss_mse):
    """
    Computes the root mean squared error.
    Args:
        loss_mse: numeric value of the mean squared error loss
    Returns:
        loss_rmse: numeric value of the root mean squared error loss
    """
    return np.sqrt(2 * loss_mse)

def sigmoid(t):
    """
    Applies the sigmoid function to a given input t.
    Args:
        t: the given input to which the sigmoid function will be applied.
    Returns:
        sigmoid_t: the value of sigmoid function applied to t
    """
    return 1. / (1. + np.exp(-t))

def compute_logistic_loss(y, tx, w):
    """
    Computes the loss as the negative log likelihood of picking the correct label.
    Args:
        y: labels
        tx: features
        w: weight vector
    Returns:
        loss: the negative log likelihood of picking the correct label
    """
    tx_dot_w = tx.dot(w)
    return np.sum(np.log(1. + np.exp(tx_dot_w)) - y * tx_dot_w)

def compute_logistic_gradient(y, tx, w):
    """
    Computes the gradient of the loss function used in logistic regression.
    Args:
        y: labels
        tx: features
        w: weight vector
    Returns:
        logistic_gradient: the gradient of the loss function used in
            logistic regression.
    """
    return tx.T.dot(sigmoid(tx.dot(w)) - y)

def penalized_logistic_regression(y, tx, w, lambda_):
    """
    Adds the penalization term (2-norm of w vector) on top of the normal
    logistic loss. Computes the modified loss and gradient.
    Args:
        y: labels
        tx: features
        w: weight vector
    Returns:
        loss: the modified version of the normal logistic loss
        logistic_gradient: the gradient of modified loss function used in
            penalized logistic regression.
    """
    loss = compute_logistic_loss(y, tx, w) + (lambda_ / 2) * w.T.dot(w)
    gradient = compute_logistic_gradient(y, tx, w) + lambda_ * w
    return loss, gradient

def cross_terms(x, x_initial):
    """
    Adds the multiplication of different features as new features.
    Args:
        x: the given feature matrix
        x_initial: the features whose multiplications will be added
    Returns:
        x_cross_terms: feature matrix with cross terms
    """
    for col1 in range(x_initial.shape[1]):
        for col2 in np.arange(col1 + 1, x_initial.shape[1]):
            if col1 != col2:
                x = np.c_[x, x_initial[:, col1] * x_initial[:, col2]]
    return x

def log_terms(x, x_initial):
    """
    Adds the logarithms of features as new features.
    Args:
        x: the given feature matrix
        x_initial: the features whose logarithms will be added
    Returns:
        x_log_terms: feature matrix with logarithms
    """
    for col in range(x_initial.shape[1]):
        current_col = x_initial[:, col]
        current_col[current_col <= 0] = 1
        x = np.c_[x, np.log(current_col)]
    return x

def sqrt_terms(x, x_initial):
    """
    Adds the square roots of features as new features.
    Args:
        x: the given feature matrix
        x_initial: the features whose square roots will be added
    Returns:
        x_sqrt_terms: feature matrix with square roots
    """
    for col in range(x_initial.shape[1]):
        current_col = np.abs(x_initial[:, col])
        x = np.c_[x, np.sqrt(current_col)]
    return x

def apply_trigonometry(x, x_initial):
    """
    Adds the sin and cos of features as new features.
    Args:
        x: the given feature matrix
        x_initial: the features whose sin and cos will be added
    Returns:
        x_sqrt_terms: feature matrix with sine values
    """
    for col in range(x_initial.shape[1]):
        x = np.c_[x, np.sin(x_initial[:, col])]
        x = np.c_[x, np.cos(x_initial[:, col])]
    return x

def feature_engineering(x, degree, has_angles = False):
    """
    Builds a polynomial with the given degree from the initial features,
    add the cross terms, logarithms and square roots of the initial features
    as new features. Also includes the sine of features as an option.
    Args:
        x: features
        degree: degree of the polynomial basis
        has_angles: Boolean value to determine including sin and cos of features
    Returns:
        x_engineered: engineered features
    """
    x_initial = x
    x = build_polynomial(x, degree)
    x = cross_terms(x, x_initial)
    x = log_terms(x, x_initial)
    x = sqrt_terms(x, x_initial)
    if has_angles:
        x = apply_trigonometry(x, x_initial)
    return x