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

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1A_UuTe0gNqR9xTNvXnsxSA-Joe9ZNNyj
"""

import time
import numpy as np
import matplotlib.pyplot as plt
from math import fabs
import sympy
from collections import defaultdict
import matplotlib.patches as patches
from numpy import inf
import re as reg
from numpy import linalg as LA
import copy
import math
from fractions import Fraction
from math import factorial
from sympy import *
from scipy.optimize import curve_fit
from numpy import pi
from typing import Tuple, List
import bisect
from matplotlib import mlab
from scipy. integrate import odeint
import random as rn
from numpy.random import choice as np_choice





def d_fun(x,fun):
    h = 1e-5
    ddif = (fun(x+h)-fun(x-h))/(2*h)
    return ddif
def diff_us(x,a,b,h,funn):
    """Дифференцирование руками
    x-список x
    a - начало прмежутка
    b -конец промежутка
    h -шаг
    funn -функция
    """
    time.time()
    k = 0
    d1 = {}
    ind = 0
    for i in np.arange(a, b, h):
        i = float('{:.3f}'.format(i))
        d1.update({i:d_fun(i,funn)})
        ind+=1

    plt.plot(x,funn)
    plt.title('Graf: $x$ from $y$')
    plt.xlabel('$x$')
    plt.ylabel('$y$')
    plt.show()
    plt.plot(x,d_fun(x))
    plt.title('diff: $x$ from $y$')
    plt.xlabel('$x$')
    plt.ylabel('$y$')
    plt.show()
    a = time.time_ns()
    print(d1)


def dydx(y,dx):
    return np.gradient(y, dx)
def diffx_fromnp(a,b,h):
    """Дифференцирование нампи
    a - начало прмежутка
    b -конец промежутка
    h -шаг
    """
    time.time()
    x = np.arange(a, b, h)
    y = np.sin(x)*x**2
    dx = x[1]-x[0]
    d2 = {}
    ind = 0
    for i in np.arange(-10, 10, 0.1):
        i = float('{:.3f}'.format(i))
        d2.update({i:dydx[ind]})
        ind+=1
    plt.plot(x,y)
    plt.title('Graf: $x$ from $y$')
    plt.xlabel('$x$')
    plt.ylabel('$y$')
    plt.show()
    plt.plot(dydx)
    plt.title('diff: $x$ from $y$')
    plt.xlabel('$x$')
    plt.ylabel('$y$')
    plt.show()
    b = time.time_ns()
    d2

def delta_dict(x,a,b,h,dydx,d_fun):
    """сравнение результатов нампи и руками"""
    D = defaultdict(list)
    ind = 0
    for i in np.arange(-10, 10, 0.1):
        i = float('{:.3f}'.format(i))
        D.update({i:[dydx[ind],d_fun(i),'Дельта: {}'.format(dydx[ind]-d_fun(i))]})
        ind+=1
    return D
def S_funn(step = 100,a = -10,b = 2 ,fun2 = 'np.cos(x)/x**5+x'):
    """Площадь под дифференциалом
    step = 100 шаг
    a = -10 начало промежутка
    b = 2 конец промежутка
    fun2 = 'np.cos(x)/x**5+x' функция по умолчанию
    """
    h = (b-a)/step
    x = np.arange(a, b+h-h/10, h)

    dx = x[1]-x[0]
    y = 1/x**2
    def f(x):
        return 1/x**2
    dydx = dydx(y, dx)


    y2 = fun2


    I = fabs(h*(sum([f(i) for i in np.arange(a,b,h)]))+h*(f(a)/2+f(b)/2))   #исправление ошибки с отрицательной площадью


    plt.plot(x,y,label = "{}".format(y2))

    for i in x:
        if i <= b:
            plt.fill([i,i,i+h,i+h],[0,f(i),f(i+h),0],'red')
        else:
            plt.fill([i,i,i+h,i+h],[0,f(i),f(i+h),0],'red',label = 'S')

    for i in dydx:   #исправление ошибки с инф
        if fabs(i)>100:
            I= inf

    plt.legend(
        fontsize = 15,
        ncol = 2,    
        facecolor = 'oldlace',    
        edgecolor = 'r',    
        title = 'S = {}'.format(I),  
        title_fontsize = '15'
    )
    plt.xlabel('$x$')
    plt.ylabel('$y$')
    plt.show()
    
def chk(x): 
    """
    определяем тип числа заносящегося в массив.
    x: str - число в строке
    Функция определяет тип числа, представленного в строковом виде.
    """
    c1 = reg.compile(r'[\d]+') 
    c2 = reg.compile(r'[\d]+[\.][\d]+') 
    c3 = reg.compile(r'[j]')
    
    if c3.search(x): 
        return complex(x)
    else:
        if c2.search(x):
            return float(x)
        else:
            if c1.search(x):
                return int(x)
            else:
                print('!Incorrect value!')
def mtx_macker(n, m, z = False, e = False): 
    """
    создаем квадратную матрицу размерности n.
    mtx_macker(n, m, z = False, e = False)
    n: int - колво столбцов
    m: int - колво строк
    z: boolean = False - необязательный логический параметр
    e: boolean = False - необязательный логический параметр
    Функция создаёт матрицу размерности nm. Для ее работы необходима фукция chk(x). 
    Если z = True, то создаётся нулевая матрица размерности nm. 
    Если e = True, то создаётся единичная матрица размерности n*m.
    Синтаксис ввода значений элементов матрицы:
    "12" - целое;
    "2.34" - число с плавающей точкой;
    "1+2j" - комплексное число.
    Числа всех трехтипов могут быть и отрицательными.
    """
    mtx = []
    
    if z == True: #создаём нулевую матрицу
        for i in range(m):
            mtx.append([0 for j in range(n)]) 
        return mtx

    else:
        if e == True: #создаём единичную матрицу
            for i in range(m):
                mtx.append([1 if j == i else 0 for j in range(n)]) 
            return mtx
        else:
            for i in range(m):
                mtx.append([chk(input(f'a{i+1}{j+1} = ')) for j in range(n)])
            return mtx

def mtx1_macker(n, m, z = False, e = False): 
    """
    создание numpy матрицы.
    mtx1_macker(n, m, z = False, e = False)
    n: int - кол-во столбцов
    m: int - кол-во строк
    z: boolean = False - необязательный логический параметр
    e: boolean = False - необязательный логический параметр
    Функция создаёт матрицу numpy размерности nm. Для ее работы необходима фукция chk(x). 
    Если z = True, то создаётся нулевая матрица размерности nm. 
    Если e = True, то создаётся единичная матрица размерности n*m.
    Синтаксис ввода значений элементов матрицы:
    "12" - целое;
    "2.34" - число с плавающей точкой;
    "1+2j" - комплексное число.
    Числа всех трехтипов могут быть и отрицательными.
    """
    mtx = []
    
    if z == True: #создаём нулевую матрицу
        for i in range(m):
            mtx.append([0 for j in range(n)]) 
        return np.matrix(mtx)

    else:
        if e == True: #создаём единичную матрицу
            for i in range(m):
                mtx.append([1 if j == i else 0 for j in range(n)]) 
            return np.matrix(mtx)
        else:
            for i in range(m):
                mtx.append([chk(input(f'a{i+1}{j+1} = ')) for j in range(n)])
            return np.matrix(mtx)
        
def print_mtx(mtx):
    """
    Вывод матрицы
    mtx: list - матрица
    Функия выводит матрицу.
    """
    for i in range(len(mtx)):
        for j in range(len(mtx[i])):
            print(mtx[i][j], end = ' ')
        print()
        
def mult(a,b):
    """
    Для ее работы необходима функция mtx_macker(n)
    функция умножения матрицы на число или матрицы на матрицу !!!ВАЖНО!!! число всегда ПЕРВЫЙ аргумент!!!
    a: int, float, complex, list - коэффицент или матрица a
    b: list - матрица b
    Функция умножает матрицу b на число a, или матрицу a на матрицу b.
    """
    if type(a) != list: #проверка коэффициента
        m = mtx_macker(len(b),len(b), z = True)
        for i in range(len(b)):
            for j in range(len(b)):
                m[i][j] = b[i][j] * a
        return m
    
    else:
        if type(a) == list: #проверка коэффициента
            m = mtx_macker(len(b),len(b), z = True)
            for ind in range(len(b)): #каждый элемент (k) строки 'ind' матрицы A умножается на соотв. элемент (k) столбца 'e' матрицы B. Значения суммируются
                for e in range(len(b)): 
                    for k in range(len(b)): 
                        m[ind][e] += a[ind][k]  * b[k][e] # m11 = a11*b11 + a12*b21 + a12*b31    m12 = a11*b12 + a12*b22 + a12*b32   ...
            return m

def add(a,b): #сложение матриц
    """
    сложение матриц
    a: list - матрица a
    b: list - матрица b
    Функция складывает матрицу a и b. Для ее работы необходима функция mtx_macker(n).
    """
    m = mtx_macker(len(a),len(a), z = True)
    for i in range(len(a)): 
        for j in range(len(b)): 
            m[i][j] = a[i][j]  + b[i][j]
    return m

def sub(a,b): 
    """
    вычитание матриц
    a: list - матрица a
    b: list - матрица b
    Функция складывает матрицу a с матрицей b. Для ее работы необходима функция mtx_macker(n).
    """
    m = mtx_macker(len(a),len(a), z = True)
    for i in range(len(a)): 
        for j in range(len(b)): 
            m[i][j] = a[i][j]  - b[i][j]
    return m

def transp(m):
    """
    Функция для транспонирования матрицы
    m - матрица для транспонирования
    выводится транспонированная матрица
    """
    transp = []
    transp = [[m[y][x] for y in range(len(m))] for x in range(len(m[0]))]
    return transp

def getminor(matrica,i,j):
    """
    Функция выводит минор по определенным коэффицентам матрицы на вводе
    matrica: матрица
    i: int - индекс ряда
    j: int - индекс столбца
    """
    return [row[:j] + row[j+1:] for row in (matrica[:i]+matrica[i+1:])]

def getdeternminant(m):
    """ 
    Функция считает определитель матрицы.
    Для ее работы необходима функция getminor(matrica,i,j).
    m: list - матрица m
    """
    if len(m) == 2:
        return m[0][0]*m[1][1]-m[0][1]*m[1][0]
    
    determinant = 0
    for c in range(len(m)):
        determinant += ((-1)**c)*m[0][c]*getdeternminant(getminor(m,0,c))
    return determinant

def getalgcompl(m):
    """
    составляет матрицу алгебраических дополнений
    Для ее работы необходимы ф-ии getminor(matrica,i,j), getdeternminant(m)
    m: list - матрица m
    Функция составляет матрицу из алгебраических дополнений элементов матрицы m.
    """
    res_A = []
    for i in range(len(m)):
        res_A.append([((- 1) ** (i + j + 2)) * getdeternminant(getminor(m, i, j)) for j in range(len(m))])
    return res_A

def reverse_mtx(mtx):
    """
    Функция считает обратную матрицу для матрицы mtx
    Для ее работы необходимы ф-ии getalgcompl(m), mult(a,b), getdeternminant(m), transp(m).
    mtx: list - матрица mtx 
    """
    return mult(1 / getdeternminant(mtx), transp(getalgcompl(mtx)))

def invr_mtx_gauss(mtx):
    """
    Функция считает обратную матрицу для матрицы mtx, используя метод гаусса.
    Для работы функции необходимы функции mtx_macker(), dgordan_gauss().
    mtx: list - матрица mtx
    """
    mtx_d = mtx_macker(len(mtx),len(mtx[0]), e = True)
    return dgordan_gauss(mtx, mtx_d, invr = True)

def invr_mtx_gauss_ratio(mtx, f = False):
    """
    Функция считает обратную матрицу для матрицы mtx, используя метод гаусса для работы с рациональными дробями (fractions).
    Для работы функции необходимы функции mtx_macker(), dgordan_gauss_ratio().
    mtx: list - матрица mtx
    f: boolean = False - необязательный логический параметр
    Если параметр f = True, то все значения выводятся как рациональные дроби. 
    """
    mtx_d = mtx_macker(len(mtx),len(mtx[0]), e = True)
    return dgordan_gauss_ratio(mtx, mtx_d, invr = True, f = f)

def expan_mtx(a,b):
    """
    Функция создаёт расширенную матрицу путём "склеивания" матриц a и b
    a: list - матрица a
    b: list - матрица b
    """
    res = copy.deepcopy(a)
    for i in range(len(b)):
        for j in range(len(b[i])):
            res[i].append(b[i][j])
    return res

def dgordan_gauss(a, b, invr = False):
    """
    Функция решает систему линейных уравнений ax = b, используя метод Жордана-Гаусса.
    Если параметр invr = True, то функция считает обратную матрицу,
    используя тот же метод (в этом случаем b - единичная матрица).
    Если invr = True, то функция возвращает обратную матрицу (список списков).
    Для работы функции необходима ф-ия expan_mtx(a,b), invr_mtx_gauss(mtx).
    a: list - матрица a (коэффиценты переменных/ исходная матрица)
    b: list - матрица b (свободные члены уравнений/ единичная матрица для исходной матрицы)
    invr: boolean = False - необязательный логический параметр
    На выход подаётся исходная матрица A, A^(-1), X.
    """
       
    m = expan_mtx(a,b) # создаём расширенную матрицу
               
    if getdeternminant(a) == 0: #проверка матрицы на вырожденность
        raise Exception('СЛАУ не имеет единственного решения')
        
    A = copy.deepcopy(m) #создаём копию расширенной матрицы
    i_r = 0    
    while i_r <= len(m) - 1:
        
        for j in reversed(range(i_r, len(m[0]))):
            A[i_r][j] = A[i_r][j] / A[i_r][i_r] #считаем элементы строки i_r с разрешающим элементом A[i_r][j]
            
        for ii in range(i_r):  
            for jj in reversed(range(i_r, len(m[0]))):
                A[ii][jj] = A[ii][jj] - A[i_r][jj] * A[ii][i_r] #считаем новое значение элементов строк до i_r строки
            
        for ie in range(i_r + 1, len(m)):  
            for je in reversed(range(i_r, len(m[0]))):
                A[ie][je] = A[ie][je] - A[i_r][je] * A[ie][i_r] ##считаем новое значение элементов строк после i_r строки
                
        i_r += 1
              
    if invr == False:
        
        print('Прямая матрица коэффицентов.')
        print_mtx(a)
        print()
        print('Обратная матрица коэффицентов.')
        print_mtx(invr_mtx_gauss(a))
        print()
        
        X = []
        for el in range(len(A)):
            print(f'x{el + 1} = {A[el][len(A[0]) - 1]}')
            X.append(A[el][len(A[0]) - 1])
        print()
        return [a, invr_mtx_gauss(a), X]
        
    else:
        if invr == True:
            for ind in range(len(m)):
                for num in reversed(range(len(m[0]) // 2)): 
                    A[ind].pop(num) #удаляем единичную матрицу, чтобы осталась только обратная
            return A

def dgordan_gauss_ratio(a, b, invr = False, f = False):
    """
    Функция работает с матрицами элементы которых относятся к классу Fractions (рациональные дроби).
    Функция решает систему линейных уравнений ax = b, используя метод Жордана-Гаусса.
    Для работы функции необходима ф-ия expan_mtx(a,b), invr_mtx_gauss_ratio(mtx, f).
    Если параметр invr = True, то функция считает обратную матрицу, используя тот же метод (в этом случаем b - единичная матрица).
    Если invr = True, то функция возвращает обратную матрицу (список списков).
    Если параметр f = True, то все значения выводятся как рациональные дроби. 
    a: list - матрица a (коэффиценты переменных/ исходная матрица)
    b: list - матрица b (свободные члены уравнений/ единичная матрица для исходной матрицы)
    invr: boolean = False - необязательный логический параметр
    f: boolean = False - необязательный логический параметр
    На выход подаётся исходная матрица A, A^(-1), X. 
    """
       
    m = expan_mtx(a,b) # создаём расширенную матрицу
               
    if getdeternminant(a) == 0: #проверка матрицы на вырожденность
        raise Exception('СЛАУ не имеет единственного решения')
        
    A = []
    for i0 in range(len(m)):  # преобразуем элементы матрицы в класс fractions
        A.append([Fraction(j0) for j0 in m[i0]])
        
    i_r = 0    
    while i_r <= len(m) - 1:
        
        for j in reversed(range(i_r, len(m[0]))):
            A[i_r][j] = A[i_r][j] / A[i_r][i_r] #считаем элементы строки i_r с разрешающим элементом A[i_r][j]
            
        for ii in range(i_r):  
            for jj in reversed(range(i_r, len(m[0]))):
                A[ii][jj] = A[ii][jj] - A[i_r][jj] * A[ii][i_r] #считаем новое значение элементов строк до i_r строки
            
        for ie in range(i_r + 1, len(m)):  
            for je in reversed(range(i_r, len(m[0]))):
                A[ie][je] = A[ie][je] - A[i_r][je] * A[ie][i_r] ##считаем новое значение элементов строк после i_r строки
                
        i_r += 1
              
    if invr == False:
        
        print('Прямая матрица коэффицентов.')
        print_mtx(a)
        print()
        print('Обратная матрица коэффицентов.')
        print_mtx(invr_mtx_gauss_ratio(a, f))
        print()
        
        X = []
        for el in range(len(A)):
            X.append(A[el][len(A[0]) - 1])
            if f == False:
                print(f'x{el + 1} = {A[el][len(A[0]) - 1].numerator / A[el][len(A[0]) - 1].denominator}')
            else:
                print(f'x{el + 1} = {A[el][len(A[0]) - 1]}')
        print()
        return [a, invr_mtx_gauss_ratio(a, f), X]
        
    else:
        if invr == True:
            for ind in range(len(m)):
                for num in reversed(range(len(m[0]) // 2)): 
                    A[ind].pop(num) #удаляем единичную матрицу, чтобы осталась только обратная
            if f == False:
                res = []
                for i1 in range(len(A)): 
                    res.append([j1.numerator / j1.denominator for j1 in A[i1]])
                return res
            else:
                res1 = []
                for i2 in range(len(A)): 
                    res1.append([j2.limit_denominator(10000) for j2 in A[i2]])
                return res1


def Correct(a):
    """
    Проверка матрицы коэффициентов на корректность
    a - матрица
    """
    for row in range(0, len(a)):
        if( len(a[row]) != len(b) ):
            print('Не соответствует размерность')
            return False
    
    for row in range(0, len(a)):
        if( a[row][row] == 0 ):
            print('Нулевые элементы на главной диагонали')
            return False
    return True
 
def check_in(x_old, x_new):
    """
    условие завершения итераций по Евклидовой метрике(расстояние между точками)
    """
    eps = 0.0001
    sum_up = 0
    sum_low = 0
    for k in range(0, len(x_old)):
        sum_up += ( x_new[k] - x_old[k] ) ** 2
        sum_low += ( x_new[k] ) ** 2
        
    return math.sqrt( sum_up / sum_low ) < eps
 
# Процедура решения
def yakobi(a, b):
    """
    Функция решает СЛАУ методом простых итерация Якоби.
    Для работы функции необходима ф-ия Correct,check_in, 
    a: list - матрица коэффицентов перед x.
    b: list - матрица свободных членов.
    """
    if( not Correct(a) ):
        print('Ошибка в исходных данных')
    else:
        count = len(b) # количество корней
        
        x = [1 for k in range(0, count) ] # начальное приближение корней
        
        numberOfIter = 0  # подсчет количества итераций
        MAX_ITER = 100    # максимально допустимое число итераций
        while( numberOfIter < MAX_ITER ):
 
            x_prev = copy.deepcopy(x)
            
            for k in range(0, count):
                S = 0
                for j in range(0, count):
                    if( j != k ): S = S + a[k][j] * x[j] 
                x[k] = b[k][0] / a[k][k] - S / a[k][k]
            
            if check_in(x_prev, x) : # проверка на выход
                break
           
            numberOfIter += 1
            
        print('Количество итераций на решение: ', numberOfIter)
        
        return x    
    
def Norma(mtx):
    """
    Функция находит норму матрицы mtx, которая нужны для определения числа обусловленности.
    mtx: list - матрица mtx.
    """
    h = []
    for i in range(len(mtx)):
        row_abs = [abs(mtx[i][j]) for j in range(len(mtx[0]))]
        h.append(sum(row_abs))
    norm = max(h)
    return norm

def cond_mtx(mtx):
    """
    Функция считает число обусловленности матрицы mtx.
    Для работы функции необходима функция Norma(mtx).
    mtx: list - матрица mtx.
    """
    return Norma(mtx) * Norma(invr_mtx_gauss(mtx))

def lagr(x, y, t):
    z = 0
    for i in range(len(y)):
        p1, p2 = 1, 1
        for j in range(len(x)):
            if i != j:
                p1 *= t - x[j]
                p2 *= x[i] - x[j]
        z += y[i] * p1 / p2
    return z


def lagrange(x,y):
    """Интерполяция по Лагранжу
      x-список x
      y-список y
    """
    xi = np.linspace(np.min(x),np.max(x),100)
    y = [lagr(x, y, i) for i in xi]
    return xi,y


def normal_d(x , mu , sig):
    prob_density = (np.pi*sig) * np.exp(-0.5*((x-mu)/sig)**2)
    return prob_density

def normal_dist(x,y):
    """ Аппроксимация по наормальному распределению
      x-список x
      y-список y
    """
    popt,pcov = curve_fit(normal_d,x,y)

    x_new = np.linspace(np.min(x),np.max(x),100)
    y_new = normal_d(x_new,popt[0],popt[1])
    return x_new,y_new
def Newton(x,y):
    """ Интерполяция по Ньютону
      x-список x
      y-список y
    """
    x=np.array(x,float)
    y=np.array(y,float)
    n=len(x)
    p=np.zeros([n,n+1])#создание таблицы (n до n+1 array)
    value =float(3)
# первые два столбца таблицы заполнены точками данных x и y
    for i in range(n):
            p[i,0]=x[i]
            p[i,1]=y[i]

## алгоритм для таблицы        
    for i in range(2,n+1): #столбец
        for j in range(n+1-i):# определяет строку
            p[j,i]=(p[j+1,i-1]-p[j,i-1])/(x[j+i-1]-x[j])
    np.set_printoptions(suppress=True)## возвращает значения в обычных цифрах
    
     ##print(p) можете проверить полную таблицу 
    b=p[0][1:]#Этот вектор содержит неизвестные коэффициенты полинома, которые являются верхними элементами каждого столбца. 
    
    lst=[] # список, куда мы добавим значения (x-x0), (x-x0)(x-x1), (x-x0)(x-x1)(x-x2)...

    t=1
    for i in range(len(x)):
        t*=(value-x[i]) ##(x-x0), (x-x0)(x-x1), (x-x0)(x-x1)(x-x2)...
        lst.append(t)
    c = lst[::-1]

    f=b[0]
    for k in range(1,len(b)):
        f+=b[k]*lst[k-1] ## важно  использовать [k-1] ,а  не  k потому что  в списке мы используем элемент на один шаг раньше.

    lst_x_new = np.linspace(np.min(x),np.max(x),n)
    lst_y_new = (y)
    lst_x = x
    lst_y = y

    return lst_x_new,lst_y_new

def Aprlin(x,y):
    """  Линейная аппроксимация
        x-список x
        y-список y
    """
    h=np.array(x,float)
    g=np.array(y,float)
    a11 = sum([i**2 for i in x])
    a12 = sum([i for i in x])
    a13 = h*g
    a13 = sum(a13)
    a21 = sum([i for i in x])
    a22 = len(x)
    a23 = sum([i for i in y])
    mat = [[a11,a12],[a21,a22]]
    koefgord = [[a13],[a23]]
    koeflin = dgordan_gauss(mat,koefgord,False)
    c1 = koeflin[0]
    c0 = koeflin[1]
    rez = ''
    if c0 != 0:
        rez += f'{c1}*x'
        if c1 != 0:
            rez += f'+{c0}'
    else:
        if c1 != 0:
            rez += f'{c0}'
    
    
    lst_x = x
    lst_y = y
    
    g = []
    for i in lst_x:
        g.append(c1*i+(c0))

    lst1 = lst_x, lst_y
    vmeste_xyf = []
    for i in range(len(lst_x)):
        x = lst_x[i]
        vmeste_xyf.append([lst_x[i],lst_y[i],g[i]])
  
    disp = sum([(i[1]-i[2])**2 for i in vmeste_xyf])

    lst_x_new = np.linspace(np.min(lst_x),np.max(lst_x),100)
    lst_y_new = [eval(rez) for x in lst_x_new]

    return lst_x_new,lst_y_new

def chebyshev_nodes(a, b, n):
    i = np.array(range(n))
    x = np.cos((2*i+1)*pi/(2*(n)))
    return 0.5*(b-a)*x+0.5*(b+a)

def cardinal(xdata, x):
    n=len(xdata)
    l = []
    for i in range(n):
        li = np.ones(len(x))
        for j in range(n):
            if i is not j:
                li = li*(x-xdata[j])/(xdata[i]-xdata[j])
        l.append(li)
    return l

def lagrang(ydata, l):
    poly = 0
    for i in range(len(ydata)):
        poly = poly + ydata[i]*l[i]
    return poly

def lagrange_cheb(x,y):
    """ Интерполяция по Лагранжу-Чебышеву 
      x-список x
      y-список y
    """
    xdata = chebyshev_nodes(np.min(x), np.max(x), len(y)) 
    l =cardinal(xdata, x)
    p =lagrang(y, l)          
    return x,p

#интерполяция кубическим сплайном-------------------------------------------------------------------------------------------



#разность между переменной и значением узла справа
def compute_changes(x: List[float]):
    return [x[i+1] - x[i] for i in range(len(x) - 1)]

#создание трёхдиагональной матрицы
def create_tridiagonalmatrix(n: int, h: List[float]):
    A = [h[i] / (h[i] + h[i + 1]) for i in range(n - 2)] + [0]
    B = [2] * n
    C = [0] + [h[i + 1] / (h[i] + h[i + 1]) for i in range(n - 2)]
    return A, B, C

#правая часть уравнения решения трёхдиагональной матрицы
def create_target(n: int, h: List[float], y: List[float]):
    return [0] + [6 * ((y[i + 1] - y[i]) / h[i] - (y[i] - y[i - 1]) / h[i - 1]) / (h[i] + h[i-1]) for i in range(1, n - 1)] + [0]

#решение трёхдиагональной матрицы
def solve_tridiagonalsystem(A: List[float], B: List[float], C: List[float], D: List[float]):
    c_p = C + [0]
    d_p = [0] * len(B)
    X = [0] * len(B)

    c_p[0] = C[0] / B[0]
    d_p[0] = D[0] / B[0]
    for i in range(1, len(B)):
        c_p[i] = c_p[i] / (B[i] - c_p[i - 1] * A[i - 1])
        d_p[i] = (D[i] - d_p[i - 1] * A[i - 1]) / (B[i] - c_p[i - 1] * A[i - 1])

    X[-1] = d_p[-1]
    for i in range(len(B) - 2, -1, -1):
        X[i] = d_p[i] - c_p[i] * X[i + 1]

    return X

#считаем сплайн
def compute_spline(x: List[float], y: List[float]):
    """ Интерполяция кубическим сплайном 
      x-список x
      y-список y
    """
    n = len(x)
    if n < 3:
        raise ValueError('Too short an array')
    if n != len(y):
        raise ValueError('Array lengths are different')

    h = compute_changes(x)
    if any(v < 0 for v in h):
        raise ValueError('X must be strictly increasing')

    A, B, C = create_tridiagonalmatrix(n, h)
    D = create_target(n, h, y)

    M = solve_tridiagonalsystem(A, B, C, D)

    coefficients = [[(M[i+1]-M[i])*h[i]*h[i]/6, M[i]*h[i]*h[i]/2, (y[i+1] - y[i] - (M[i+1]+2*M[i])*h[i]*h[i]/6), y[i]] for i in range(n-1)]

    def spline(val):
            idx = min(bisect.bisect(x, val)-1, n-2)
            z = (val - x[idx]) / h[idx]
            C = coefficients[idx]
            return (((C[0] * z) + C[1]) * z + C[2]) * z + C[3]
    return spline


#квадратичная аппроксимация-------------------------------------------------------------------------------------------------

def quadratic_shit(xi, yi):
    
    a = []
    for i in range(3):
        a.append([sum(list(map(lambda x: x ** (j-i), xi))) if j-i != 0  else len(xi) for j in reversed(range(2,5))])

    
    b = []
    for ii in range(1,4):
        b.append([sum(list(map(lambda x,y: x ** (3-ii) * y, xi, yi)))])

    
    sol = dgordan_gauss(a, b, invr = False)
    
    return(sol)
    
def sweet_funk_of_mine(data_x, xi, yi): 
    """ Квадротичная аппроксимация 
      x-список x
      y-список y
    """
    sol = quadratic_shit(xi, yi)
    return data_x, list(map(lambda x: sol[0] * x**2 + sol[1] * x + sol[2], data_x))

def show_6_appint(data):
    """ Вывод графиков аппроксимация и интерполяций, на вход подается массив списков x y 
      data - массив x y
    """
    xi = np.array(data[0])

    yi = np.array(data[1])

    newton_inter = Newton(xi,yi)
    lin_approx = Aprlin(xi,yi)
    lagrange_inter = lagrange(xi,yi)
    lagrange_inter2 = lagrange_cheb(xi,yi)
    normaldist_approx = normal_dist(xi,yi)
    spline = compute_spline(xi,yi)
    quadratic_fit = sweet_funk_of_mine(data_x = xi, xi = xi, yi = yi)

    plt.figure(figsize=(18, 10))
    plt.subplot(2, 4, 1)
    plt.plot(newton_inter[0],newton_inter[1],color='r' )
    plt.scatter(xi,yi)
    plt.legend([ 'интер.функ','данные точки'])
    plt.grid(True)
    plt.title("интерполяция методом Ньютона:")

    plt.subplot(2, 4, 2)
    plt.plot(lagrange_inter[0],lagrange_inter[1],color='r' )
    plt.scatter(xi,yi)
    plt.legend(['интер.функ','данные точки'])
    plt.grid(True)
    plt.title("интерполяция методом лагранжа:")

    plt.subplot(2, 4, 3)
    plt.plot(lagrange_inter2[0],lagrange_inter2[1],color='r' )
    plt.scatter(xi,yi)
    plt.legend(['интер.функ','данные точки'])
    plt.grid(True)
    plt.title("интерполяция методом лагранжа+чебышев:")

    plt.subplot(2, 4, 4)
    plt.plot(xi,[spline(ind) for ind in xi],color='r' )
    plt.scatter(xi,yi)
    plt.legend(['интер.функ','данные точки'])
    plt.grid(True)
    plt.title("Интерполяция кубическим сплайном")

    plt.subplot(2, 4, 5)
    plt.plot(quadratic_fit[0], quadratic_fit[1])
    plt.scatter(xi,yi)
    plt.legend(['аппрокс.функ','данные точки'])
    plt.grid(True)
    plt.title("Квадратичная аппроксимация")

    plt.subplot(2, 4, 6)
    plt.plot(lin_approx[0],lin_approx[1],color='r' )
    plt.scatter(xi,yi)
    plt.legend([ 'аппрокс.функ','данные точки'])
    plt.grid(True)
    plt.title("Линейная аппроксимация:")

    plt.subplot(2, 4, 7)
    plt.plot(normaldist_approx[0],normaldist_approx[1],color='r' )
    plt.scatter(xi,yi)
    plt.legend([ 'аппрокс.функ','данные точки'])
    plt.grid(True)
    plt.title("Аппроксимация нормальным распределением")
    plt.show()
    
def eiler_koshi(f,g,x0,xn,y0):
    """Решение однр дфф ур методом эйлера коши
    
    f - функция
    g - кол во итераций int
    x0 - нач условие 
    xn - правая граница исследуемого интервала 
    y0 - нач условия
    
    """
    h = (xn - x0)/g
    ilist = [i+1 for i in range(g)]
    xlist = [(x0+h*i) for i in ilist]
    ylist = []
    prev = y0
   
    print("Mетод Эйлера-Коши")
    for x in xlist:
        y_elr = prev + h*f(x,prev)
        y=prev+(h/2)*(f(x,prev)+f(x+h,y_elr))
        prev = y
        ylist.append(prev)

    print('i,   x[i],   y[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',ylist[i])

    plt.xlabel("ось абцисс")
    plt.ylabel("ось ординат")
    newton_inter = Newton(xlist, ylist)
    lin_approx = Aprlin(xlist, ylist)
    plt.plot(newton_inter[0],newton_inter[1],color='r',label="интерполяция методом Ньютона" )
    plt.plot(lin_approx[0],lin_approx[1],color='orange',label="линейная аппроксимируем" )
    plt.scatter(np.array(xlist), np.array(ylist), label="метод Эйлера-Коши")
    plt.legend()
    plt.grid()
    plt.show()
    return ylist


def eiler_koshi_sys(f1,f2,g,x0,xn,y0,z0):
    """Решение системы однр дфф ур методом эйлера коши
    
    f1 - функция 1
    f2 - функция 2
    g - кол во итераций int
    x0 - нач условие 
    xn - правая граница исследуемого интервала 
    y0 - нач условия
    z0 - нач условия
    
    """
    h = (xn - x0)/g
    ilist = [i+1 for i in range(g)]
    xlist = [(x0+h*i) for i in ilist]
    ylist = []
    zlist = []
    prev_y = y0
    prev_z = z0
   
    print("Mетод Эйлера-Коши")
    for x in xlist:
        y_elr = prev_y + h*f1(x,prev_y,prev_z)
        z_elr = prev_z + h*f2(x,prev_y,prev_z)
        
        y=prev_y+(h/2)*(f1(x,prev_y,prev_z)+f1(x+h,y_elr,z_elr))
        prev_y = y
        ylist.append(prev_y)
        
        z=prev_z+(h/2)*(f2(x,prev_y,prev_z)+f2(x+h,y_elr,z_elr))
        prev_z = z
        zlist.append(prev_z)

    print('i,   x[i],   y[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',ylist[i])
    print('')    
    print('i,   x[i],   z[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',zlist[i])
        
    plt.xlabel("ось абцисс")
    plt.ylabel("ось ординат")

    newton_interf1 = Newton(xlist, ylist)
    lin_approxf1 = Aprlin(xlist, ylist)
    newton_interf2 = Newton(xlist, zlist)
    lin_approxf2 = Aprlin(xlist, zlist)
    plt.plot(newton_interf1[0],newton_interf1[1],color='r',label="интерполяция методом Ньютона" )
    plt.plot(lin_approxf1[0],lin_approxf1[1],color='orange',label="линейная аппроксимируем" )

    plt.plot(newton_interf2[0],newton_interf2[1],color='r')
    plt.plot(lin_approxf2[0],lin_approxf2[1],color='orange')
    plt.scatter(np.array(xlist), np.array(ylist),color='black', label="метод Эйлера-Коши функция 1")
    plt.scatter(np.array(xlist), np.array(zlist),color='blue', label="метод Эйлера-Коши функция 2")
    plt.legend()
    plt.grid()
    plt.show()
    
    

def runge_kytti(f,g,x0,xn,y0):
    """Решение однр дфф ур методом Рунге-Кутты
    
    f - функция
    g - кол во итераций int
    x0 - нач условие 
    xn - правая граница исследуемого интервала 
    y0 - нач условия
    
    """
    h = (xn - x0)/g
    ilist = [i+1 for i in range(g)]
    xlist = [(x0+h*i) for i in ilist]
    ylist = []
    prev = y0
   
    print(" Mетод Рунге-Кутты")
    for x in xlist:
        k1=h*f(x,prev)
        k2=h*f(x+h/2,prev+k1/2)
        k3=h*f(x+h/2,prev+k2/2)
        k4=h*f(x+h,prev+k3)
        y=prev+1/6*(k1+2*k2+2*k3+k4)
        prev=y
        ylist.append(prev)
    print('i,   x[i],   y[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',ylist[i])
    plt.xlabel("ось абцисс")
    plt.ylabel("ось ординат")
    newton_inter = Newton(xlist, ylist)
    lin_approx = Aprlin(xlist, ylist)
    plt.plot(newton_inter[0],newton_inter[1],color='r',label="интерполяция методом Ньютона" )
    plt.plot(lin_approx[0],lin_approx[1],color='orange',label="линейная аппроксимируем" )
    plt.scatter(np.array(xlist), np.array(ylist), label="метод Рунге-Кутты")
    plt.legend()
    plt.grid()
    plt.show()
    return ylist


def runge_kytti_sys(f1,f2,g,x0,xn,y0,z0):
    """Решение системы однр дфф ур методом Рунге-Кутты
    
    f1 - функция 1
    f2 - функция 2
    g - кол во итераций int
    x0 - нач условие 
    xn - правая граница исследуемого интервала 
    y0 - нач условия
    z0 - нач условие
    
    """
    h = (xn - x0)/g
    ilist = [i+1 for i in range(g)]
    xlist = [(x0+h*i) for i in ilist]
    ylist = []
    zlist = []
    prev_y = y0
    prev_z = z0
   
    print("Mетод Рунге-Кутты")
    for x in xlist:
        k1=h*f1(x,prev_y,prev_z)
        l1=h*f2(x,prev_y,prev_z)
        
        k2=h*f1(x+h/2,prev_y+k1/2,prev_z+l1/2)
        l2=h*f2(x+h/2,prev_y+k1/2,prev_z+l1/2)
        
        k3=h*f1(x+h/2,prev_y+k2/2,prev_z+l2/2)
        l3=h*f2(x+h/2,prev_y+k2/2,prev_z+l2/2)
        
        k4=h*f1(x+h,prev_y+k3,prev_z+l3)
        l4=h*f2(x+h,prev_y+k3,prev_z+l3)
        
        y=prev_y+1/6*(k1+2*k2+2*k3+k4)
        prev_y=y
        ylist.append(prev_y)
        
        z=prev_z+1/6*(l1+2*l2+2*l3+l4)
        prev_z=z
        zlist.append(prev_z)
        
        
    print('i,   x[i],   y[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',ylist[i])
    print('')    
    print('i,   x[i],   z[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',zlist[i])

    plt.xlabel("ось абцисс")
    plt.ylabel("ось ординат")

    newton_interf1 = Newton(xlist, ylist)
    lin_approxf1 = Aprlin(xlist, ylist)
    newton_interf2 = Newton(xlist, zlist)
    lin_approxf2 = Aprlin(xlist, zlist)
    plt.plot(newton_interf1[0],newton_interf1[1],color='r',label="интерполяция методом Ньютона" )
    plt.plot(lin_approxf1[0],lin_approxf1[1],color='orange',label="линейная аппроксимируем" )

    plt.plot(newton_interf2[0],newton_interf2[1],color='r')
    plt.plot(lin_approxf2[0],lin_approxf2[1],color='orange')
    plt.scatter(np.array(xlist), np.array(ylist),color='black', label="метод Рунге-Кутты функция 1")
    plt.scatter(np.array(xlist), np.array(zlist),color='blue', label="метод Рунге-Кутты функция 2")
    plt.legend()
    plt.grid()
    plt.show()
    
    
    
    
def eiler(f,g,x0,xn,y0):
    """Решение однр дфф ур методом эйлера
    
    f - функция
    g - кол во итераций int
    x0 - нач условие 
    xn - правая граница исследуемого интервала 
    y0 - нач условия
    
    """
    h = (xn - x0)/g
    start_time = time.time()
    ilist = [i+1 for i in range(g)]
    xlist = [(x0+h*i) for i in ilist]
    ylist = []
    prev = y0
    print("Mетод Эйлера")
    for x in xlist:
        y = prev + h*f(x,prev)
        prev = y
        ylist.append(prev)
    print("--- %s seconds -(our)-" % (time.time() - start_time))
    print('i,   x[i],   y[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',ylist[i])
    plt.xlabel("ось абцисс")
    plt.ylabel("ось ординат")
    newton_inter = Newton(xlist, ylist)
    lin_approx = Aprlin(xlist, ylist)
    plt.plot(newton_inter[0],newton_inter[1],color='r',label="интерполяция методом Ньютона" )
    plt.plot(lin_approx[0],lin_approx[1],color='orange',label="линейная аппроксимируем" )
    plt.scatter(np.array(xlist), np.array(ylist), label="метод Эйлера")
    plt.legend()
    plt.grid()
    plt.show()
    return ylist
    

    
def eiler_sys(f1,f2,g,x0,xn,y0,z0):
    """Решение системы однр дфф ур методом эйлера
    
    f1 - функция 1
    f2 - функция 2
    g - кол во итераций int
    x0 - нач условие 
    xn - правая граница исследуемого интервала 
    y0 - нач условия
    z0 - нач условия
    
    """
    h = (xn - x0)/g
    ilist = [i+1 for i in range(g)]
    xlist = [(x0+h*i) for i in ilist]
    ylist = []
    zlist = []
    prev_y = y0
    prev_z = z0
    print("Mетод Эйлера")
    for x in xlist:
        y = prev_y + h*f1(x,prev_y,prev_z)
        prev_y = y
        ylist.append(prev_y)
        
        z = prev_z + h*f2(x,prev_y,prev_z)
        prev_z = z
        zlist.append(prev_z)

    print('i,   x[i],   y[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',ylist[i])
    print('')    
    print('i,   x[i],   z[i]')
    for i in range(len(ilist)):
        print(ilist[i],' ',xlist[i],' ',zlist[i])
    plt.xlabel("ось абцисс")
    plt.ylabel("ось ординат")
    newton_interf1 = Newton(xlist, ylist)
    lin_approxf1 = Aprlin(xlist, ylist)
    newton_interf2 = Newton(xlist, zlist)
    lin_approxf2 = Aprlin(xlist, zlist)
    plt.plot(newton_interf1[0],newton_interf1[1],color='r',label="интерполяция методом Ньютона" )
    plt.plot(lin_approxf1[0],lin_approxf1[1],color='orange',label="линейная аппроксимируем" )

    plt.plot(newton_interf2[0],newton_interf2[1],color='r')
    plt.plot(lin_approxf2[0],lin_approxf2[1],color='orange')
    plt.scatter(np.array(xlist), np.array(ylist),color='black', label="метод Эйлера функция 1")
    plt.scatter(np.array(xlist), np.array(zlist),color='blue', label="метод Эйлера функция 2")
    plt.legend()
    plt.grid()
    plt.show()



def otzhig(apa, bet):
    """ Решение задачи коммиваяжера алгоритмом отжига
       
       apa - значение alpha float
       beta - значение beta float
       
       
    """
    
    print(f'[1] - ввод матрицы с клавиатуры, любая цифра - выбор готовой матрицы из массива матриц d')  
    g = int(input(f'\nВы хотите сами ввести матрицу или ее выбрать готовую?' ))
    if g == 1:
        iteration = int(input('\nВведите кол-во итераций: ' ))
        c = int(input('\nВведите кол-во вершин(городов), это размерность матрицы: '))
        a = []
        for i in range(c):
                a.append([float(input(f'a{i+1}{j+1} = ')) for j in range(c)])
        a = np.array(a)
        print(a)
    else:
        iteration = int(input('\nВведите кол-во итераций: ' ))
        a = d[int(input('\nВведите номер матрицы, начиная с 0: '))]
        c = len(a)
        print(a)
    
    e = .5
    koef = 1/a
    koef[koef == inf ] = 0
    ideag = .1*np.ones((c,c))
    burning = np.ones((c,c+1))
    iter = 0
    for ite in range(iteration):
        iter = iter + 1
        burning[:,0] = 1
        for i in range(c):
            temp_koef = np.array(koef)         
            for j in range(c-1):
                bye = np.zeros(5)     
                problem = np.zeros(5)            
                space = int(burning[i,j]-1)      
                temp_koef[0:,space] = 0     
                p = np.power(ideag[space,:],bet)         
                v = np.power(temp_koef[space,:],apa)  
                p = p[0:,np.newaxis]                    
                v = v[0:,np.newaxis]                     
                bye = np.multiply(p,v)          
                sumi = np.sum(bye)                        
                apk = bye/sumi 
                problem = np.cumsum(apk)     
                l = np.random.random_sample()   
                c1 = np.nonzero(problem>l)[0][0]+1        
                burning[i,j+1] = c1              
        left = list(set([i for i in range(1,c+1)])-set(burning[i,:-2]))[0]     
        burning[i,-2] = left                   
    burning_option = np.array(burning)               
    value = np.zeros((c,1))             
    for i in range(c):
        h = 0
        for j in range(c-1):
            h = h + a[int(burning_option[i,j])-1,int(burning_option[i,j+1])-1]   
        value[i]=h
    min_position = np.argmin(value)            
    valueitog = value[min_position] 
    road = burning[min_position,:]  
    ideag = (1-e)*ideag
    for i in range(c):
        for j in range(c-1):
            dt = 1/value[i]
            ideag[int(burning_option[i,j])-1,int(burning_option[i,j+1])-1] = ideag[int(burning_option[i,j])-1,int(burning_option[i,j+1])-1] + dt  
    
    return  ("-".join([str(int(i)) for i in road]), int(valueitog[0]) + a[int(road[-2])-1,0])      
    

    
class AC(object): # создание "маравьиной колонии"
    def __init__(self, distances, pheromone, n_ants, n_best, max_iterations, decay, alpha=1, beta=1):
        """Класс муравьинная колония.
        
           distance - numpy матрица расстояний из float значений.
           pheromone - начальный феромон float
           n_ants - количество муравьев int
           n_best - количество элитных муравьев int
           max_iterations - количество иттераций int
           decay - испарение феромона float
           alpha - значение альфа float         
           beta -значение beta float
           
        """
        # избавляемся от нулей в матрице
        i = 0
        j = 0
        while i < len(distances):
            while j < len(distances):
                if distances[i][j] == 0:
                    distances[i][j] = np.inf
                    i += 1
                    j += 1
                else:
                    continue
        self.distances  = distances #матрица растояний
        self.pheromone = np.ones(self.distances.shape) * pheromone - np.eye(len(distances)) * pheromone # данные о феромонах
        self.all_inds = range(len(distances)) # список городов
        self.n_ants = n_ants # колличество муравьев
        self.n_best = n_best # колличество элитных муравьев
        self.n_iterations = max_iterations # колличество итераций
        self.decay = decay # испарения феромона
        self.alpha = alpha
        self.beta = beta

    def ACO(self): #оптимизация алгаритмом муравьиной колонии
        shortest_path = None
        all_time_shortest_path = ("placeholder", np.inf)
        paths = []
        for i in range(self.n_iterations):
            all_paths = self.gen_all_paths()
            self.spread_pheronome(all_paths, self.n_best, shortest_path=shortest_path)
            shortest_path = min(all_paths, key=lambda x: x[1])
            paths.append(shortest_path[1])
            if shortest_path[1] < all_time_shortest_path[1]:
                all_time_shortest_path = shortest_path
            self.pheromone * self.decay
        print("-".join([str(i[0] + 1) for i in shortest_path[0]] + list(str(shortest_path[0][len(shortest_path[0]) - 1][1] + 1))))

        return all_time_shortest_path, paths

    def spread_pheronome(self, all_paths, n_best, shortest_path):
        sorted_paths = sorted(all_paths, key=lambda x: x[1])
        for path, dist in sorted_paths[:n_best]:
            for move in path:
                self.pheromone[move] += 1.0 / self.distances[move]

    def gen_path_dist(self, path):
        total_dist = 0
        for ele in path:
            total_dist += self.distances[ele]
        return total_dist

    def gen_all_paths(self):
        all_paths = []
        for i in range(self.n_ants):
            path = self.gen_path(0)
            all_paths.append((path, self.gen_path_dist(path)))
        return all_paths

    def gen_path(self, start): #генерация пути
        path = []
        visited = set()
        visited.add(start)
        prev = start
        for i in range(len(self.distances) - 1):
            move = self.pick_move(self.pheromone[prev], self.distances[prev], visited)
            path.append((prev, move))
            prev = move
            visited.add(move)
        path.append((prev, start)) # возвращаемся туда, где мы начинали
        return path

    def pick_move(self, pheromone, dist, visited): #выбор следующего города
        pheromone = np.copy(pheromone)
        pheromone[list(visited)] = 0
        row = pheromone ** self.alpha * (( 1.0 / dist) ** self.beta)
        norm_row = row / row.sum()
        move = np_choice(self.all_inds, 1, p=norm_row)[0]
        return move