# all_4.ipynb
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import griddata
from matplotlib.animation import PillowWriter, FuncAnimation

# параметры рождаемости популяций
b_v = 8
b_u = 4

# параметры смертности популяций
d_v = 0.5
d_u = 0.2

# параметры конкуренции популяций
c_v = 1
c_u = 0.7

# радиус "близлежащей" области
D = 5

# "места силы" популяций (а также начальные центры областей популяций (радиус = 1))
x_kv = 70-1; y_kv = 70-1
x_ku = 30-1; y_ku = 30-1

# Интенсивности "мест силы"
I_v = 10
I_u = 8

# Вытянутости "мест силы"
sig_vx = 16; sig_vy = 9
sig_ux = 25; sig_uy = 4

# Размер территории (NxN)
N = 100

# Двойной интеграл для дискретного случая
def double_integral(func, x_start, x_end, y_start, y_end, V, U):
    integral = 0
    for x in range(max(0, x_start), min(N, x_end + 1)):
        for y in range(max(0, y_start), min(N, y_end + 1)):
            integral += func(x = x, y = y, V = V, U = U)
    return integral

# Функции Kv и Ku (домноженные на V(x,y,t) и U(x,y,t) соответственно)
def fun_Kv(x, y, V, U):
    return I_v * np.exp(-(x - x_kv)**2/(2*sig_vx**2) - (y - y_kv)**2/(2*sig_vy**2)) * V

def fun_Ku(x, y, V, U):
    return I_u * np.exp(-(x - x_ku)**2/(2*sig_ux**2) - (y - y_ku)**2/(2*sig_uy**2)) * U

# Функции V't и U't
def fun_Vt(x, y, V, U):
    return b_v * V - d_v * V**2 - c_u * V * U + double_integral(fun_Kv, x - D, x + D, y - D, y + D, V, U)

def fun_Ut(x, y, V, U):
    return b_u * U - d_u * U**2 - c_v * V * U + double_integral(fun_Ku, x - D, x + D, y - D, y + D, V, U)

# Метод Рунге-Кутты 4 порядка (по V)
def rk4_method_V(f, x, y, V, U, dt = 0.1):
    k1 = dt * f(x, y, V, U)
    k2 = dt * f(x, y, V + k1/2, U)
    k3 = dt * f(x, y, V + k2/2, U)
    k4 = dt * f(x, y, V + k3, U)
    n = f(x, y, V, U) + (k1 + k4 + 2*(k2 + k3))/6
    return n

# Метод Рунге-Кутты 4 порядка (по U)
def rk4_method_U(f, x, y, V, U, dt = 0.1):
    k1 = dt * f(x, y, V, U)
    k2 = dt * f(x, y, V, U + k1/2)
    k3 = dt * f(x, y, V, U + k2/2)
    k4 = dt * f(x, y, V, U + k3)
    n = f(x, y, V, U) + (k1 + k4 + 2*(k2 + k3))/6
    return n

# Функция обновления популяций
def update_data(data):
    for x in range(N):
        for y in range(N):
            V = Data[0][x][y][0]
            U = Data[1][x][y][0]
            data[0][x][y][1] = min(max(rk4_method_V(f = fun_Vt, x = x, y = y, V = V, U = U), 0), 12)
            data[1][x][y][1] = min(max(rk4_method_U(f = fun_Ut, x = x, y = y, V = V, U = U), 0), 15)
    for x in range(N):
        for y in range(N):
            data[0][x][y][0] = data[0][x][y][1]
            data[1][x][y][0] = data[1][x][y][1]
    return data

# Сетка
Data = [[[[0, 0] for j in range(N)] for i in range(N)], # список значений популяции V
        [[[0, 0] for j in range(N)] for i in range(N)]] # список значений популяции U

# Заселили популяцию V
Data[0][x_ku][y_ku][0] = 5
Data[0][x_ku-1][y_ku][0] = 2
Data[0][x_ku][y_ku-1][0] = 2
Data[0][x_ku+1][y_ku][0] = 2
Data[0][x_ku][y_ku+1][0] = 2

# Заселили популяцию U
Data[1][x_kv][y_kv][0] = 4
Data[1][x_kv-1][y_kv][0] = 3
Data[1][x_kv][y_kv-1][0] = 3
Data[1][x_kv+1][y_kv][0] = 3
Data[1][x_kv][y_kv+1][0] = 3

# Анимация
fig, ax = plt.subplots(figsize=(10, 6))

def animate(frame):
    global Data

    ax.clear() 
    
    # Все точки на сетке с популяциями V и U (class_1 и class_2)
    class_1 = [[i, j, Data[0][i][j][0]] for i in range(N) for j in range(N) if Data[0][i][j][0] != 0]
    class_2 = [[i, j, Data[1][i][j][0]] for i in range(N) for j in range(N) if Data[1][i][j][0] != 0]
    class_1 = np.array(class_1)
    class_2 = np.array(class_2)

    # Рассчёт градиентов для визуализации
    xi, yi = np.arange(N), np.arange(N)
    xi, yi = np.meshgrid(xi, yi)
    if class_1.size > 0:
        zi_class_1 = griddata((class_1[:, 0], class_1[:, 1]), class_1[:, 2], (xi, yi), method='cubic')
        contour_1 = ax.contourf(xi, yi, zi_class_1, levels=50, cmap='plasma', alpha=0.5)
    if class_2.size > 0:
        zi_class_2 = griddata((class_2[:, 0], class_2[:, 1]), class_2[:, 2], (xi, yi), method='cubic')
        contour_2 = ax.contourf(xi, yi, zi_class_2, levels=50, cmap='inferno', alpha=0.5)

    # Итоговый фрейм
    ax.set_title(f'Step {frame}')
    ax.set_xlabel('X')
    ax.set_ylabel('Y')

    # Обновление данных
    Data = update_data(Data)

ani = FuncAnimation(fig, animate, frames=20, repeat=False)
ani.save('population_dynamics.gif', writer=PillowWriter(fps=2))
plt.show()


# electricity_consumption_analysis.ipynb
# Markdown:
# Трофимов Михаил, ПМ22-1, ДЗ-6
# Markdown:
# Markdown:
https://cloud.mail.ru/public/mnqr/wCE6wkzXj
# Markdown:
# Реализация
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
# Выводим датасет
data = pd.read_csv('Electricity_consumption.csv', sep = ';', decimal = ',')
data = data[data.columns[1:]]
data.head()
# Заводим колонку часов - это основной критерий, от которого меняется потребление электроэнергии
data['hour'] = np.arange(len(data)) % 24
data.head()
data.dtypes # Нужные столбцы в нужных типах
data.isnull().sum() # Нет пропущенных значений
# Переводим часы в dummy-переменные
data = pd.get_dummies(data, columns = ['hour'])
data.head()
# Делим датасет на целевую переменную и регрессоры
X, y = data[data.columns[2:]], data[['demand_kW']]
# Стандартизируем регрессоры
scaler = StandardScaler().fit(X)
X = scaler.transform(X)
# Распределение целевой переменной
y.plot(title = 'Потребляемая мощность (кВт)')
plt.grid(True)
plt.show()
# Частотная спектрограмма потребляемой мощности
Y = np.fft.fft(y.to_numpy().flatten())
freq = np.fft.fftfreq(len(Y))
plt.plot(abs(freq), abs(Y))
plt.title('Частотная спектрограмма - demand_kW')
plt.show()
print(np.quantile(abs(Y), 0.95)) # выберем в качестве threshold амплитуду 25000
threshold = 25000
# Удаляем сезонные компоненты
Y[Y < threshold] = 0
# Выполняем обратное преобразование
demand_kW = np.real(np.fft.ifft(Y))
plt.title('Очищенный demand_kW')
plt.plot(demand_kW)
plt.grid(True)
plt.show()
# Строим модель множественной регрессии
model = LinearRegression().fit(X, demand_kW)
y_pred = model.predict(X)
y_pred
# Качество построенной модели
from sklearn.metrics import r2_score
r2_score(demand_kW, y_pred)
plt.plot(demand_kW, label = 'Десезионированный ряд')
plt.plot(y_pred, label = 'Предсказания модели')
plt.grid(True)
plt.legend()
plt.show()
# Считаем остатки (ошибки)
errors = demand_kW - y_pred # остатки
e2 = pd.DataFrame(errors).rolling(window = 5).mean().to_numpy().flatten() # сглаженные
# Приводим сглаженные остатки и исходный ряд к единому масштабу (от 0 до 1)
y_norm = y.to_numpy().flatten() / max(y.to_numpy())
errors_norm = (e2 + abs(min(e2[4:]))) / max(e2[4:] + abs(min(e2[4:])))
plt.title('Исходный и десезонированный ряды')
plt.plot(y.to_numpy().flatten(), label = 'Исходник')
plt.plot(demand_kW, label = 'Десезионированный ряд')
plt.grid(True)
plt.legend()
plt.show()
plt.title('Остатки')
plt.plot(errors, label = 'Остатки')
plt.plot(e2, label = 'Сглаженные остатки')
plt.grid(True)
plt.legend()
plt.show()
plt.title('Исходный ряд и сглаженные остатки: единый масштаб')
plt.plot(y_norm, label = 'Ряд')
plt.plot(errors_norm, label = 'Остатки')
plt.grid(True)
plt.legend()
plt.show()
# Markdown:
**Отчёт:**  
Модель множественной линейной регрессии не является оптимальной для описания изменения потребления электроэнергии  
Природа изменения тренда больше всего зависит от времени суток - ночью потребление меньше, чем к 15-16 часам  
Если следовать "логике времени", спрос **в дальнейшем будет увеличиваться** - крайний час в датасете это 4 часа ночи
# Потребление электроэнергии от времени (для отчёта)
y[:100].plot()
plt.show()

# immune_response_phase_portraits.ipynb
# Markdown:
# Трофимов Михаил, ПМ22-1, ДЗ-3
![Снимок экрана 2024-11-08 в 19.53.43.png](attachment:23427026-a62f-49e1-86a8-30d57db66080.png)
# Markdown:
# Уравнения и пример задания параметров
![Снимок экрана 2024-11-08 в 19.56.27.png](attachment:719886c4-834c-483e-83c8-3b3cbc7c971a.png)
![Снимок экрана 2024-11-08 в 19.56.34.png](attachment:20d577d8-cfbe-40cd-934a-96cc8aca3270.png)
![Снимок экрана 2024-11-08 в 19.56.39.png](attachment:464583ff-7d3f-4c19-bac0-3af277590017.png)
![Снимок экрана 2024-11-08 в 19.56.49.png](attachment:b51c9124-b9be-4fab-95be-f201f94c146d.png)
![Снимок экрана 2024-11-08 в 19.56.56.png](attachment:ae1c8688-d4f7-4432-be2f-839ef8637597.png)
# Markdown:
# Код
import matplotlib.pyplot as plt
from random import random # для задания исходных точек на сетке

def phase_portraits(beta, gamma, alpha, mu_c, C_star, mu_f, ro, nu, sigma, mu_m, t_lim, 
                    teta = 1, # время, в течение которого осуществляется формирование каскада плазматических клеток 
                    # (по умолчанию 1 для простоты задания начальных условий и работы кода)
                    h = 0.05, # шаг по Эйлеру
                    dots = 100): # Число исходных точек на сетке

    # заводим общие массивы зависимых от времени значений
    V_ = []
    C_ = []
    F_ = []
    m_ = []

    for i in range(dots):

        # заводим массивы зависимых от времени значений, добавляем начальные условия модели (в УСЛОВНЫХ значениях)
        V = [random()]
        C = [random()]
        F = [random()]
        m = [0.2] # пусть изначальная степень поражения органа всегда будет на уровне 0,2

        for t in range(t_lim):

            # изменение числа антигенов (вирусов) в организме
            dV = beta * V[t] - gamma * F[t] * V[t]
            # рост плазматических клеток
            dC = alpha * F[t - teta] * V[t - teta] - mu_c * (C[t] - C_star)
            # изменение числа антител
            dF = ro * C[t] - mu_f * F[t] - nu * gamma * V[t] * F[t]
            # характеристика поражения органа-мишени
            dm = sigma * V[t] - mu_m * m[t]

            # Фиксируем изменения V, C, F, m
            V.append(V[t] + h * dV)
            C.append(C[t] + h * dC)
            F.append(F[t] + h * dF)
            m.append(m[t] + h * dm)

        # добавляем изолинии в общий массив
        V_.append(V)
        F_.append(F)
        C_.append(C)
        m_.append(m)

    # выводим фазовые портреты и график динамики m от времени
    fig, ax = plt.subplots(3, 1, figsize = (10, 20))

    for i in range(dots):

        V = V_[i]
        F = F_[i]
        C = C_[i]
        m = m_[i]

        ax[0].set_title('Фазовый портрет FV')
        ax[0].set_xlabel('V - число вирусов')
        ax[0].set_ylabel('F - число антител')
        ax[0].plot(V, F, 'c')
        ax[0].grid(True)

        ax[1].set_title('Фазовый портрет CV')
        ax[1].set_xlabel('V - число вирусов')
        ax[1].set_ylabel('C - число плазматических клеток')
        ax[1].plot(V, C, 'c')
        ax[1].grid(True)

        ax[2].set_title('Динамика m от времени')
        ax[2].plot(m, 'c')
        ax[2].grid(True)
phase_portraits(beta = 1, # коэффициент размножения антигенов
                gamma = 0.8, # коэффициент, связанный с вероятностью нейтрализации антигена антителами при встречи с ним
                
                alpha = 5, # коэффициент, учитывающий вероятность встречи антиген-антитело,
                # возбуждение каскадной реакции и скорость образования новых клеток
                
                mu_c = 0.5, # коэффициент, равный обратной величине времени жизни плазматических клеток
                C_star = 1, # постоянный уровень плазматических клеток в здоровом организме
                mu_f = 0.2, # скорость производства антител одной плазматической клеткой
                ro = 0.17, # коэффициент, обратно пропорциональный времени распада антител
                nu = 10, # коэффициент, связанный с уменьшением антител за счет связи с антителами
                sigma = 1, # некоторая постоянная, своя для каждого заболевания (в связи с V характеризует степень поражения органа)
                mu_m = 0.2, # коэффициент восстановления организма
                t_lim = 10) # время наблюдения
# Markdown:
При заданных мной коэффициентах поражённый орган пациента со временем наверняка откажет (m стремится к 1, F к нулю)  
Однако в малом числе случаев поражённый орган "идёт на поправку", пожелаем нашему мнимому пациенту удачи :З

# qr_decomposition_and_eigenvalues.ipynb
# Markdown:
# Трофимов Михаил, ПМ22-1, ДЗ-2
![Снимок экрана 2024-10-16 в 23.06.55.png](attachment:43cccf3e-571b-43b0-b3f3-e9df9490a6a6.png)
# алгоритм перемножения двух матриц
def matmult(a, b):
    n = len(a)
    k = len(a[0])
    m = len(b[0])
    c = [[0 for i in range(m)] for j in range(n)]
    for i in range(n):
        for j in range(m):
            for s in range(k):
                c[i][j] += a[i][s] * b[s][j]
    return c
# алгоритм транспонирования матрицы
def transpose(matrix):
    n = len(matrix)
    k = len(matrix[0])
    return_matrix = [[0 for i in range(n)] for j in range(k)]
    for i in range(n):
        for j in range(k):
            return_matrix[j][i] = matrix[i][j]
    return return_matrix
# скалярное произведение векторов
def proj(a, b):
    return sum([a[i] * b[i] for i in range(len(a))])
# реализуем алгоритм qr-разложения с помощью процедуры Грама-Шмидта
def qr_decomposition(A, rounding = 2):
    # найдём векторы-столбцы исходной матрицы
    n = len(A)
    v = transpose(A)

    # чтобы в дальнейшем найти векторы-столбцы матрицы Q, найдём промежуточные векторы u
    u = [v[0]]
    for i in range(n-1):
        v_ = v[i+1]
        u_new = v[i+1]
        for j in range(i+1):
            scale = proj(u[j], v_) / proj(u[j], u[j])
            for r in range(len(u_new)):
                u_new[r] -= scale * u[j][r]
        u.append(u_new)

    # найдём векторы-столбцы матрицы Q
    e = []
    for i in range(n):
        e_ = u[i]
        distance = sum([j**2 for j in e_])**0.5
        for j in range(len(e_)):
            e_[j] /= distance
        e.append(e_)

    # находим матрицу Q
    Q = transpose(e)
    
    # находим матрицу R
    R = matmult(e, A)

    # округляем значения матриц до адекватного числа знаков
    for i in range(n):
        for j in range(n):
            Q[i][j] = round(Q[i][j], rounding)
            R[i][j] = round(R[i][j], rounding)

    return Q, R
# реализация стандартного qr-алгоритма
def qr_algorithm(A, steps, rounding = 2):
    Ak = A
    for i in range(steps):
        Qk, Rk = qr_decomposition(Ak)
        Ak = matmult(Rk, Qk)
    n = len(A)
    for i in range(n):
        for j in range(n):
            Ak[i][j] = round(Ak[i][j], rounding)
        print(Ak[i])
    return Ak
A = [[1, 2, 4],
     [3, 3, 2],
     [4, 1, 3]]
qr_algorithm(A, steps = 20) # к 20 шагу значение A[2][1] перестало меняться
# реализация неявного qr-алгоритма

import random
def sk():
    return random.random()

def qr_algorithm_implicit(A, steps, rounding = 2):
    n = len(A)
    Ak = A

    for i in range(steps):

        sk_ = sk()
        for j in range(n): # вычли переменную сдвига из главной диагонали A
            Ak[j][j] -= sk_
        Qk, Rk = qr_decomposition(Ak)

        Ak = matmult(Rk, Qk)
        for j in range(n): # добавили переменную сдвига к главной диагонали A
            Ak[j][j] += sk_
        
    for i in range(n):
        for j in range(n):
            Ak[i][j] = round(Ak[i][j], rounding)
        print(Ak[i])
    return Ak
# сравнение результатов
A = [[1, 2, 4],
     [3, 3, 2],
     [4, 1, 3]]
qr_algorithm_implicit(A, steps = 10) 
# к 10 шагу матрица A стала верхнетреугольной - добавление sk привело к более быстрой сходимости
# Markdown:
В файле **metod_Yakobi_.py** располагаются все функции из Трофимов_Михаил_ПМ22-1_ДЗ1 - <br>
сравним ответы матрицы A и её же после обработки qr-алгоритмами
import metod_Yakobi_ as my
A = [[1, 2, 4],
     [3, 3, 2],
     [4, 1, 3]]
my.metod_Yakobi(qr_algorithm(A, steps = 20))
A = [[1, 2, 4],
     [3, 3, 2],
     [4, 1, 3]]
my.metod_Yakobi(qr_algorithm_implicit(A, steps = 10))
A = [[1, 2, 4],
     [3, 3, 2],
     [4, 1, 3]]
my.metod_Yakobi(A)
# Markdown:
*Как мы можем наблюдать, ответы близки друг к другу*

# sound_processing_dft_filtering.ipynb
# Markdown:
# Трофимов Михаил, ПМ22-1, ДЗ-5
# Markdown:
# Markdown:
https://cloud.mail.ru/public/LVfj/bwpMydCv7
# Markdown:
# Реализация
# Markdown:
**Примечание:** размер аудиофайла понижен в ~8 раз, чтобы размер html не превышал 25 мб (ограничения gmail) и рассчёты шли быстрее
import scipy.io.wavfile as siowav
import numpy as np
import IPython.display as ipd
import matplotlib.pyplot as plt
from tqdm import tqdm
# Загружаем файл и читаем частоту дискретизации
sr, sound = siowav.read('test_sound.wav')
sound = sound.astype('float32')
print(sr, sound.shape)
# Смотрим на канал sound - он моно (можно понять и по sound.shape)
sound
sound = sound / np.max(sound)
# Выводим чистое аудио
plt.plot(sound[:1000000])
plt.show()
ipd.Audio(sound[:1000000], rate = sr)
# Добавляем шум
corrupt_sound = sound + 0.1 * np.random.randn(sound.shape[0])
corrupt_sound
corrupt_sound = corrupt_sound / np.max(np.abs(corrupt_sound))
# Выводим аудио с шумом
plt.plot(corrupt_sound[:1000000])
plt.show()
ipd.Audio(corrupt_sound[:1000000], rate = sr)
# Функция Дискретного Преобразования Фурье с семинара
def DFT(x):
    N = len(x)
    n = np.arange(N)
    k = n.reshape((N, 1))
    e = np.exp(-2j * np.pi * k * n / N)
    X = np.dot(e, x)
    return X
# Реализация обратного DFT
def IDFT(X):
    N = len(X)
    k = np.arange(N)
    n = k.reshape((N, 1))
    e = np.exp(2j * np.pi * k * n / N)
    X = np.dot(e, X) / N
    return X
def cleaning_sound(audio, # Наше аудио с шумом
                   sr, # Частота дискретизации
                   n, # Количество фрагментов, на которые мы делим аудио для обработки
                   threshold): # Порог для высокочастотных гармоник (шума)

    # Массив очищенного аудио и дискретизация каждого из фрагментов
    clean_sound = np.array([])
    discretization = int(len(audio) / n)

    for i in tqdm(range(n)):
        # Выделяем текущий фрагмент для обработки
        if i == n-1:
            start = discretization * i
            fin = len(audio)
            part_sound = audio[start:fin]
        else:
            start = discretization * i
            fin = discretization * (i+1)
            part_sound = audio[start:fin]

        # Преобразование аудио в частоты
        dft_p_sound = DFT(part_sound)
        frequencies = np.arange(discretization) * sr / discretization

        # Фильтр
        dft_p_sound[np.abs(frequencies) > threshold] = 0

        # Восстановление аудио из частот
        filtered_p_sound = np.real(IDFT(dft_p_sound))

        # Добавляем отфильтрованное аудио в Массив очищенного аудио
        clean_sound = np.concatenate((clean_sound, filtered_p_sound))

    # Выводим чистое аудио
    plt.plot(clean_sound)
    plt.show()
    return ipd.Audio(clean_sound, rate = sr)
cleaning_sound(corrupt_sound[:1000000], sr, 1000, 1200)
# Получился звук, будто пластинка движется по граммофону


# taylor_series_and_jacobi_method.ipynb
# Markdown:
# Трофимов Михаил, ПМ22-1, ДЗ-1
![%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-10-03%20%D0%B2%2014.29.16.png](attachment:%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-10-03%20%D0%B2%2014.29.16.png)
# Markdown:
**Алгоритм:**
![%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-10-03%20%D0%B2%2014.40.27.png](attachment:%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-10-03%20%D0%B2%2014.40.27.png)
# Markdown:
![%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-10-03%20%D0%B2%2014.40.50.png](attachment:%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-10-03%20%D0%B2%2014.40.50.png)
# алгоритмы нахождения синуса, косинуса и тангенса через ряды Тейлора + факториал

def factorial(x):
    value = 1
    for i in range(1, x+1):
        value *= i
    return value

def another_sin(x, iter = 10, pi = 3.141592653589793):
    # от -2*pi до 2*pi значения another_sin совпадают с math.sin, 
    # дальше - нет, воспользовался периодичностью функций
    if x > 2*pi:
        while x > 2*pi:
            x -= 2*pi
    if x < -2*pi:
        while x < -2*pi:
            x += 2*pi
    value = 0
    for i in range(iter):
        value += (-1)**i * x**(2*i+1) / factorial(2*i+1)
    return value

def another_cos(x, iter = 10, pi = 3.141592653589793):
    if x > 2*pi:
        while x > 2*pi:
            x -= 2*pi
    if x < -2*pi:
        while x < -2*pi:
            x += 2*pi
    value = 0
    for i in range(iter):
        value += (-1)**i * x**(2*i) / factorial(2*i)
    return value

def another_atan(x, iter = 10000, pi = 3.141592653589793):
    if abs(x) <= 1:
        value = 0
        for i in range(1, iter+1):
            value += (-1)**(i-1) * x**(2*i-1) / (2*i-1)
    else:
        value = pi/2 * abs(x)/x
        for i in range(1, iter+1):
            value -= (-1)**(i-1) * x**(-2*i+1) / (2*i-1)
    return value
# алгоритм перемножения двух матриц
def matmult(a, b):
    n = len(a)
    k = len(a[0])
    m = len(b[0])
    c = [[0 for i in range(m)] for j in range(n)]
    for i in range(n):
        for j in range(m):
            for s in range(k):
                c[i][j] += a[i][s] * b[s][j]
    return c
# алгоритм транспонирования матрицы
def transpose(matrix):
    n = len(matrix)
    k = len(matrix[0])
    return_matrix = [[0 for i in range(n)] for j in range(k)]
    for i in range(n):
        for j in range(k):
            return_matrix[j][i] = matrix[i][j]
    return return_matrix
def metod_Yakobi(A, e = 0.01):
    
    # 1 шаг - находим максимальный a_ij в верхнетреугольной матрице
    n = len(A)
    max_a = (A[0][1], 0, 1) # выбираем первый элемент над диагональю
    for i in range(0, n):
        for j in range(i, n):
            if i != j:
                if A[i][j] > max_a[0]:
                    max_a = (A[i][j], i, j)
                    
    V = [[0 for i in range(n)] for j in range(n)] # матрица собственных векторов
    for i in range(n):
            V[i][i] = 1
    
    # запускаем цикл работы алгоритма
    while abs(max_a[0]) > e:
        
        # 2 шаг - находим угол поворота
        a_ij, i, j = max_a[0], max_a[1], max_a[2]
        if A[i][i] == A[j][j]:
            fi = another_atan(a_ij * 100_000)
        else:
            fi = another_atan(2*a_ij / (A[i][i] - A[j][j])) / 2
        
        # 3 шаг - составляем матрицу вращения
        H = [[0 for i in range(n)] for j in range(n)]
        for h in range(n):
            H[h][h] = 1
        H[i][i], H[j][j] = another_cos(fi), another_cos(fi)
        H[i][j] = -another_sin(fi)
        H[j][i] = another_sin(fi)
    
        # 4 шаг - вычисляем очередное приближение и новую матрицу собственных векторов
        A = matmult(matmult(transpose(H), A), H)
        V = matmult(V, H)
        
        # повтор шага 1
        max_a = (A[0][1], 0, 1)
        for i in range(0, n):
            for j in range(i, n):
                if i != j:
                    if A[i][j] > max_a[0]:
                        max_a = (A[i][j], i, j)
                        
    # вывод ответа:
    print(f'Собственные числа:')
    for i in range(n):
        print(A[i][i])
    print()
    print(f'Собственные векторы:')
    for i in range(n):
        print(V[i])
A = [[200, 13, 47],
     [69, 2, 3004],
     [70, 101, 90]]

metod_Yakobi(A)
# проверка numpy
import numpy as np

print(np.linalg.eig(A)[0]) # Собственные числа
np.linalg.eig(A)[1] # Собственные векторы

# adams_moulton_method_am5.ipynb
# Markdown:
5-шаговый метод Адамса-Мултона  
Используем метод Рунге-Кутта 4 порядка для инициализации и метод Адамса-Бэшфорта 5 порядка для прогноза
import numpy as np
import matplotlib.pyplot as plt
def am5_method(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N + 1)
    y = np.zeros((N + 1, len(y_0)))
    fn = np.zeros_like(y)
    y[0, :] = y_0

    k1 = np.zeros_like(y_0)
    k2 = np.zeros_like(y_0)
    k3 = np.zeros_like(y_0)
    k4 = np.zeros_like(y_0)

    for n in range(N):
        fn[n, :] = f(x[n], y[n, :])
        k1 = dx * fn[n, :]
        k2 = dx * f(x[n] + dx / 2, y[n, :] + k1 / 2)
        k3 = dx * f(x[n] + dx / 2, y[n, :] + k2 / 2)
        k4 = dx * f(x[n] + dx, y[n, :] + k3)
        y[n + 1, :] = y[n, :] + 1 / 6 * (k1 + 2 * k2 + 2 * k3 + k4)

    coeff_A = np.array(
        [
            [1, 1, 1, 1, 1],
            [0, -1, -2, -3, -4],
            [0, 0, 2, 6, 12],
            [0, 0, 0, -6, -24],
            [0, 0, 0, 0, 24],
        ]
    )
    coeff_b = np.array([1, 1 / 2, 5 / 6, 9 / 4, 251 / 30])
    b_ab4 = np.linalg.solve(coeff_A, coeff_b)
    b_am5 = np.array([251, 646, -264, 106, -19]) / 720

    for n in range(4, N):
        fn[n, :] = f(x[n], y[n, :])
        yp = y[n, :] + dx * (
            b_ab4[0] * fn[n, :]
            + b_ab4[1] * fn[n - 1, :]
            + b_ab4[2] * fn[n - 2, :]
            + b_ab4[3] * fn[n - 3, :]
            + b_ab4[4] * fn[n - 4, :]
        )
        y[n + 1, :] = y[n, :] + dx * (
            b_am5[0] * f(x[n+1], yp)
            + b_am5[1] * fn[n, :]
            + b_am5[2] * fn[n - 1, :]
            + b_am5[3] * fn[n - 2, :]
            + b_am5[4] * fn[n - 3, :]
        )

    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = am5_method(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = am5_method(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при h = 0,1 - {}.'.format(y_5[-1, 0]))
print('Ошибка в x = 0,5 при h = 0,1 - {}.\n'.format(abs(y_5[-1, 0] - np.cos(0.5))))

print('Решение в x = 0,5 при h = 0,01 - {}.'.format(y_50[-1, 0]))
print('Ошибка в x = 0,5 при h = 0,01 - {}.\n'.format(abs(y_50[-1, 0] - np.cos(0.5))))

print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt
y0 = np.array([1.0, 0.0])

t_01, y_01 = am5_method(motion, 0.0, 50, y0, 500)
t_001, y_001 = am5_method(motion, 0.0, 50, y0, 5000)

fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(t_01, np.sqrt(y_01[:, 0]**2 + y_01[:, 1]**2) - 1, label = '$h = 0.1$')
ax1.set_xlabel('$t$')
ax1.set_ylabel('$\|$ Ошибка $\|$')

ax2 = fig.add_subplot(122)
ax2.plot(t_001, np.sqrt(y_001[:, 0]**2 + y_001[:, 1]**2) - 1, label = '$h = 0.01$')
ax2.set_xlabel('$t$')
ax2.set_ylabel('$\|$ Ошибка $\|$')
plt.show()
import math

y0 = np.array([1.0, 0.0])
N = np.array([50*2**i for i in range(3, 10)])
h = np.zeros_like(N, float)
h = 50.0 / N
err = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = am5_method(motion, 0.0, 50, y0, n)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)
p = np.polyfit(np.log(h[:-1]), np.log(err[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(111)
ax1.loglog(h, err, 'kx', label = 'Невязки в $r$')
ax1.loglog(h, np.exp(p[1])*h**p[0], 'b-', label = 'Наклон линии {:.2f}'.format(p[0]))

ax1.set_xlabel('h')
ax1.set_ylabel('$\|$ Ошибка $\|$')
ax1.set_title('Сходимость метода Адамса-Бэшфорта')
ax1.legend(loc = 2)
plt.show()


# conjugate_fourier_matrix..ipynb
# Markdown:
![Снимок экрана 2024-12-19 в 10.27.48.png](attachment:9d114505-3695-4687-a822-3e0b45645543.png)
# Markdown:
c есть первый столбец матрицы С (c_0, c_1, ..., c_n-1)  
Fn* - сопряжённая матрица Фурье
# Markdown:
![Снимок экрана 2024-12-19 в 10.31.54.png](attachment:621c2e5b-ef23-4943-bcc8-a19a6f2390df.png)

# Markdown:
# Сопряжённая матрица Фурье: концепция и применение

Матрица Фурье — это ключевой инструмент для дискретного преобразования Фурье (DFT), широко используемого в обработке сигналов, анализе данных и численных методах. Сопряжённая матрица Фурье представляет собой транспонированный и комплексно-сопряжённый вид этой матрицы. Она играет важную роль в обратном преобразовании Фурье, а также в изучении симметрий и свойств преобразования.

---

## Определение

Для дискретного преобразования Фурье на \( $N$ \) точках матрица Фурье \( $F$ \) определяется как:
\[
$F_{k,n} = \exp\left(-2\pi i \frac{kn}{N}\right), \quad k, n = 0, 1, \dots, N-1.$
\]

Сопряжённая матрица Фурье \( $F^*$ \) определяется как:
\[
$F^*_{k,n} = \overline{F_{n,k}} = \exp\left(2\pi i \frac{kn}{N}\right).$
\]

---

## Свойства

1. **Унитарность**  
   Матрица Фурье является унитарной при нормировке на \( $1/\sqrt{N}$ \):
   \[
   $F \cdot F^* = I,$
   \]
   где \( I \) — единичная матрица.

2. **Обратное преобразование**  
   Сопряжённая матрица Фурье используется для обратного дискретного преобразования Фурье:
   \[
   $x_n = \frac{1}{N} \sum_{k=0}^{N-1} X_k \exp\left(2\pi i \frac{kn}{N}\right).$
   \]

3. **Симметрии**  
   - Элементы \( $F$ \) и \( $F^*$ \) обладают циклической симметрией.
   - Модуль всех элементов матрицы равен 1.

---

## Применения

1. **Обратное преобразование Фурье**  
   Использование сопряжённой матрицы Фурье позволяет восстановить временной сигнал из его спектра.

2. **Анализ частотного спектра**  
   Сопряжённая матрица Фурье используется для изучения частотного содержания сигналов.

3. **Квантовая механика**  
   Сопряжённая матрица Фурье применяется для перехода между базисами (например, импульсного и координатного).

4. **Компрессия данных**  
   Используется в алгоритмах сжатия, таких как JPEG, где анализируется энергетическое распределение сигналов.

5. **Численные методы**  
   Сопряжённая матрица Фурье используется в задачах аппроксимации, интерполяции и фильтрации данных.

---

## Пример вычислений

Рассмотрим \( $N = 4$ \), где матрица Фурье \( $F$ \) и её сопряжённая выглядят так:
\[
$F$ =
\begin{bmatrix}
1 & 1 & 1 & 1 \\
1 & \omega & \omega^2 & \omega^3 \\
1 & \omega^2 & \omega^4 & \omega^6 \\
1 & \omega^3 & \omega^6 & \omega^9
\end{bmatrix},
\]
где \( $\omega = \exp\left(-2\pi i / N\right)$ \).

Сопряжённая матрица \( $F^*$ \):
\[
$F^*$ =
\begin{bmatrix}
1 & 1 & 1 & 1 \\
1 & \overline{\omega} & \overline{\omega^2} & \overline{\omega^3} \\
1 & \overline{\omega^2} & \overline{\omega^4} & \overline{\omega^6} \\
1 & \overline{\omega^3} & \overline{\omega^6} & \overline{\omega^9}
\end{bmatrix}.
\]

---

## Заключение

Сопряжённая матрица Фурье — это неотъемлемый инструмент для анализа частотного спектра, восстановления сигналов и решения различных задач в физике и математике. Её свойства обеспечивают высокую точность и эффективность вычислений в самых разных областях науки и техники.

# dft_analysis_and_visualization.ipynb
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 20, 201)
y = np.sin(x)

plt.figure(figsize=(8, 6))
plt.plot(x, y)
plt.ylabel("Амплитуда")
plt.xlabel("Положенение точки (x)")
plt.show()
x = np.linspace(0, 20, 201)
times = np.arange(5)
n = len(times)

plt.figure(figsize=(8, 8))

for t in times:
    plt.subplot(n, 1, t+1)
    y = np.sin(x + t)
    plt.plot(x, y)
    plt.plot(x[25], y[25], 'ro')
    plt.title(f't = {t}')
    

plt.ylabel("Амплитуда")
plt.xlabel("Положенение точки (x)")
plt.show()
# Markdown:
Период = 1 / частота

y(t) = A * sin(omega*t + phi)  
omega - угловая частота, сколько циклов проходит за секунду  
phi - фаза  

T - период волны  
f - частота волны, то:  
omega = 2 * pi / T = 2 * pi * f
sr = 100
ts = 1 / sr
t = np.arange(0, 1, ts)

freq = 5 # частота сигнала
y = np.sin(2*np.pi*freq*t)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

freq = 10 # частота сигнала
y = np.sin(2*np.pi*freq*t)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

plt.xlabel('Время')
plt.show()
# Markdown:
Сгенерировать две синусоидальные волны с временным интервалом от 0 до 1 секунды, с чпстотами 5 Гц и 10 Гц все с частотой дискретизации 100гц
sr = 100
ts = 1 / sr
t = np.arange(0, 1, ts)

freq = 5 # частота сигнала
y = 5*np.sin(2*np.pi*freq*t)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

freq = 10 # частота сигнала
y = 10*np.sin(2*np.pi*freq*t + 10)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

plt.xlabel('Время')
plt.show()
# Markdown:
### Дискретное преобразование Фурье (DFT) 

Преобразование Фурье помогает разложить любой сигнал на сумму простых синусоидальных волн, что позволяет легко измерять их частоту, амплитуду, фазу. 

Используя DFT можно представить сигнал как сумму синусоид с разными частотами 

Ампоитуда: amp = |X_k| / N = sqrt(Re(X_k)^2 + Im(X_k)^2)  
Фаза: phase = arctg(Im(X_k), Re(X_k))  
# Markdown:
Построить три синусоиды с частотам 1 Гц, 4 Гц, 7 Гц; Амплитудами 3, 1 и 0,5; фазами равными 0. Сложить их при частоте дескритезации
sr = 100
ts = 1 / sr
t = np.arange(0, 1, ts)

freq = 1 # частота сигнала
x = 3*np.sin(2*np.pi*freq*t)
# 
# plt.figure(figsize=(9, 9))
# plt.subplot(212)
# plt.plot(t, y, 'b')
# plt.ylabel('Амплитуда')

freq = 4 # частота сигнала
x += np.sin(2*np.pi*freq*t + 10)

freq = 7 # частота сигнала
x += 0.5*np.sin(2*np.pi*freq*t + 10)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, x, 'b')
plt.ylabel('Амплитуда')

plt.xlabel('Время')
plt.show()
def DFT(x):
    N = len(x)
    n = np.arange(N)
    k = n.reshape((N, 1))
    e = np.exp(-2j * np.pi * k * n / N)
    X = np.dot(e, x)
    
    return X
X = DFT(x)
N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize=(8, 6))
plt.stem(freq, abs(X), 'b', markerfmt=' ', basefmt='-b')

plt.xlabel('Частота (Гц)')
plt.ylabel('Амплитуда DFT')
plt.show()

n_oneside = N//2
f_oneside = freq[:n_oneside]

X_oneside = X[:n_oneside]/n_oneside

plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), 'b', markerfmt=' ', basefmt='-b')
plt.xlabel('Частота (Гц)')
plt.ylabel('Амплитуда DFT')

plt.subplot(122)
plt.stem(freq, abs(X), 'b', markerfmt=' ', basefmt='-b')
plt.xlim(0, 10)
plt.xlabel('Частота (Гц)')
plt.ylabel('Амплитуда DFT')
plt.show()
# Markdown:
x_n = 1/N * sum(
def gen_sig(sr):
    ts = 1.0 / sr
    t = np.arange(0, 1, ts)
    
    freq = 1
    x = 3*np.sin(2*np.pi*freq*t)
    return x
sr = 2000

%timeit DFT(gen_sig(sr))
sr = 10000
ts = 1 / sr

%timeit DFT(gen_sig(sr)


# eigenvalues_and_vectors.ipynb
import numpy as np
import matplotlib.pyplot as plt
#!pip install matplotlib --force-reinstall
def plot_vect(x, b, xlim, ylim):
    plt.figure(figsize = (10, 6))
    plt.quiver(0, 0, x[0], x[1], label = 'Original Vector',
               scale = 1, scale_units = 'xy', angles = 'xy')
    plt.quiver(0, 0, b[0], b[1], label = 'Transformed Vector',
               scale = 1, scale_units = 'xy', angles = 'xy', color = 'b')
    plt.xlim(xlim)
    plt.ylim(ylim)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.legend()
    plt.show()
A = np.array([[2, 0], [0, 1]])
x = np.array([[1], [1]])
b = np.dot(A, x)
plot_vect(x, b, (0, 3), (0, 2))
x = np.array([[1], [0]])
b = np.dot(A, x)
plot_vect(x, b, (0, 2), (-0.5, 0.5))
# Markdown:
Вычисление собственных значений с помощью характерисического многочлена

Характеристическое уравнение является полиномом степени n. Многочлен степени n имеет n комплексных корней
a = [[1, 0, 1], [1, 2, 0], [8, 0, -1]]
a = np.array(a)
cf = np.poly(a)
cf
ev_roots = np.roots(cf)
ev_roots
n = 40
a = [[1/(i + j + 0.5) for i in range(n)] for j in range(n)]
a = np.array(a)
ev = np.linalg.eigvals(a)
cf = np.poly(a)
ev_roots = np.roots(cf)

plt.scatter(ev_roots.real, ev_roots.imag, marker = 'x', label = 'roots')

b = a + np.random.rand(n, n)
ev_b = np.linalg.eigvals(b)

plt.scatter(ev_b.real, ev_b.imag, marker = 'o', label = 'rand')

plt.xlabel('Real part')
plt.xlabel('Imaginary part')
# Markdown:
Степенной метод (Power method):

Берём некоторый случайный вектор b и начинаем действовать на него оператором А (умножая), при этом нормируя: <br>
$b_{i+1} = A * b_i$ / ||$A$||

И так повторяем, пока изменение вектора не будет меньше заданного значения eps. <br>
Когда достигнем этого условия, считаем, что мы нашли собственный вектор, соответствующий наибольшему собственному значению
A = np.array([[2, 1], [1, 2]]) #Линейный оператор
x = np.array([[1, 2]]).T #Исходный вектор
tol = 1e-6 #Порог точности
max_iter = 100

lam_prev = 0

for i in range(max_iter):
    x = A @ x / np.linalg.norm(A @ x) 
    
    lam = (x.T @ A @ x) / (x.T @ x)
    
    if np.abs(lam - lam_prev) < tol:
        break
    
    lam_prev = lam
    
print(lam)
print(x)


# euler_method_analysis.ipynb
import numpy as np
import matplotlib.pyplot as plt
# Markdown:
Применим метод Эйлера к функции y'(x) = -sin(x) при x_0 = 0 до x_n = 0.5 <br>
    Шаг dx = 0.1
def method_euler(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0

    for n in range(N):
        y[n+1, :] = y[n, :] + dx * f(x[n], y[n, :])

    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = method_euler(fun_sin, 0.0, 0.5, [1.0], 5)
x_100, y_100 = method_euler(fun_sin, 0.0, 0.5, [1.0], 100)

print('Решение в x = 0,5 при h = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при h = 0,005 - {}.'.format(y_100[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
# Markdown:
Сходимость
N = np.array([2**i for i in range(3, 12)])
dx = 0.5 / N
err = np.zeros_like(dx)

for i in range(len(N)):
    n = N[i]
    x, y = method_euler(fun_sin, 0.0, 0.5, [1.0], n)
    err[i] = np.abs(np.cos(0.5) - y[-1, 0])

p = np.polyfit(np.log(dx[:-1]), np.log(err[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(dx, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(dx, np.exp(p[1])*dx**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('dx')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода Эйлера')
ax.legend(loc = 2)
plt.show()
# Markdown:
Система:

$x_{i+1} = -y_i$ <br>
$y_{i+1} = x_i$

x(0) = 1 <br>
y(0) = 0

В полярных координатах это r = 0, phi = 1

Возьмём шаг h = 0.1, рассчитав 500 шагов
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt

y0 = np.array([1.0, 0.0])

t_01, y_01 = method_euler(motion, 0.0, 50, y0, 500)
t_001, y_001 = method_euler(motion, 0.0, 50, y0, 5000)
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
# Markdown:
Сходимость
N = np.array([500*2**i for i in range(0, 10)])
h = 0.5 / N
err = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = method_euler(motion, 0.0, 50, y0, n)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(h, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(h, np.exp(p[1])*h**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода Эйлера')
ax.legend(loc = 2)
plt.show()


# euler_pc_rk4_analysis.ipynb
import numpy as np
import matplotlib.pyplot as plt
# Markdown:
Метод предиктор-корректор
def method_euler(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0

    for n in range(N):
        y[n+1, :] = y[n, :] + dx * f(x[n], y[n, :])

    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = method_euler(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = method_euler(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при dx = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при dx = 0,01 - {}.'.format(y_50[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
# Markdown:
Применим метод предиктор-корректор к функции y'(x) = -sin(x) при x_0 = 0 до x_n = 0.5 <br>
Шаг dx = 0.1
def euler_pc(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0

    for n in range(N):
        yp = y[n, :] + dx * f(x[n], y[n, :])
        y[n+1, :] = y[n, :] + dx/2 * (f(x[n], y[n, :]) + f(x[n+1], yp))
    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = euler_pc(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = euler_pc(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при dx = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при dx = 0,01 - {}.'.format(y_50[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
N = np.array([2**i for i in range(3, 12)])
dx = 0.5 / N
err = np.zeros_like(dx)

for i in range(len(N)):
    n = N[i]
    x, y = euler_pc(fun_sin, 0.0, 0.5, [1.0], n)
    err[i] = np.abs(np.cos(0.5) - y[-1, 0])

p = np.polyfit(np.log(dx[:-1]), np.log(err[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(dx, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(dx, np.exp(p[1])*dx**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('dx')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода предиктор-корректор')
ax.legend(loc = 2)
plt.show()
# Markdown:
Система:

$x_{i+1} = -y_i$ <br>
$y_{i+1} = x_i$

x(0) = 1 <br>
y(0) = 0

В полярных координатах это r = 0, phi = 1

Возьмём шаг h = 0.1, рассчитав 500 шагов
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt

y0 = np.array([1.0, 0.0])

t_01, y_01 = euler_pc(motion, 0.0, 50, y0, 500)
t_001, y_001 = euler_pc(motion, 0.0, 50, y0, 5000)
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
N = np.array([500*2**i for i in range(0, 10)])
h = 0.5 / N
err = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = euler_pc(motion, 0.0, 50, y0, n)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(h, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(h, np.exp(p[1])*h**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода предиктор-корректор')
ax.legend(loc = 2)
plt.show()
# Markdown:
Ошибка (и локальная, и глобальная) состоит из:

- Округления
- Сокращения

Общая ошибка является суммой глобальной ошибки сокращения и глобальной ошибки округления
# Markdown:
**Метод Рунге-Кутта 2**

$y_{n+1} = y_n + a * k_1 + b * k_2$  
$k_1 = h * f(x_n, y_n)$  
$k_2 = h * f(x_n + \alpha * h, y_n + \beta * k1)$  

С коэффициентами:  
$a + b = 1$  
$\alpha * b = 1/2$  
$\beta * b = 1/2$  
# Markdown:
**Метод Рунге-Кутта 4**

$y_{n+1} = y_n + 1/6 * (k_1 + 2 * (k_2+ k_3) + k_4)$  
$k1 = h * f(x_n, y_n)$  
$k2 = h * f(x_n + h/2, y_n + k_1/2)$  
$k3 = h * f(x_n + h/2, y_n + k_2/2)$  
$k4 = h * f(x_n + h, y_n + k_3)$  

Локальная ошибка: O(n^5)  
Глобальная ошибка: O(n^4)  
# Markdown:
Применим метод Рунге-Кутта 4 порядка к функции y'(x) = -sin(x) при x_0 = 0 до x_n = 0.5  
Шаг dx = 0.1
def rk4_method(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0
    k1 = np.zeros_like(y_0)
    k2 = np.zeros_like(y_0)
    k3 = np.zeros_like(y_0)
    k4 = np.zeros_like(y_0)

    for n in range(N):
        k1 = dx * f(x[n], y[n, :])
        k2 = dx * f(x[n] + dx/2, y[n, :] + k1/2)
        k3 = dx * f(x[n] + dx/2, y[n, :] + k2/2)
        k4 = dx * f(x[n] + dx, y[n, :] + k3)
        y[n+1, :] = y[n, :] + (k1 + k4 + 2*(k2 + k3))/6

    return x, y
def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = rk4_method(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = rk4_method(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при dx = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при dx = 0,01 - {}.'.format(y_50[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt

y0 = np.array([1.0, 0.0])

t_01, y_01 = rk4_method(motion, 0.0, 50, y0, 500)
t_001, y_001 = rk4_method(motion, 0.0, 50, y0, 5000)
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(t_01, np.sqrt(y_01[:, 0]**2 + y_01[:, 1]**2) - 1, label = '$h = 0.1$')
ax1.set_xlabel('$t$')
ax1.set_ylabel('$\|$ Ошибка $\|$')

ax2 = fig.add_subplot(122)
ax2.plot(t_001, np.sqrt(y_001[:, 0]**2 + y_001[:, 1]**2) - 1, label = '$h = 0.01$')
ax2.set_xlabel('$t$')
ax2.set_ylabel('$\|$ Ошибка $\|$')
plt.show()
import math

y0 = np.array([1.0, 0.0])
N = np.array([50*2**i for i in range(0, 10)])
h = np.zeros_like(N, float)
h = 50.0 / N
err_r = np.zeros_like(h)
err_theta = np.zeros_like(h)
err_x = np.zeros_like(h)
err_y = np.zeros_like(h)
err_all = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = rk4_method(motion, 0.0, 50, y0, n)
    err_r[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)
    err_theta[i] = np.abs(math.atan2(y[-1, 1], y[-1, 0]) - np.mod(50.0, 2.0*np.pi) + 2.0*np.pi)
    err_x[i] = np.abs(y[-1, 0] - np.cos(50.0))
    err_y[i] = np.abs(y[-1, 1] - np.sin(50.0))
    err_all[i] = np.linalg.norm([y[-1, 0] - np.cos(50.0), y[-1, 1] - np.sin(50.0)])
p_r = np.polyfit(np.log(h[:-1]), np.log(err_r[:-1]), 1)
p_theta = np.polyfit(np.log(h[:-1]), np.log(err_theta[:-1]), 1)
p_x = np.polyfit(np.log(h[:-1]), np.log(err_x[:-1]), 1)
p_y = np.polyfit(np.log(h[:-1]), np.log(err_y[:-1]), 1)
p_all = np.polyfit(np.log(h[:-1]), np.log(err_all[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.loglog(h, err_r, 'kx', label = 'Невязки в $r$')
ax1.loglog(h, np.exp(p_r[1])*h**p_r[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_r[0]))
ax1.loglog(h, err_theta, 'ro', label = 'Невязки в $theta$')
ax1.loglog(h, np.exp(p_theta[1])*h**p_theta[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_theta[0]))
ax1.loglog(h, err_all, 'c+', label = '$\|$ Ошибка $\|_2$ в (\bf y)')
ax1.loglog(h, np.exp(p_all[1])*h**p_all[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_all[0]))
ax1.set_xlabel('h')
ax1.set_ylabel('$\|$ Ошибка $\|$')
ax1.set_title('Сходимость метода предиктор-корректор')
ax1.legend(loc = 2)

ax2 = fig.add_subplot(122)
ax2.loglog(h, err_x, 'kx', label = 'Невязки в $x$')
ax2.loglog(h, np.exp(p_x[1])*h**p_x[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_x[0]))
ax2.loglog(h, err_y, 'ro', label = 'Невязки в $y$')
ax2.loglog(h, np.exp(p_y[1])*h**p_y[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_y[0]))
ax2.loglog(h, err_all, 'c+', label = '$\|$ Ошибка $\|_2$ в (\bf y)')
ax2.loglog(h, np.exp(p_all[1])*h**p_all[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_all[0]))
ax2.set_xlabel('h')
ax2.set_ylabel('$\|$ Ошибка $\|$')
ax2.set_title('Сходимость метода предиктор-корректор')
ax2.legend(loc = 2)
plt.show()


# fft_signal_analysis.ipynb
# Markdown:
## Быстрое преобразование Фурье
import matplotlib.pyplot as plt
import numpy as np
def FFT(x):
    N = len(x)

    if N == 1:
        return x
    else:
        X_even = FFT(x[::2])
        X_odd = FFT(x[1::2])

        factor = np.exp(-2j*np.pi*np.arange(N)/N)

        X = np.concatenate([X_even + factor[:int(N/2)]*X_odd,
                X_even + factor[int(N/2):]*X_odd])
        return X
sr = 128
ts = 1/sr
t = np.arange(0, 1, ts)

freq = 1
x = 3*np.sin(2*np.pi*freq*t)

freq = 4
x += np.sin(2*np.pi*freq*t)

freq = 7
x += 0.5*np.sin(2*np.pi*freq*t)

plt.figure(figsize = (8, 4))
plt.plot(t, x, 'b')
plt.ylabel('Амплитуда')
plt.xlabel('Время')
plt.show()
X = FFT(x)

N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize = (12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

n_oneside = N//2
f_oneside = freq[:n_oneside]

X_oneside = X[:n_oneside]/n_oneside

plt.subplot(122)
plt.stem(f_oneside, abs(X_oneside), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

plt.show()
def gen_sig(sr):
    ts = 1/sr
    t = np.arange(0, 1, ts)

    freq = 1
    x = 3*np.sin(2*np.pi*freq*t)
    return x
sr = 2048
%timeit FFT(gen_sig(sr))
sr = 2000
ts = 1/sr
t = np.arange(0, 1, ts)

freq = 1
x = 3*np.sin(2*np.pi*freq*t)

freq = 4
x += np.sin(2*np.pi*freq*t)

freq = 7
x += 0.5*np.sin(2*np.pi*freq*t)
# Markdown:
FFT в Numpy
from numpy.fft import fft, ifft

X = fft(x)

N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize = (12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

plt.subplot(122)
plt.plot(t, ifft(X), 'r')
plt.ylabel('Амплитуда')
plt.xlabel('Время')

plt.show()
%timeit fft(x)
from scipy.fftpack import fft, ifft

X = fft(x)

N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize = (12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

plt.subplot(122)
plt.plot(t, ifft(X), 'r')
plt.ylabel('Амплитуда')
plt.xlabel('Время')

plt.show()
%timeit fft(x)
import pandas as pd
df = pd.read_csv('./930-data-export.csv',
                 delimiter=',', parse_dates=[1])
df = df.dropna()
df = df.reset_index(drop = True)
df.rename(columns = {'Timestamp (Hour Ending)':'hour',
                    'Demand (MWh)':'demand'},
                    inplace = True)
plt.figure(figsize = (12, 6))
plt.plot(df['hour'], df['demand'])
plt.xlabel('Дата, время')
plt.ylabel('Потребление электроэнергии (MWh)')
demand_values = df['demand'].values

X = fft(demand_values)
N = len(X)
n = np.arange(N)
sr = 1/ (60*60)
T = N/sr
freq = n/T

n_oneside = N//2
f_oneside = freq[1:n_oneside]

plt.figure(figsize = (12, 4))
plt.stem(f_oneside, np.abs(X[1:n_oneside]), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')
t_h = 1 / f_oneside / (60*60)

plt.figure(figsize = (16, 4))
plt.plot(t_h, np.abs(X[1:n_oneside])/n_oneside, 'b')
plt.xticks([12, 24, 90, 182])
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')
plt.show()

# fft_vs_dft_analysis.ipynb
# Markdown:
Алгоритм вычисления свёртки:  
1. Вычисляем преобразование Фурье x(t) и y(t)
2. Вычисляем их произведение
3. Применяем к результату обратное преобразование Фурье

Связь с вычислительной обработкой данных:
- Быстрое вычисление свёртки через быстрое преобразование Фурье, требующее O(n log n) операций, а не O(n^2)
- В обработке сигналов для выделения определённых частотных компонент часто используется гауссовый фильтр (низкочастотный) - как свёртка во временной области или умножение спектра на фильтр в частотной области.
# Markdown:
Матрица называется Тёплицевой если её элементы определены как:  
a_ij = t_(i-j)
- Все элементы на диагонали Тёплицевой матрицы одинаковы, она определяется двумя векторами - верхней строкой и первым столбцом (2n-1 параметр).
- Плотная матрица
- Основная операция для вычисления дискретной свёртки - произведение Тёплицевой матрицы на вектор.
# Markdown:
Циркулянтые матрицы
![Снимок экрана 2024-12-12 в 11.12.47.png](attachment:335bde0a-5467-4a33-8bf8-f4bd2ad61841.png)
![Снимок экрана 2024-12-12 в 11.15.01.png](attachment:5e81b9f4-163a-4d50-af13-4a852f2497dd.png)
import matplotlib.pyplot as plt
import numpy as np

N = 1000
dt = 1/800
x = np.linspace(0, N*dt, N)
y = np.sin(50* 2*np.pi*x) + 0.5*np.sin(80* 2*np.pi*x) + 0.2*np.sin(3000* 2*np.pi*x)

plt.plot(x, y)
#plt.ylabel('')
plt.show()
yf = np.fft.fft(y)
xf = np.linspace(0, 1/(2*dt), N//2)
plt.plot(xf, 2/N * np.abs(yf[0:N//2]))
plt.xlabel('Frequency') 
plt.ylabel('Amplitude')
plt.show()
import time
import scipy as sp

n = 1000
F = sp.linalg.dft(n)
x = np.random.randn(n)

y_full = F.dot(x)

full_time = %timeit -q -o F.dot(x)
print('Время для вычисления множения "наивного" = ', full_time.average)

y_fft = np.fft.fft(x)
fft_time = %timeit -q -o np.fft.fft(x)
print('Время с использованием FFT = ', fft_time.average)

print('Относительная ошибка = ', np.linalg.norm(y_full - y_fft) / np.linalg.norm(y_full))


# finite_difference_error_analysis.ipynb
# Markdown:
# Обыкновенные Дифференциальные Уравнения
# Markdown:
Прямая разность: $f'(x_0) = \frac{f(x_0 + h) - f(x_0)}{h}$

Обратная разность: $f'(x_0) = \frac{f(x_0) - f(x_0 - h)}{h}$

---

Вспомнили ряд Тейлора: $f(x_0 + h) = f(x_0) + \frac{h f'(x_0)}{1!} + \frac{h^2 f''(x_0)}{2!} + O(h^3)$

$f'(x_0) = \frac{f(x_0 + h) - f(x_0)}{h} = f'(x_0) + \frac{hf''(x_0)}{2} + O(h^2) = f'(x_0) + O(h)$

---

$f(x_0 + h) = f(x_0) + \frac{h f'(x_0)}{1!} + \frac{h^2 f''(x_0)}{2!} + O(h^3)$

$f(x_0 - h) = f(x_0) - \frac{h f'(x_0)}{1!} + \frac{h^2 f''(x_0)}{2!} - O(h^3)$

Центральная разность: $f'(x_0) = \frac{f(x_0 + h) - f(x_0 - h)}{2h} = f'(x_0) + O(h^2)$
import numpy as np
import matplotlib.pyplot as plt
# Markdown:
Вычислим производную f(x) = e^(x) при x = 0 с использованием метода прямой разности <br>
с проверкой ошибки по сравнению с точным результатом f'(0) = 1
h = np.logspace(-7, 1)
estimate = (np.exp(h) - np.exp(0.0)) / h
err = np.abs(1.0 - estimate)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot()
ax.loglog(h, err, 'kx', label = 'Расчётные данные')
ax.loglog(h, np.exp(p[1]) * h**p[0], label = 'Линейная аппроксимация')
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость оценок значения производной')
ax.legend(loc = 2)
plt.show()
# Markdown:
Вычислим производную f(x) = e^(x) при x = 0 с использованием метода центральной разности <br> 
с проверкой ошибки по сравнению с точным результатом f'(0) = 1
h = np.logspace(-5, 1)
estimate = (np.exp(h) - np.exp(-h)) / (2*h)
err = np.abs(1.0 - estimate)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot()
ax.loglog(h, err, 'kx', label = 'Расчётные данные')
ax.loglog(h, np.exp(p[1]) * h**p[0], label = 'Линейная аппроксимация')
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость оценок значения производной')
ax.legend(loc = 2)
plt.show()


# gershgorin_circles_and_qr.ipynb
# Круги Гершгорина

import numpy as np
import matplotlib.pyplot as plt

n = 3
fig, ax = plt.subplots(1, 1)
a = np.array([[5, 1, 1], [1, 0, 0.5], [2, 0, 10]])

a += 2*np.random.randn(n, n)

xg = np.diag(a).real
yg = np.diag(a).imag
rg = np.zeros(n)
ev = np.linalg.eigvals(a)

for i in range(n):
    rg[i] = np.sum(np.abs(a[i, :])) - np.abs(a[i, i])
    crc = plt.Circle((xg[i], yg[i]), radius = rg[i], fill = False)
    ax.add_patch(crc)
plt.scatter(ev.real, ev.imag, color = 'r', label = "Eigenvalues")
plt.axis('equal')
a = np.array([[5, 1, 1], [1, 0, 0.5], [2, 0, 10]])
np.diag(np.diag(a))
# QR алгоритм O(n^4)

n = 5
a = [[1 / (i + j + 0.5) for i in range(n)] for j in range(n)]
niter = 150

for k in range(niter):
    q, r = np.linalg.qr(a)
    a = r.dot(q)

print(a)
# Первая итерация алгоритма qr O(n^3)

n = 10
d = np.random.randn(n)
sub_diag = np.random.randn(n-1)

mat = np.diag(d) + np.diag(sub_diag, -1) + np.diag(sub_diag, 1)

plt.spy(mat)
plt.title('Original matrix')

q, r = np.linalg.qr(mat)
plt.figure()
b = r.dot(q)
b[abs(b) <= 1e-12] = 0

plt.spy(b)
plt.title('After one itaration for qr algorithm')

b[0, :]


# matrix_multiplication_methods.ipynb
import numpy as np
from numba import njit
import matplotlib.pyplot as plt
# Markdown:
Рукописная реализация алгоритма перемножения двух матриц
def matmult(a, b):
    n = a.shape[0]
    k = a.shape[1]
    m = b.shape[1] #!!!
    c = np.zeros((n, m))
    for i in range(n):
        for j in range(m):
            for s in range(k):
                c[i, j] += a[i, s] * b[s, j]
    return c
# Markdown:
Numba версия рукописной реализации алгоритма перемножения двух матриц
@njit
def matmult_n(a, b):
    n = a.shape[0]
    k = a.shape[1]
    m = b.shape[1]
    c = np.zeros((n, m))
    for i in range(n):
        for j in range(m):
            for s in range(k):
                c[i, j] += a[i, s] * b[s, j]
    return c
# Markdown:
Сравним скорость выполнения различных алгоритмов
n = 100
A = np.random.randn(n, n)
B = np.random.randn(n, n)

%timeit matmult(A, B)
%timeit matmult_n(A, B)
%timeit np.dot(A, B)
# Markdown:
Проверим для всех ли размерностей выполняются такие соотношения
dim_range = [10*i for i in range(1, 11)]
time_range_matmul = []
time_range_numba_matmul = []
time_range_np = []

for n in dim_range:
    a = np.random.randn(n, n)
    b = np.random.randn(n, n)

    t = %timeit -o -q matmult(a, b)
    time_range_matmul.append(t.average)
    
    t = %timeit -o -q matmult_n(a, b)
    time_range_numba_matmul.append(t.average)
    
    t = %timeit -o -q np.dot(a, b)
    time_range_np.append(t.average)
plt.plot(dim_range, time_range_matmul, c = 'red')
plt.plot(dim_range, time_range_numba_matmul, c = 'blue')
plt.plot(dim_range, time_range_np, c = 'green')
plt.yscale('log')
plt.show()
# Markdown:
Классический алгоритм имеет сложность $O(n^3)$ <br>
Рассмотрим метод Штрассена (https://ru.wikipedia.org/wiki/Алгоритм_Штрассена)
def strassen(A, B):
    n = len(A)

    if n <= 2:
        return np.dot(A, B)

    # Разделим матрицы на подматрицы
    mid = n // 2
    A11 = A[:mid, :mid]
    A12 = A[:mid, mid:]
    A21 = A[mid:, :mid]
    A22 = A[mid:, mid:]
    B11 = B[:mid, :mid]
    B12 = B[:mid, mid:]
    B21 = B[mid:, :mid]
    B22 = B[mid:, mid:]

    # Рекурсивное умножение
    P1 = strassen(A11, B12 - B22)
    P2 = strassen(A11 + A12, B22)
    P3 = strassen(A21 + A22, B11)
    P4 = strassen(A22, B12 - B22)
    P5 = strassen(A11 + A22, B11 + B22)
    P6 = strassen(A12 - A22, B21 + B22)
    P7 = strassen(A11 - A21, B11 + B12)

    # Соединим результаты в матрицу C
    C11 = P5 + P4 - P2 + P6
    C12 = P1 + P2
    C21 = P3 + P4
    C22 = P5 + P1 - P3 - P7

    C = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))

    return C
n = 128
A = np.random.randn(n, n)
B = np.random.randn(n, n)

%timeit matmult(A, B)
%timeit strassen(A, B)
%timeit matmult_n(A, B)
%timeit np.dot(A, B)
dim_range = [2**i for i in range(1, 8)]
time_range_matmul = []
time_range_numba_matmul = []
time_range_np = []
time_range_strassen = []

for n in dim_range:
    a = np.random.randn(n, n)
    b = np.random.randn(n, n)

    t = %timeit -o -q matmult(a, b)
    time_range_matmul.append(t.average)
    
    t = %timeit -o -q matmult_n(a, b)
    time_range_numba_matmul.append(t.average)
    
    t = %timeit -o -q np.dot(a, b)
    time_range_np.append(t.average)
    
    t = %timeit -o -q strassen(a, b)
    time_range_strassen.append(t.average)
plt.plot(dim_range, time_range_matmul, c = 'red')
plt.plot(dim_range, time_range_numba_matmul, c = 'blue')
plt.plot(dim_range, time_range_np, c = 'green')
plt.plot(dim_range, time_range_strassen, c = 'orange')
plt.yscale('log')
plt.show()


# sparse_matrices_and_performance.ipynb
import numpy as np
import matplotlib.pyplot as plt

lm = [1, 2, 3, 4] #начальный вектор
M = len(lm)
D = np.array(lm)
a = np.min(lm)
b = np.max(lm)
u = 0.5 * np.ones(M)
p = 1 #смещение

t = np.linspace(-1, 6, 1000)

def fun(lam):
    return 1 + p*np.sum(u**2 / (D - lam))

res = [fun(lam) for lam in t]

plt.plot(t, res)
plt.plot(t, np.zeros_like(t))
plt.ylim([-6, 6])
plt.show()
import scipy as sp
import scipy.sparse
from scipy.sparse import csc_matrix, csr_matrix
import scipy.linalg
import scipy.sparse.linalg

n = 5
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n)
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csc_matrix(A)

plt.spy(A, aspect = 'equal', marker = '.')
# Markdown:
Способы хранения матриц:
* coo (координатный формат) - (i, j, value)
* LIL (список списков)
* CSR (compressed sparse row) - ia, ja, sa
* CSC (compressed sparse column)
* Блочные варианты

В scipy представлены конструкторы для этих форматов, например scipy.sparse.lil_matrix(A)
from scipy.sparse import csc_matrix, csr_matrix, coo_matrix

n = 1000
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n)
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csc_matrix(A)
rhs = np.ones(n*n)
B = coo_matrix(A)

%timeit A.dot(rhs)
%timeit B.dot(rhs)
import time

n = 4000
a = np.random.randn(n, n)
v = np.random.randn(n)
t = time.time()
np.dot(a, v)
t = time.time() - t

print('Время: {0: 3.1e}, Эффективность: {1: 3.1e} Gflops'.format(t, ((2*n**2)/t) / 10**9))
n = 40000
ex = np.random.randn(n)
a = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n)
rhs = np.random.randn(n)
t = time.time()
a.dot(rhs)
t = time.time() - t

print('Время: {0: 3.1e}, Эффективность: {1: 3.1e} Gflops'.format(t, ((3*n)/t) / 10**9))

n = 10
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n, 'csr')
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csr_matrix(A)
rhs = np.ones(n*n)
sol = sp.sparse.linalg.spsolve(A, rhs)

_, ax = plt.subplots(1, 2)

ax[0].plot(sol)
ax[0].set_title('Не упорядоченное решение')

ax[1].contourf(sol.reshape((n, n)), order = 'f')
ax[1].set_title('Упорядоченное решение')
import networkx as nx

n = 10
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n, 'csr')
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csr_matrix(A)

G = nx.Graph(A)
nx.draw_networkx(G, pos = nx.spectral_layout(G), node_size = 20, with_labels = False)
import scipy.sparse as spsp
import scipy.sparse.linalg as spsplin

A = spsp.coo_matrix((np.random.randn(10), ([0, 0, 0, 0, 1, 1, 2, 2, 3, 3], 
                                           [0, 1, 2, 3, 0, 1, 0, 2, 0, 3])))

plt.title('Исходная матрица')
plt.spy(A)
plt.show()

lu = spsplin.splu(A.tocsc(), permc_spec = 'COLAMD')

plt.title('L factor')
plt.spy(lu.L)
plt.show()

plt.title('U factor')
plt.spy(lu.U)
plt.show()

# adams_moulton_method_am5.ipynb
# Markdown:
5-шаговый метод Адамса-Мултона  
Используем метод Рунге-Кутта 4 порядка для инициализации и метод Адамса-Бэшфорта 5 порядка для прогноза
import numpy as np
import matplotlib.pyplot as plt
def am5_method(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N + 1)
    y = np.zeros((N + 1, len(y_0)))
    fn = np.zeros_like(y)
    y[0, :] = y_0

    k1 = np.zeros_like(y_0)
    k2 = np.zeros_like(y_0)
    k3 = np.zeros_like(y_0)
    k4 = np.zeros_like(y_0)

    for n in range(N):
        fn[n, :] = f(x[n], y[n, :])
        k1 = dx * fn[n, :]
        k2 = dx * f(x[n] + dx / 2, y[n, :] + k1 / 2)
        k3 = dx * f(x[n] + dx / 2, y[n, :] + k2 / 2)
        k4 = dx * f(x[n] + dx, y[n, :] + k3)
        y[n + 1, :] = y[n, :] + 1 / 6 * (k1 + 2 * k2 + 2 * k3 + k4)

    coeff_A = np.array(
        [
            [1, 1, 1, 1, 1],
            [0, -1, -2, -3, -4],
            [0, 0, 2, 6, 12],
            [0, 0, 0, -6, -24],
            [0, 0, 0, 0, 24],
        ]
    )
    coeff_b = np.array([1, 1 / 2, 5 / 6, 9 / 4, 251 / 30])
    b_ab4 = np.linalg.solve(coeff_A, coeff_b)
    b_am5 = np.array([251, 646, -264, 106, -19]) / 720

    for n in range(4, N):
        fn[n, :] = f(x[n], y[n, :])
        yp = y[n, :] + dx * (
            b_ab4[0] * fn[n, :]
            + b_ab4[1] * fn[n - 1, :]
            + b_ab4[2] * fn[n - 2, :]
            + b_ab4[3] * fn[n - 3, :]
            + b_ab4[4] * fn[n - 4, :]
        )
        y[n + 1, :] = y[n, :] + dx * (
            b_am5[0] * f(x[n+1], yp)
            + b_am5[1] * fn[n, :]
            + b_am5[2] * fn[n - 1, :]
            + b_am5[3] * fn[n - 2, :]
            + b_am5[4] * fn[n - 3, :]
        )

    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = am5_method(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = am5_method(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при h = 0,1 - {}.'.format(y_5[-1, 0]))
print('Ошибка в x = 0,5 при h = 0,1 - {}.\n'.format(abs(y_5[-1, 0] - np.cos(0.5))))

print('Решение в x = 0,5 при h = 0,01 - {}.'.format(y_50[-1, 0]))
print('Ошибка в x = 0,5 при h = 0,01 - {}.\n'.format(abs(y_50[-1, 0] - np.cos(0.5))))

print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt
y0 = np.array([1.0, 0.0])

t_01, y_01 = am5_method(motion, 0.0, 50, y0, 500)
t_001, y_001 = am5_method(motion, 0.0, 50, y0, 5000)

fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(t_01, np.sqrt(y_01[:, 0]**2 + y_01[:, 1]**2) - 1, label = '$h = 0.1$')
ax1.set_xlabel('$t$')
ax1.set_ylabel('$\|$ Ошибка $\|$')

ax2 = fig.add_subplot(122)
ax2.plot(t_001, np.sqrt(y_001[:, 0]**2 + y_001[:, 1]**2) - 1, label = '$h = 0.01$')
ax2.set_xlabel('$t$')
ax2.set_ylabel('$\|$ Ошибка $\|$')
plt.show()
import math

y0 = np.array([1.0, 0.0])
N = np.array([50*2**i for i in range(3, 10)])
h = np.zeros_like(N, float)
h = 50.0 / N
err = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = am5_method(motion, 0.0, 50, y0, n)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)
p = np.polyfit(np.log(h[:-1]), np.log(err[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(111)
ax1.loglog(h, err, 'kx', label = 'Невязки в $r$')
ax1.loglog(h, np.exp(p[1])*h**p[0], 'b-', label = 'Наклон линии {:.2f}'.format(p[0]))

ax1.set_xlabel('h')
ax1.set_ylabel('$\|$ Ошибка $\|$')
ax1.set_title('Сходимость метода Адамса-Бэшфорта')
ax1.legend(loc = 2)
plt.show()


# conjugate_fourier_matrix..ipynb
# Markdown:
![Снимок экрана 2024-12-19 в 10.27.48.png](attachment:9d114505-3695-4687-a822-3e0b45645543.png)
# Markdown:
c есть первый столбец матрицы С (c_0, c_1, ..., c_n-1)  
Fn* - сопряжённая матрица Фурье
# Markdown:
![Снимок экрана 2024-12-19 в 10.31.54.png](attachment:621c2e5b-ef23-4943-bcc8-a19a6f2390df.png)

# Markdown:
# Сопряжённая матрица Фурье: концепция и применение

Матрица Фурье — это ключевой инструмент для дискретного преобразования Фурье (DFT), широко используемого в обработке сигналов, анализе данных и численных методах. Сопряжённая матрица Фурье представляет собой транспонированный и комплексно-сопряжённый вид этой матрицы. Она играет важную роль в обратном преобразовании Фурье, а также в изучении симметрий и свойств преобразования.

---

## Определение

Для дискретного преобразования Фурье на \( $N$ \) точках матрица Фурье \( $F$ \) определяется как:
\[
$F_{k,n} = \exp\left(-2\pi i \frac{kn}{N}\right), \quad k, n = 0, 1, \dots, N-1.$
\]

Сопряжённая матрица Фурье \( $F^*$ \) определяется как:
\[
$F^*_{k,n} = \overline{F_{n,k}} = \exp\left(2\pi i \frac{kn}{N}\right).$
\]

---

## Свойства

1. **Унитарность**  
   Матрица Фурье является унитарной при нормировке на \( $1/\sqrt{N}$ \):
   \[
   $F \cdot F^* = I,$
   \]
   где \( I \) — единичная матрица.

2. **Обратное преобразование**  
   Сопряжённая матрица Фурье используется для обратного дискретного преобразования Фурье:
   \[
   $x_n = \frac{1}{N} \sum_{k=0}^{N-1} X_k \exp\left(2\pi i \frac{kn}{N}\right).$
   \]

3. **Симметрии**  
   - Элементы \( $F$ \) и \( $F^*$ \) обладают циклической симметрией.
   - Модуль всех элементов матрицы равен 1.

---

## Применения

1. **Обратное преобразование Фурье**  
   Использование сопряжённой матрицы Фурье позволяет восстановить временной сигнал из его спектра.

2. **Анализ частотного спектра**  
   Сопряжённая матрица Фурье используется для изучения частотного содержания сигналов.

3. **Квантовая механика**  
   Сопряжённая матрица Фурье применяется для перехода между базисами (например, импульсного и координатного).

4. **Компрессия данных**  
   Используется в алгоритмах сжатия, таких как JPEG, где анализируется энергетическое распределение сигналов.

5. **Численные методы**  
   Сопряжённая матрица Фурье используется в задачах аппроксимации, интерполяции и фильтрации данных.

---

## Пример вычислений

Рассмотрим \( $N = 4$ \), где матрица Фурье \( $F$ \) и её сопряжённая выглядят так:
\[
$F$ =
\begin{bmatrix}
1 & 1 & 1 & 1 \\
1 & \omega & \omega^2 & \omega^3 \\
1 & \omega^2 & \omega^4 & \omega^6 \\
1 & \omega^3 & \omega^6 & \omega^9
\end{bmatrix},
\]
где \( $\omega = \exp\left(-2\pi i / N\right)$ \).

Сопряжённая матрица \( $F^*$ \):
\[
$F^*$ =
\begin{bmatrix}
1 & 1 & 1 & 1 \\
1 & \overline{\omega} & \overline{\omega^2} & \overline{\omega^3} \\
1 & \overline{\omega^2} & \overline{\omega^4} & \overline{\omega^6} \\
1 & \overline{\omega^3} & \overline{\omega^6} & \overline{\omega^9}
\end{bmatrix}.
\]

---

## Заключение

Сопряжённая матрица Фурье — это неотъемлемый инструмент для анализа частотного спектра, восстановления сигналов и решения различных задач в физике и математике. Её свойства обеспечивают высокую точность и эффективность вычислений в самых разных областях науки и техники.

# dft_analysis_and_visualization.ipynb
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 20, 201)
y = np.sin(x)

plt.figure(figsize=(8, 6))
plt.plot(x, y)
plt.ylabel("Амплитуда")
plt.xlabel("Положенение точки (x)")
plt.show()
x = np.linspace(0, 20, 201)
times = np.arange(5)
n = len(times)

plt.figure(figsize=(8, 8))

for t in times:
    plt.subplot(n, 1, t+1)
    y = np.sin(x + t)
    plt.plot(x, y)
    plt.plot(x[25], y[25], 'ro')
    plt.title(f't = {t}')
    

plt.ylabel("Амплитуда")
plt.xlabel("Положенение точки (x)")
plt.show()
# Markdown:
Период = 1 / частота

y(t) = A * sin(omega*t + phi)  
omega - угловая частота, сколько циклов проходит за секунду  
phi - фаза  

T - период волны  
f - частота волны, то:  
omega = 2 * pi / T = 2 * pi * f
sr = 100
ts = 1 / sr
t = np.arange(0, 1, ts)

freq = 5 # частота сигнала
y = np.sin(2*np.pi*freq*t)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

freq = 10 # частота сигнала
y = np.sin(2*np.pi*freq*t)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

plt.xlabel('Время')
plt.show()
# Markdown:
Сгенерировать две синусоидальные волны с временным интервалом от 0 до 1 секунды, с чпстотами 5 Гц и 10 Гц все с частотой дискретизации 100гц
sr = 100
ts = 1 / sr
t = np.arange(0, 1, ts)

freq = 5 # частота сигнала
y = 5*np.sin(2*np.pi*freq*t)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

freq = 10 # частота сигнала
y = 10*np.sin(2*np.pi*freq*t + 10)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, y, 'b')
plt.ylabel('Амплитуда')

plt.xlabel('Время')
plt.show()
# Markdown:
### Дискретное преобразование Фурье (DFT) 

Преобразование Фурье помогает разложить любой сигнал на сумму простых синусоидальных волн, что позволяет легко измерять их частоту, амплитуду, фазу. 

Используя DFT можно представить сигнал как сумму синусоид с разными частотами 

Ампоитуда: amp = |X_k| / N = sqrt(Re(X_k)^2 + Im(X_k)^2)  
Фаза: phase = arctg(Im(X_k), Re(X_k))  
# Markdown:
Построить три синусоиды с частотам 1 Гц, 4 Гц, 7 Гц; Амплитудами 3, 1 и 0,5; фазами равными 0. Сложить их при частоте дескритезации
sr = 100
ts = 1 / sr
t = np.arange(0, 1, ts)

freq = 1 # частота сигнала
x = 3*np.sin(2*np.pi*freq*t)
# 
# plt.figure(figsize=(9, 9))
# plt.subplot(212)
# plt.plot(t, y, 'b')
# plt.ylabel('Амплитуда')

freq = 4 # частота сигнала
x += np.sin(2*np.pi*freq*t + 10)

freq = 7 # частота сигнала
x += 0.5*np.sin(2*np.pi*freq*t + 10)

plt.figure(figsize=(9, 9))
plt.subplot(212)
plt.plot(t, x, 'b')
plt.ylabel('Амплитуда')

plt.xlabel('Время')
plt.show()
def DFT(x):
    N = len(x)
    n = np.arange(N)
    k = n.reshape((N, 1))
    e = np.exp(-2j * np.pi * k * n / N)
    X = np.dot(e, x)
    
    return X
X = DFT(x)
N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize=(8, 6))
plt.stem(freq, abs(X), 'b', markerfmt=' ', basefmt='-b')

plt.xlabel('Частота (Гц)')
plt.ylabel('Амплитуда DFT')
plt.show()

n_oneside = N//2
f_oneside = freq[:n_oneside]

X_oneside = X[:n_oneside]/n_oneside

plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), 'b', markerfmt=' ', basefmt='-b')
plt.xlabel('Частота (Гц)')
plt.ylabel('Амплитуда DFT')

plt.subplot(122)
plt.stem(freq, abs(X), 'b', markerfmt=' ', basefmt='-b')
plt.xlim(0, 10)
plt.xlabel('Частота (Гц)')
plt.ylabel('Амплитуда DFT')
plt.show()
# Markdown:
x_n = 1/N * sum(
def gen_sig(sr):
    ts = 1.0 / sr
    t = np.arange(0, 1, ts)
    
    freq = 1
    x = 3*np.sin(2*np.pi*freq*t)
    return x
sr = 2000

%timeit DFT(gen_sig(sr))
sr = 10000
ts = 1 / sr

%timeit DFT(gen_sig(sr)


# eigenvalues_and_vectors.ipynb
import numpy as np
import matplotlib.pyplot as plt
#!pip install matplotlib --force-reinstall
def plot_vect(x, b, xlim, ylim):
    plt.figure(figsize = (10, 6))
    plt.quiver(0, 0, x[0], x[1], label = 'Original Vector',
               scale = 1, scale_units = 'xy', angles = 'xy')
    plt.quiver(0, 0, b[0], b[1], label = 'Transformed Vector',
               scale = 1, scale_units = 'xy', angles = 'xy', color = 'b')
    plt.xlim(xlim)
    plt.ylim(ylim)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.legend()
    plt.show()
A = np.array([[2, 0], [0, 1]])
x = np.array([[1], [1]])
b = np.dot(A, x)
plot_vect(x, b, (0, 3), (0, 2))
x = np.array([[1], [0]])
b = np.dot(A, x)
plot_vect(x, b, (0, 2), (-0.5, 0.5))
# Markdown:
Вычисление собственных значений с помощью характерисического многочлена

Характеристическое уравнение является полиномом степени n. Многочлен степени n имеет n комплексных корней
a = [[1, 0, 1], [1, 2, 0], [8, 0, -1]]
a = np.array(a)
cf = np.poly(a)
cf
ev_roots = np.roots(cf)
ev_roots
n = 40
a = [[1/(i + j + 0.5) for i in range(n)] for j in range(n)]
a = np.array(a)
ev = np.linalg.eigvals(a)
cf = np.poly(a)
ev_roots = np.roots(cf)

plt.scatter(ev_roots.real, ev_roots.imag, marker = 'x', label = 'roots')

b = a + np.random.rand(n, n)
ev_b = np.linalg.eigvals(b)

plt.scatter(ev_b.real, ev_b.imag, marker = 'o', label = 'rand')

plt.xlabel('Real part')
plt.xlabel('Imaginary part')
# Markdown:
Степенной метод (Power method):

Берём некоторый случайный вектор b и начинаем действовать на него оператором А (умножая), при этом нормируя: <br>
$b_{i+1} = A * b_i$ / ||$A$||

И так повторяем, пока изменение вектора не будет меньше заданного значения eps. <br>
Когда достигнем этого условия, считаем, что мы нашли собственный вектор, соответствующий наибольшему собственному значению
A = np.array([[2, 1], [1, 2]]) #Линейный оператор
x = np.array([[1, 2]]).T #Исходный вектор
tol = 1e-6 #Порог точности
max_iter = 100

lam_prev = 0

for i in range(max_iter):
    x = A @ x / np.linalg.norm(A @ x) 
    
    lam = (x.T @ A @ x) / (x.T @ x)
    
    if np.abs(lam - lam_prev) < tol:
        break
    
    lam_prev = lam
    
print(lam)
print(x)


# euler_method_analysis.ipynb
import numpy as np
import matplotlib.pyplot as plt
# Markdown:
Применим метод Эйлера к функции y'(x) = -sin(x) при x_0 = 0 до x_n = 0.5 <br>
    Шаг dx = 0.1
def method_euler(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0

    for n in range(N):
        y[n+1, :] = y[n, :] + dx * f(x[n], y[n, :])

    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = method_euler(fun_sin, 0.0, 0.5, [1.0], 5)
x_100, y_100 = method_euler(fun_sin, 0.0, 0.5, [1.0], 100)

print('Решение в x = 0,5 при h = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при h = 0,005 - {}.'.format(y_100[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
# Markdown:
Сходимость
N = np.array([2**i for i in range(3, 12)])
dx = 0.5 / N
err = np.zeros_like(dx)

for i in range(len(N)):
    n = N[i]
    x, y = method_euler(fun_sin, 0.0, 0.5, [1.0], n)
    err[i] = np.abs(np.cos(0.5) - y[-1, 0])

p = np.polyfit(np.log(dx[:-1]), np.log(err[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(dx, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(dx, np.exp(p[1])*dx**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('dx')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода Эйлера')
ax.legend(loc = 2)
plt.show()
# Markdown:
Система:

$x_{i+1} = -y_i$ <br>
$y_{i+1} = x_i$

x(0) = 1 <br>
y(0) = 0

В полярных координатах это r = 0, phi = 1

Возьмём шаг h = 0.1, рассчитав 500 шагов
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt

y0 = np.array([1.0, 0.0])

t_01, y_01 = method_euler(motion, 0.0, 50, y0, 500)
t_001, y_001 = method_euler(motion, 0.0, 50, y0, 5000)
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
# Markdown:
Сходимость
N = np.array([500*2**i for i in range(0, 10)])
h = 0.5 / N
err = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = method_euler(motion, 0.0, 50, y0, n)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(h, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(h, np.exp(p[1])*h**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода Эйлера')
ax.legend(loc = 2)
plt.show()


# euler_pc_rk4_analysis.ipynb
import numpy as np
import matplotlib.pyplot as plt
# Markdown:
Метод предиктор-корректор
def method_euler(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0

    for n in range(N):
        y[n+1, :] = y[n, :] + dx * f(x[n], y[n, :])

    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = method_euler(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = method_euler(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при dx = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при dx = 0,01 - {}.'.format(y_50[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
# Markdown:
Применим метод предиктор-корректор к функции y'(x) = -sin(x) при x_0 = 0 до x_n = 0.5 <br>
Шаг dx = 0.1
def euler_pc(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0

    for n in range(N):
        yp = y[n, :] + dx * f(x[n], y[n, :])
        y[n+1, :] = y[n, :] + dx/2 * (f(x[n], y[n, :]) + f(x[n+1], yp))
    return x, y

def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = euler_pc(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = euler_pc(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при dx = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при dx = 0,01 - {}.'.format(y_50[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
N = np.array([2**i for i in range(3, 12)])
dx = 0.5 / N
err = np.zeros_like(dx)

for i in range(len(N)):
    n = N[i]
    x, y = euler_pc(fun_sin, 0.0, 0.5, [1.0], n)
    err[i] = np.abs(np.cos(0.5) - y[-1, 0])

p = np.polyfit(np.log(dx[:-1]), np.log(err[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(dx, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(dx, np.exp(p[1])*dx**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('dx')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода предиктор-корректор')
ax.legend(loc = 2)
plt.show()
# Markdown:
Система:

$x_{i+1} = -y_i$ <br>
$y_{i+1} = x_i$

x(0) = 1 <br>
y(0) = 0

В полярных координатах это r = 0, phi = 1

Возьмём шаг h = 0.1, рассчитав 500 шагов
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt

y0 = np.array([1.0, 0.0])

t_01, y_01 = euler_pc(motion, 0.0, 50, y0, 500)
t_001, y_001 = euler_pc(motion, 0.0, 50, y0, 5000)
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
N = np.array([500*2**i for i in range(0, 10)])
h = 0.5 / N
err = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = euler_pc(motion, 0.0, 50, y0, n)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot(111)
ax.loglog(h, err, 'kx', label = 'Посчитанные невязки')
ax.loglog(h, np.exp(p[1])*h**p[0], label = 'Наклон линии {:.2f}'.format(p[0]))
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость метода предиктор-корректор')
ax.legend(loc = 2)
plt.show()
# Markdown:
Ошибка (и локальная, и глобальная) состоит из:

- Округления
- Сокращения

Общая ошибка является суммой глобальной ошибки сокращения и глобальной ошибки округления
# Markdown:
**Метод Рунге-Кутта 2**

$y_{n+1} = y_n + a * k_1 + b * k_2$  
$k_1 = h * f(x_n, y_n)$  
$k_2 = h * f(x_n + \alpha * h, y_n + \beta * k1)$  

С коэффициентами:  
$a + b = 1$  
$\alpha * b = 1/2$  
$\beta * b = 1/2$  
# Markdown:
**Метод Рунге-Кутта 4**

$y_{n+1} = y_n + 1/6 * (k_1 + 2 * (k_2+ k_3) + k_4)$  
$k1 = h * f(x_n, y_n)$  
$k2 = h * f(x_n + h/2, y_n + k_1/2)$  
$k3 = h * f(x_n + h/2, y_n + k_2/2)$  
$k4 = h * f(x_n + h, y_n + k_3)$  

Локальная ошибка: O(n^5)  
Глобальная ошибка: O(n^4)  
# Markdown:
Применим метод Рунге-Кутта 4 порядка к функции y'(x) = -sin(x) при x_0 = 0 до x_n = 0.5  
Шаг dx = 0.1
def rk4_method(f, x_0, x_n, y_0, N):
    dx = (x_n - x_0) / N
    x = np.linspace(x_0, x_n, N+1)
    y = np.zeros((N+1, len(y_0)))
    y[0, :] = y_0
    k1 = np.zeros_like(y_0)
    k2 = np.zeros_like(y_0)
    k3 = np.zeros_like(y_0)
    k4 = np.zeros_like(y_0)

    for n in range(N):
        k1 = dx * f(x[n], y[n, :])
        k2 = dx * f(x[n] + dx/2, y[n, :] + k1/2)
        k3 = dx * f(x[n] + dx/2, y[n, :] + k2/2)
        k4 = dx * f(x[n] + dx, y[n, :] + k3)
        y[n+1, :] = y[n, :] + (k1 + k4 + 2*(k2 + k3))/6

    return x, y
def fun_sin(x, y):
    return -np.sin(x)

x_5, y_5 = rk4_method(fun_sin, 0.0, 0.5, [1.0], 5)
x_50, y_50 = rk4_method(fun_sin, 0.0, 0.5, [1.0], 50)

print('Решение в x = 0,5 при dx = 0,1 - {}.'.format(y_5[-1, 0]))
print('Решение в x = 0,5 при dx = 0,01 - {}.'.format(y_50[-1, 0]))
print('Точное решение в x = 0,5 - {}.'.format(np.cos(0.5)))
def motion(x, y):
    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] = y[0]

    return dfdt

y0 = np.array([1.0, 0.0])

t_01, y_01 = rk4_method(motion, 0.0, 50, y0, 500)
t_001, y_001 = rk4_method(motion, 0.0, 50, y0, 5000)
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(y_01[:, 0], y_01[:, 1], label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

ax2 = fig.add_subplot(122)
ax2.plot(y_001[:, 0], y_001[:, 1], label = '$h = 0.1$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')

plt.show()
fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.plot(t_01, np.sqrt(y_01[:, 0]**2 + y_01[:, 1]**2) - 1, label = '$h = 0.1$')
ax1.set_xlabel('$t$')
ax1.set_ylabel('$\|$ Ошибка $\|$')

ax2 = fig.add_subplot(122)
ax2.plot(t_001, np.sqrt(y_001[:, 0]**2 + y_001[:, 1]**2) - 1, label = '$h = 0.01$')
ax2.set_xlabel('$t$')
ax2.set_ylabel('$\|$ Ошибка $\|$')
plt.show()
import math

y0 = np.array([1.0, 0.0])
N = np.array([50*2**i for i in range(0, 10)])
h = np.zeros_like(N, float)
h = 50.0 / N
err_r = np.zeros_like(h)
err_theta = np.zeros_like(h)
err_x = np.zeros_like(h)
err_y = np.zeros_like(h)
err_all = np.zeros_like(h)

for i in range(len(N)):
    n = N[i]
    x, y = rk4_method(motion, 0.0, 50, y0, n)
    err_r[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)
    err_theta[i] = np.abs(math.atan2(y[-1, 1], y[-1, 0]) - np.mod(50.0, 2.0*np.pi) + 2.0*np.pi)
    err_x[i] = np.abs(y[-1, 0] - np.cos(50.0))
    err_y[i] = np.abs(y[-1, 1] - np.sin(50.0))
    err_all[i] = np.linalg.norm([y[-1, 0] - np.cos(50.0), y[-1, 1] - np.sin(50.0)])
p_r = np.polyfit(np.log(h[:-1]), np.log(err_r[:-1]), 1)
p_theta = np.polyfit(np.log(h[:-1]), np.log(err_theta[:-1]), 1)
p_x = np.polyfit(np.log(h[:-1]), np.log(err_x[:-1]), 1)
p_y = np.polyfit(np.log(h[:-1]), np.log(err_y[:-1]), 1)
p_all = np.polyfit(np.log(h[:-1]), np.log(err_all[:-1]), 1)

fig = plt.figure(figsize = (10, 6))
ax1 = fig.add_subplot(121)
ax1.loglog(h, err_r, 'kx', label = 'Невязки в $r$')
ax1.loglog(h, np.exp(p_r[1])*h**p_r[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_r[0]))
ax1.loglog(h, err_theta, 'ro', label = 'Невязки в $theta$')
ax1.loglog(h, np.exp(p_theta[1])*h**p_theta[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_theta[0]))
ax1.loglog(h, err_all, 'c+', label = '$\|$ Ошибка $\|_2$ в (\bf y)')
ax1.loglog(h, np.exp(p_all[1])*h**p_all[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_all[0]))
ax1.set_xlabel('h')
ax1.set_ylabel('$\|$ Ошибка $\|$')
ax1.set_title('Сходимость метода предиктор-корректор')
ax1.legend(loc = 2)

ax2 = fig.add_subplot(122)
ax2.loglog(h, err_x, 'kx', label = 'Невязки в $x$')
ax2.loglog(h, np.exp(p_x[1])*h**p_x[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_x[0]))
ax2.loglog(h, err_y, 'ro', label = 'Невязки в $y$')
ax2.loglog(h, np.exp(p_y[1])*h**p_y[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_y[0]))
ax2.loglog(h, err_all, 'c+', label = '$\|$ Ошибка $\|_2$ в (\bf y)')
ax2.loglog(h, np.exp(p_all[1])*h**p_all[0], 'b-', label = 'Наклон линии {:.2f}'.format(p_all[0]))
ax2.set_xlabel('h')
ax2.set_ylabel('$\|$ Ошибка $\|$')
ax2.set_title('Сходимость метода предиктор-корректор')
ax2.legend(loc = 2)
plt.show()


# fft_signal_analysis.ipynb
# Markdown:
## Быстрое преобразование Фурье
import matplotlib.pyplot as plt
import numpy as np
def FFT(x):
    N = len(x)

    if N == 1:
        return x
    else:
        X_even = FFT(x[::2])
        X_odd = FFT(x[1::2])

        factor = np.exp(-2j*np.pi*np.arange(N)/N)

        X = np.concatenate([X_even + factor[:int(N/2)]*X_odd,
                X_even + factor[int(N/2):]*X_odd])
        return X
sr = 128
ts = 1/sr
t = np.arange(0, 1, ts)

freq = 1
x = 3*np.sin(2*np.pi*freq*t)

freq = 4
x += np.sin(2*np.pi*freq*t)

freq = 7
x += 0.5*np.sin(2*np.pi*freq*t)

plt.figure(figsize = (8, 4))
plt.plot(t, x, 'b')
plt.ylabel('Амплитуда')
plt.xlabel('Время')
plt.show()
X = FFT(x)

N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize = (12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

n_oneside = N//2
f_oneside = freq[:n_oneside]

X_oneside = X[:n_oneside]/n_oneside

plt.subplot(122)
plt.stem(f_oneside, abs(X_oneside), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

plt.show()
def gen_sig(sr):
    ts = 1/sr
    t = np.arange(0, 1, ts)

    freq = 1
    x = 3*np.sin(2*np.pi*freq*t)
    return x
sr = 2048
%timeit FFT(gen_sig(sr))
sr = 2000
ts = 1/sr
t = np.arange(0, 1, ts)

freq = 1
x = 3*np.sin(2*np.pi*freq*t)

freq = 4
x += np.sin(2*np.pi*freq*t)

freq = 7
x += 0.5*np.sin(2*np.pi*freq*t)
# Markdown:
FFT в Numpy
from numpy.fft import fft, ifft

X = fft(x)

N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize = (12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

plt.subplot(122)
plt.plot(t, ifft(X), 'r')
plt.ylabel('Амплитуда')
plt.xlabel('Время')

plt.show()
%timeit fft(x)
from scipy.fftpack import fft, ifft

X = fft(x)

N = len(X)
n = np.arange(N)
T = N/sr
freq = n/T

plt.figure(figsize = (12, 6))
plt.subplot(121)
plt.stem(freq, abs(X), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')

plt.subplot(122)
plt.plot(t, ifft(X), 'r')
plt.ylabel('Амплитуда')
plt.xlabel('Время')

plt.show()
%timeit fft(x)
import pandas as pd
df = pd.read_csv('./930-data-export.csv',
                 delimiter=',', parse_dates=[1])
df = df.dropna()
df = df.reset_index(drop = True)
df.rename(columns = {'Timestamp (Hour Ending)':'hour',
                    'Demand (MWh)':'demand'},
                    inplace = True)
plt.figure(figsize = (12, 6))
plt.plot(df['hour'], df['demand'])
plt.xlabel('Дата, время')
plt.ylabel('Потребление электроэнергии (MWh)')
demand_values = df['demand'].values

X = fft(demand_values)
N = len(X)
n = np.arange(N)
sr = 1/ (60*60)
T = N/sr
freq = n/T

n_oneside = N//2
f_oneside = freq[1:n_oneside]

plt.figure(figsize = (12, 4))
plt.stem(f_oneside, np.abs(X[1:n_oneside]), markerfmt=" ", basefmt='-b')
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')
t_h = 1 / f_oneside / (60*60)

plt.figure(figsize = (16, 4))
plt.plot(t_h, np.abs(X[1:n_oneside])/n_oneside, 'b')
plt.xticks([12, 24, 90, 182])
plt.ylabel('Амплитуда')
plt.xlabel('Частота (Hz)')
plt.show()

# fft_vs_dft_analysis.ipynb
# Markdown:
Алгоритм вычисления свёртки:  
1. Вычисляем преобразование Фурье x(t) и y(t)
2. Вычисляем их произведение
3. Применяем к результату обратное преобразование Фурье

Связь с вычислительной обработкой данных:
- Быстрое вычисление свёртки через быстрое преобразование Фурье, требующее O(n log n) операций, а не O(n^2)
- В обработке сигналов для выделения определённых частотных компонент часто используется гауссовый фильтр (низкочастотный) - как свёртка во временной области или умножение спектра на фильтр в частотной области.
# Markdown:
Матрица называется Тёплицевой если её элементы определены как:  
a_ij = t_(i-j)
- Все элементы на диагонали Тёплицевой матрицы одинаковы, она определяется двумя векторами - верхней строкой и первым столбцом (2n-1 параметр).
- Плотная матрица
- Основная операция для вычисления дискретной свёртки - произведение Тёплицевой матрицы на вектор.
# Markdown:
Циркулянтые матрицы
![Снимок экрана 2024-12-12 в 11.12.47.png](attachment:335bde0a-5467-4a33-8bf8-f4bd2ad61841.png)
![Снимок экрана 2024-12-12 в 11.15.01.png](attachment:5e81b9f4-163a-4d50-af13-4a852f2497dd.png)
import matplotlib.pyplot as plt
import numpy as np

N = 1000
dt = 1/800
x = np.linspace(0, N*dt, N)
y = np.sin(50* 2*np.pi*x) + 0.5*np.sin(80* 2*np.pi*x) + 0.2*np.sin(3000* 2*np.pi*x)

plt.plot(x, y)
#plt.ylabel('')
plt.show()
yf = np.fft.fft(y)
xf = np.linspace(0, 1/(2*dt), N//2)
plt.plot(xf, 2/N * np.abs(yf[0:N//2]))
plt.xlabel('Frequency') 
plt.ylabel('Amplitude')
plt.show()
import time
import scipy as sp

n = 1000
F = sp.linalg.dft(n)
x = np.random.randn(n)

y_full = F.dot(x)

full_time = %timeit -q -o F.dot(x)
print('Время для вычисления множения "наивного" = ', full_time.average)

y_fft = np.fft.fft(x)
fft_time = %timeit -q -o np.fft.fft(x)
print('Время с использованием FFT = ', fft_time.average)

print('Относительная ошибка = ', np.linalg.norm(y_full - y_fft) / np.linalg.norm(y_full))


# finite_difference_error_analysis.ipynb
# Markdown:
# Обыкновенные Дифференциальные Уравнения
# Markdown:
Прямая разность: $f'(x_0) = \frac{f(x_0 + h) - f(x_0)}{h}$

Обратная разность: $f'(x_0) = \frac{f(x_0) - f(x_0 - h)}{h}$

---

Вспомнили ряд Тейлора: $f(x_0 + h) = f(x_0) + \frac{h f'(x_0)}{1!} + \frac{h^2 f''(x_0)}{2!} + O(h^3)$

$f'(x_0) = \frac{f(x_0 + h) - f(x_0)}{h} = f'(x_0) + \frac{hf''(x_0)}{2} + O(h^2) = f'(x_0) + O(h)$

---

$f(x_0 + h) = f(x_0) + \frac{h f'(x_0)}{1!} + \frac{h^2 f''(x_0)}{2!} + O(h^3)$

$f(x_0 - h) = f(x_0) - \frac{h f'(x_0)}{1!} + \frac{h^2 f''(x_0)}{2!} - O(h^3)$

Центральная разность: $f'(x_0) = \frac{f(x_0 + h) - f(x_0 - h)}{2h} = f'(x_0) + O(h^2)$
import numpy as np
import matplotlib.pyplot as plt
# Markdown:
Вычислим производную f(x) = e^(x) при x = 0 с использованием метода прямой разности <br>
с проверкой ошибки по сравнению с точным результатом f'(0) = 1
h = np.logspace(-7, 1)
estimate = (np.exp(h) - np.exp(0.0)) / h
err = np.abs(1.0 - estimate)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot()
ax.loglog(h, err, 'kx', label = 'Расчётные данные')
ax.loglog(h, np.exp(p[1]) * h**p[0], label = 'Линейная аппроксимация')
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость оценок значения производной')
ax.legend(loc = 2)
plt.show()
# Markdown:
Вычислим производную f(x) = e^(x) при x = 0 с использованием метода центральной разности <br> 
с проверкой ошибки по сравнению с точным результатом f'(0) = 1
h = np.logspace(-5, 1)
estimate = (np.exp(h) - np.exp(-h)) / (2*h)
err = np.abs(1.0 - estimate)

p = np.polyfit(np.log(h), np.log(err), 1)

fig = plt.figure(figsize = (10, 6))
ax = fig.add_subplot()
ax.loglog(h, err, 'kx', label = 'Расчётные данные')
ax.loglog(h, np.exp(p[1]) * h**p[0], label = 'Линейная аппроксимация')
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Ошибка $\|$')
ax.set_title('Сходимость оценок значения производной')
ax.legend(loc = 2)
plt.show()


# gershgorin_circles_and_qr.ipynb
# Круги Гершгорина

import numpy as np
import matplotlib.pyplot as plt

n = 3
fig, ax = plt.subplots(1, 1)
a = np.array([[5, 1, 1], [1, 0, 0.5], [2, 0, 10]])

a += 2*np.random.randn(n, n)

xg = np.diag(a).real
yg = np.diag(a).imag
rg = np.zeros(n)
ev = np.linalg.eigvals(a)

for i in range(n):
    rg[i] = np.sum(np.abs(a[i, :])) - np.abs(a[i, i])
    crc = plt.Circle((xg[i], yg[i]), radius = rg[i], fill = False)
    ax.add_patch(crc)
plt.scatter(ev.real, ev.imag, color = 'r', label = "Eigenvalues")
plt.axis('equal')
a = np.array([[5, 1, 1], [1, 0, 0.5], [2, 0, 10]])
np.diag(np.diag(a))
# QR алгоритм O(n^4)

n = 5
a = [[1 / (i + j + 0.5) for i in range(n)] for j in range(n)]
niter = 150

for k in range(niter):
    q, r = np.linalg.qr(a)
    a = r.dot(q)

print(a)
# Первая итерация алгоритма qr O(n^3)

n = 10
d = np.random.randn(n)
sub_diag = np.random.randn(n-1)

mat = np.diag(d) + np.diag(sub_diag, -1) + np.diag(sub_diag, 1)

plt.spy(mat)
plt.title('Original matrix')

q, r = np.linalg.qr(mat)
plt.figure()
b = r.dot(q)
b[abs(b) <= 1e-12] = 0

plt.spy(b)
plt.title('After one itaration for qr algorithm')

b[0, :]


# matrix_multiplication_methods.ipynb
import numpy as np
from numba import njit
import matplotlib.pyplot as plt
# Markdown:
Рукописная реализация алгоритма перемножения двух матриц
def matmult(a, b):
    n = a.shape[0]
    k = a.shape[1]
    m = b.shape[1] #!!!
    c = np.zeros((n, m))
    for i in range(n):
        for j in range(m):
            for s in range(k):
                c[i, j] += a[i, s] * b[s, j]
    return c
# Markdown:
Numba версия рукописной реализации алгоритма перемножения двух матриц
@njit
def matmult_n(a, b):
    n = a.shape[0]
    k = a.shape[1]
    m = b.shape[1]
    c = np.zeros((n, m))
    for i in range(n):
        for j in range(m):
            for s in range(k):
                c[i, j] += a[i, s] * b[s, j]
    return c
# Markdown:
Сравним скорость выполнения различных алгоритмов
n = 100
A = np.random.randn(n, n)
B = np.random.randn(n, n)

%timeit matmult(A, B)
%timeit matmult_n(A, B)
%timeit np.dot(A, B)
# Markdown:
Проверим для всех ли размерностей выполняются такие соотношения
dim_range = [10*i for i in range(1, 11)]
time_range_matmul = []
time_range_numba_matmul = []
time_range_np = []

for n in dim_range:
    a = np.random.randn(n, n)
    b = np.random.randn(n, n)

    t = %timeit -o -q matmult(a, b)
    time_range_matmul.append(t.average)
    
    t = %timeit -o -q matmult_n(a, b)
    time_range_numba_matmul.append(t.average)
    
    t = %timeit -o -q np.dot(a, b)
    time_range_np.append(t.average)
plt.plot(dim_range, time_range_matmul, c = 'red')
plt.plot(dim_range, time_range_numba_matmul, c = 'blue')
plt.plot(dim_range, time_range_np, c = 'green')
plt.yscale('log')
plt.show()
# Markdown:
Классический алгоритм имеет сложность $O(n^3)$ <br>
Рассмотрим метод Штрассена (https://ru.wikipedia.org/wiki/Алгоритм_Штрассена)
def strassen(A, B):
    n = len(A)

    if n <= 2:
        return np.dot(A, B)

    # Разделим матрицы на подматрицы
    mid = n // 2
    A11 = A[:mid, :mid]
    A12 = A[:mid, mid:]
    A21 = A[mid:, :mid]
    A22 = A[mid:, mid:]
    B11 = B[:mid, :mid]
    B12 = B[:mid, mid:]
    B21 = B[mid:, :mid]
    B22 = B[mid:, mid:]

    # Рекурсивное умножение
    P1 = strassen(A11, B12 - B22)
    P2 = strassen(A11 + A12, B22)
    P3 = strassen(A21 + A22, B11)
    P4 = strassen(A22, B12 - B22)
    P5 = strassen(A11 + A22, B11 + B22)
    P6 = strassen(A12 - A22, B21 + B22)
    P7 = strassen(A11 - A21, B11 + B12)

    # Соединим результаты в матрицу C
    C11 = P5 + P4 - P2 + P6
    C12 = P1 + P2
    C21 = P3 + P4
    C22 = P5 + P1 - P3 - P7

    C = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))

    return C
n = 128
A = np.random.randn(n, n)
B = np.random.randn(n, n)

%timeit matmult(A, B)
%timeit strassen(A, B)
%timeit matmult_n(A, B)
%timeit np.dot(A, B)
dim_range = [2**i for i in range(1, 8)]
time_range_matmul = []
time_range_numba_matmul = []
time_range_np = []
time_range_strassen = []

for n in dim_range:
    a = np.random.randn(n, n)
    b = np.random.randn(n, n)

    t = %timeit -o -q matmult(a, b)
    time_range_matmul.append(t.average)
    
    t = %timeit -o -q matmult_n(a, b)
    time_range_numba_matmul.append(t.average)
    
    t = %timeit -o -q np.dot(a, b)
    time_range_np.append(t.average)
    
    t = %timeit -o -q strassen(a, b)
    time_range_strassen.append(t.average)
plt.plot(dim_range, time_range_matmul, c = 'red')
plt.plot(dim_range, time_range_numba_matmul, c = 'blue')
plt.plot(dim_range, time_range_np, c = 'green')
plt.plot(dim_range, time_range_strassen, c = 'orange')
plt.yscale('log')
plt.show()


# sparse_matrices_and_performance.ipynb
import numpy as np
import matplotlib.pyplot as plt

lm = [1, 2, 3, 4] #начальный вектор
M = len(lm)
D = np.array(lm)
a = np.min(lm)
b = np.max(lm)
u = 0.5 * np.ones(M)
p = 1 #смещение

t = np.linspace(-1, 6, 1000)

def fun(lam):
    return 1 + p*np.sum(u**2 / (D - lam))

res = [fun(lam) for lam in t]

plt.plot(t, res)
plt.plot(t, np.zeros_like(t))
plt.ylim([-6, 6])
plt.show()
import scipy as sp
import scipy.sparse
from scipy.sparse import csc_matrix, csr_matrix
import scipy.linalg
import scipy.sparse.linalg

n = 5
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n)
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csc_matrix(A)

plt.spy(A, aspect = 'equal', marker = '.')
# Markdown:
Способы хранения матриц:
* coo (координатный формат) - (i, j, value)
* LIL (список списков)
* CSR (compressed sparse row) - ia, ja, sa
* CSC (compressed sparse column)
* Блочные варианты

В scipy представлены конструкторы для этих форматов, например scipy.sparse.lil_matrix(A)
from scipy.sparse import csc_matrix, csr_matrix, coo_matrix

n = 1000
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n)
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csc_matrix(A)
rhs = np.ones(n*n)
B = coo_matrix(A)

%timeit A.dot(rhs)
%timeit B.dot(rhs)
import time

n = 4000
a = np.random.randn(n, n)
v = np.random.randn(n)
t = time.time()
np.dot(a, v)
t = time.time() - t

print('Время: {0: 3.1e}, Эффективность: {1: 3.1e} Gflops'.format(t, ((2*n**2)/t) / 10**9))
n = 40000
ex = np.random.randn(n)
a = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n)
rhs = np.random.randn(n)
t = time.time()
a.dot(rhs)
t = time.time() - t

print('Время: {0: 3.1e}, Эффективность: {1: 3.1e} Gflops'.format(t, ((3*n)/t) / 10**9))

n = 10
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n, 'csr')
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csr_matrix(A)
rhs = np.ones(n*n)
sol = sp.sparse.linalg.spsolve(A, rhs)

_, ax = plt.subplots(1, 2)

ax[0].plot(sol)
ax[0].set_title('Не упорядоченное решение')

ax[1].contourf(sol.reshape((n, n)), order = 'f')
ax[1].set_title('Упорядоченное решение')
import networkx as nx

n = 10
ex = np.ones(n)
lp1 = scipy.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n, 'csr')
e = sp.sparse.eye(n)
A = sp.sparse.kron(lp1, e) + sp.sparse.kron(e, lp1)
A = csr_matrix(A)

G = nx.Graph(A)
nx.draw_networkx(G, pos = nx.spectral_layout(G), node_size = 20, with_labels = False)
import scipy.sparse as spsp
import scipy.sparse.linalg as spsplin

A = spsp.coo_matrix((np.random.randn(10), ([0, 0, 0, 0, 1, 1, 2, 2, 3, 3], 
                                           [0, 1, 2, 3, 0, 1, 0, 2, 0, 3])))

plt.title('Исходная матрица')
plt.spy(A)
plt.show()

lu = spsplin.splu(A.tocsc(), permc_spec = 'COLAMD')

plt.title('L factor')
plt.spy(lu.L)
plt.show()

plt.title('U factor')
plt.spy(lu.U)
plt.show()
