import numpy as np
import matplotlib.pyplot as plt
1 задание¶
Вид 1 - интерполяция¶
Пример 1 (билет 5, ПМ23-4) - кубическая сплайн-интерполяция¶
import numpy as np
import matplotlib.pyplot as plt
from scipy.sparse import lil_matrix, csr_matrix
from scipy.sparse.linalg import spsolve
def cubic_spline_coeffs(x, y):
n = len(x) - 1
# Создаем разреженную матрицу
A = lil_matrix((4*n, 4*n))
b = np.zeros(4*n)
row = 0
for i in range(n):
A[row, 4 * i: 4 * (i + 1)] = [x[i] ** 3, x[i] ** 2, x[i], 1]
b[row] = y[i]
row += 1
A[row, 4 * i: 4 * (i + 1)] = [x[i+1] ** 3, x[i+1] ** 2, x[i+1], 1]
b[row] = y[i+1]
row += 1
# условие непрерывности первых производных
for j in range(n-1):
A[row, 4 * j: 4 * (j + 1)] = [3 * x[j + 1] ** 2, 2 * x[j + 1], 1, 0]
A[row, 4 * (j + 1): 4 * (j + 2)] = [-3 * x[j + 1] ** 2, -2 * x[j + 1], -1, 0]
b[row] = 0
row += 1
# условие непрерывности вторых производных
for j in range(n-1):
A[row, 4 * j: 4 * (j + 1)] = [6 * x[j + 1] ** 2, 2, 0, 0]
A[row, 4*(j+1):4*(j+2)] = [-6*x[j+1] ** 2, -2, 0, 0]
b[row] = 0
row += 1
A[row, :4] = [6*x[0], 2, 0, 0]
b[row] = 0
row += 1
A[row, 4*(n-1):4*n] = [6*x[n], 2, 0, 0]
b[row] = 0
row += 1
# Решаем систему с помощью разреженного решателя
A_csr = A.tocsr()
coeffs = spsolve(A_csr, b)
# Ручное преобразование формы массива
coeffs_reshaped = np.zeros((n, 4))
for i in range(n):
coeffs_reshaped[i] = coeffs[4*i:4*(i+1)]
return coeffs_reshaped
def cubic_spline(coeffs, x_points, x_new):
for i in range(len(x_points) - 1):
if x_points[i] <= x_new <= x_points[i+1]:
a, b, c, d = coeffs[i]
return a*x_new**3 + b*x_new**2 + c*x_new + d
return None
x = np.array([-2, -1, 0, 1])
y = np.array([4, 1, 0, 2])
coeffs = cubic_spline_coeffs(x, y)
print('Коэффициенты сплайнов:')
for i, c in enumerate(coeffs):
print(f'S_{i}(x) = {c[0]:.3f}x^3 + {c[1]:.3f}x^2 + {c[2]:.3f}x + {c[3]:.3f}')
x_new = np.linspace(min(x), max(x), 200)
y_new = [cubic_spline(coeffs, x, xi) for xi in x_new]
plt.figure(figsize=(6, 4))
plt.plot(x_new, y_new, '-b', label = 'Кубический сплайн')
plt.plot(x, y, 'ro', label = 'Исходные точки')
plt.xlabel('x')
plt.xlabel('y')
plt.legend()
plt.grid()
plt.show()
Коэффициенты сплайнов: S_0(x) = 0.333x^3 + 2.000x^2 + 0.667x + -0.000 S_1(x) = 0.333x^3 + 2.000x^2 + 0.667x + 0.000 S_2(x) = -0.667x^3 + 2.000x^2 + 0.667x + 0.000
# для нахождения значения в точке
x_point = -0.5
y_point = cubic_spline(coeffs, x, x_point)
print(f"f({x_point}) = {y_point}")
f(-0.5) = 0.12499999999999989
# для проверки себя
from scipy.interpolate import CubicSpline
x = [-2, -1, 0, 1]
y = [4, 1, 0, 2]
f = CubicSpline(x, y, bc_type = 'natural')
x_new = np.linspace(-2, 1, 100)
y_new = f(x_new)
print(f(-0.5))
plt.plot(x_new, y_new)
plt.show()
0.12500000000000006
Ответы на вопросы:
Глобальная интерполяция строит единый интерполяционный полином, который проходит через все заданные точки на всем рассматриваемом отрезке. Такой подход, например, реализуется в полиномах Лагранжа или Ньютона. Однако при большом количестве узлов глобальные методы могут приводить к неустойчивости и значительным колебаниям между узлами. В отличие от этого, локальная интерполяция разбивает исходный отрезок на части и строит отдельные полиномы низкой степени для каждого подотрезка, как в случае кубических сплайнов. Локальные методы обеспечивают лучшую устойчивость и точность на практике, особенно для данных с резкими изменениями, но требуют дополнительных условий гладкости на стыках отрезков (например, совпадения производных).
Локальная интерполяция предпочтительнее в нескольких случаях. Во-первых, при работе с большими наборами данных, где глобальные полиномы становятся неустойчивыми. Во-вторых, когда исходная функция имеет резкие локальные изменения, так как локальные методы лучше адаптируются к таким особенностям. В-третьих, в инженерных расчетах, где важна вычислительная эффективность: кубические сплайны требуют меньше операций для вычислений по сравнению с высокостепенными глобальными полиномами. Наконец, локальные методы незаменимы в задачах, требующих гладкости, где нужно точно контролировать производные на границах отрезков.
Переполнение (overflow) возникает, когда результат операции превышает максимальное число, которое может быть корректно представлено в выбранном типе данных. В интерполяции это может проявиться при вычислении высокостепенных полиномов или больших коэффициентов, например, в глобальных методах при большом количестве узлов. Переполнение приводит к искажениям: вместо корректных значений компьютер возвращает «бесконечность» (inf) или «не число» (NaN), что полностью нарушает вычисления. Даже если переполнение происходит в промежуточных расчетах, это снижает точность, так как часть информации теряется.
Пример 2 (билет 7, ПМ23-5-6?) - интерполяция многочленом Лагранжа¶
$L(x) = \sum^n_{i=1} y_i P_i(x)$
$P_i(x) = П \frac{x - x_j}{x_i - x_j}$
import numpy.polynomial.polynomial as poly
x = [0, 2, 4, 6]
y = [0, 4, 10, 18]
# ручками посчитали коэффициенты полинома по формуле выше
# P1 = ((x-2)*(x-4)*(x-6))/((0-2)*(0-4)*(0-6)) = (x^2-6x+8)*(x-6)/(-48) = (x^3-12x^2+44x-48)/(-48)
# P2 = ((x-0)*(x-4)*(x-6))/((2-0)*(2-4)*(2-6)) = (x^2-4x)*(x-6)/16 = (x^3-10x^2+24x)/16
# P3 = ((x-2)*(x-0)*(x-6))/((4-2)*(4-0)*(4-6)) = (x^2-2x)*(x-6)/(-16) = (x^3-8x^2+12x)/(-16)
# P4 = ((x-0)*(x-2)*(x-4))/((6-2)*(6-4)*(6-0)) = (x^2-2x)*(x-4)/48 = (x^3-6x^2+8x)/48
P1_coeff = [1, -44/48, 12/48, -1/48] # коэффы начиная со свободного члена и по возрастанию степени
P2_coeff = [0, 24/16, -10/16, 1/16]
P3_coeff = [0, -12/16, 8/16, -1/16]
P4_coeff = [0, 8/48, -6/48, 1/48]
P1 = poly.Polynomial(P1_coeff)
P2 = poly.Polynomial(P2_coeff)
P3 = poly.Polynomial(P3_coeff)
P4 = poly.Polynomial(P4_coeff)
L = 0*P1 + 4*P2 + 10*P3 + 18*P4
# для нахождения значения в точке
x_point = 3
y_point = L(x_point)
print(f"f({x_point}) = {y_point}")
x_new = np.linspace(0, 6, 60)
fig = plt.figure(figsize=(6, 4))
plt.plot(x_new, L(x_new), label='L')
plt.plot(x, y, 'ro')
plt.grid()
plt.legend()
plt.show()
f(3) = 6.75
# для проверки
from scipy.interpolate import lagrange
f = lagrange(x, y)
print(f(3))
fig = plt.figure(figsize=(6, 4))
plt.plot(x_new, f(x_new), label='L')
plt.grid()
plt.legend()
plt.show()
6.75
Ответы на вопросы:
Равномерное распределение точек при высокой степени полинома Лагранжа вызывает колебания, резко снижая точность между узлами. Оптимальное расположение точек минимизирует максимальную погрешность, "сгущая" их к концам отрезка. Для гладких функций такой выбор может улучшить точность в десятки раз по сравнению с равномерной сеткой.
При высокой степени полинома Лагранжа операции перемножения значений базисных функций и коэффициентов усиливают погрешности округления IEEE 754 из-за потери значащих разрядов. Особенно критично вычитание близких чисел в числителе и знаменателе базисных полиномов, приводящее к потере точности. Накопление таких ошибок может сделать результат бессмысленным уже при 20-30 узлах.
Для уменьшения ошибок можно: 1) Найти подходящие неравномерные узлы вместо равномерных для стабилизации погрешности и понижения степени полиномов. 2) Перейти к локальной интерполяции (сплайны) или разбить задачу на подотрезки с полиномами низкой степени. 3) Увеличить точность вычислений через float128 там, где это критично.
Пример 3 (билет 15, ПМ23-5) - интерполяция многочленом Лагранжа 2¶
import numpy.polynomial.polynomial as poly
x = [0, 2, 5, 8]
y = [15, 18, 22, 20]
# ручками посчитали коэффициенты полинома по формуле выше
# P1 = ((x-2)*(x-5)*(x-8))/((0-2)*(0-5)*(0-8)) = (x^2-7x+10)*(x-8)/(-80) = (x^3-15x^2+66x-80)/(-80)
# P2 = ((x-0)*(x-5)*(x-8))/((2-0)*(2-5)*(2-8)) = (x^2-5x)*(x-8)/36 = (x^3-13x^2+40x)/36
# P3 = ((x-2)*(x-0)*(x-8))/((5-2)*(5-0)*(5-8)) = (x^2-2x)*(x-8)/(-45) = (x^3-10x^2+16x)/(-45)
# P4 = ((x-0)*(x-2)*(x-5))/((8-2)*(8-5)*(8-0)) = (x^2-2x)*(x-5)/(48*3) = (x^3-7x^2+10x)/(48*3)
P1_coeff = [1, -66/80, 15/80, -1/80] # коэффы начиная со свободного члена и по возрастанию степени
P2_coeff = [0, 40/36, -13/36, 1/36]
P3_coeff = [0, -16/45, 10/45, -1/45]
P4_coeff = [0, 10/(48*3), -7/(48*3), 1/(48*3)]
P1 = poly.Polynomial(P1_coeff)
P2 = poly.Polynomial(P2_coeff)
P3 = poly.Polynomial(P3_coeff)
P4 = poly.Polynomial(P4_coeff)
L = 15*P1 + 18*P2 + 22*P3 + 20*P4
# для нахождения значения в точке
x_point = 4
y_point = L(x_point)
print(f"f({x_point}) = {y_point}")
x_new = np.linspace(0, 8, 80)
fig = plt.figure(figsize=(6, 4))
plt.plot(x_new, L(x_new), label='L')
plt.plot(x, y, 'ro')
plt.grid()
plt.legend()
plt.show()
f(4) = 21.03333333333332
# для проверки
from scipy.interpolate import lagrange
f = lagrange(x, y)
print(f(4))
fig = plt.figure(figsize=(6, 4))
plt.plot(x_new, f(x_new), label='L')
plt.grid()
plt.legend()
plt.show()
21.033333333333335
Ответы на вопросы:
- Многочлен Лагранжа строится как сумма базисных полиномов, каждый из которых равен 1 в своём узле интерполяции и 0 во всех остальных узлах, что гарантирует точное совпадение значений в заданных точках.
- Высокая степень полинома Лагранжа для зашумленных данных приводит к "переобучению" — полином начинает точно воспроизводить не только полезный сигнал, но и шум. Это ухудшает точность интерполяции в промежуточных точках, так как полином отражает случайные отклонения, а не истинную зависимость.
Вид 2 - метод Ньютона для уравнения¶
Пример 1 (билет 18, ПМ23-4)¶
# Параметры
P = 1.0
T = 300.0
R = 0.08314
a = 0.034
b = 0.018
# Уравнение Ван дер Ваальса
f = lambda V: (P + a/(V**2)) * (V - b) - R*T
# Производная
f_prime = lambda V: P + a/(V**2) - 2*a*(V - b)/(V**3)
# P.S. если вдруг производную будет не посчитать ручками, то я хз, придется нарушить его правило и пользоваться
# from scipy.misc import derivative и f_prime = lambda V: derivative(f, V, dx=1e-6)
def m_newton(f, df, x0, tol):
if abs(f(x0)) < tol:
return x0
else:
return m_newton(f, df, x0 - f(x0)/df(x0), tol)
estim = m_newton(f, f_prime, 1, 1e-6)
print('Ответ: ', estim)
Ответ: 24.958638728666205
# Пример, где Ньютон расходится:
P = 1.0
T = 300.0
R = 0.08314
a = 0.034
b = 0.018
# Уравнение
f = lambda x: x**3 - 2*x + 2
# Производная
f_prime = lambda x: 3*x**2 - 2
# P.S. если вдруг производную будет не посчитать ручками, то я хз, придется нарушить его правило и пользоваться
# from scipy.misc import derivative - f_prime = lambda V: derivative(f, V, dx=1e-6)
def m_newton(f, df, x0, tol):
if abs(f(x0)) < tol:
return x0
else:
return m_newton(f, df, x0 - f(x0)/df(x0), tol)
estim = m_newton(f, f_prime, 0, 1e-6)
print('Ответ: ', estim)
--------------------------------------------------------------------------- RecursionError Traceback (most recent call last) Cell In[39], line 23 20 else: 21 return m_newton(f, df, x0 - f(x0)/df(x0), tol) ---> 23 estim = m_newton(f, f_prime, 0, 1e-6) 25 print('Ответ: ', estim) Cell In[39], line 21, in m_newton(f, df, x0, tol) 19 return x0 20 else: ---> 21 return m_newton(f, df, x0 - f(x0)/df(x0), tol) Cell In[39], line 21, in m_newton(f, df, x0, tol) 19 return x0 20 else: ---> 21 return m_newton(f, df, x0 - f(x0)/df(x0), tol) [... skipping similar frames: m_newton at line 21 (2969 times)] Cell In[39], line 21, in m_newton(f, df, x0, tol) 19 return x0 20 else: ---> 21 return m_newton(f, df, x0 - f(x0)/df(x0), tol) Cell In[39], line 18, in m_newton(f, df, x0, tol) 17 def m_newton(f, df, x0, tol): ---> 18 if abs(f(x0)) < tol: 19 return x0 20 else: RecursionError: maximum recursion depth exceeded
Ответы на вопросы:
Влияние начального приближения: Метод Ньютона требует хорошего начального приближения. При неудачном выборе (например, где производная близка к нулю) метод может расходиться или зацикливаться.
Метод Ньютона имеет квадратичную скорость сходимости (быстрее), но требует вычисления производной и может расходиться. Метод бисекции сходится линейно (медленнее), но всегда находит корень, если функция меняет знак на интервале.
Пример $f(x) = x^3 - 2 \cdot x + 2$, выше видно, что расходится. В этом случае метод Ньютона зацикливается: он попеременно возвращает 0 и 1. При $x_0 = 0$ $f(0) = 2$, $f'(0) = -2$, $x_{new} = x_0 - f(x_0)/f'(x_0) = 0 - 2/(-2) = 1$. При $x_0 = 1$ $f(1) = 1$, $f'(1) = 1$, $x_{new} = x_0 - f(x_0)/f'(x_0) = 1 - 1/1 = 0$. И мы зациклились.
Вид 3 - решить систему¶
Пример 1 (билет 29, ПМ23-4) - метод Гаусса-Зейделя¶
# Метод Зейделя способ 1
# Система уравнений:
# x^2 - y = 1
# x - y^2 = 0
def gauss_zeidel_system(x0, y0, tol=1e-6, max_iter=100):
x, y = x0, y0
for k in range(max_iter):
x_new = np.sqrt(y + 1) # Из первого уравнения
y_new = np.sqrt(x_new) # Из второго уравнения
# Проверка условий остановки
if max(abs(x_new - x), abs(y_new - y)) < tol:
break
x, y = x_new, y_new
return x, y
# Начальное приближение
x0, y0 = 1.5, 1.5
x, y = gauss_zeidel_system(x0, y0)
print(f"Решение: x = {x:.6f}, y = {y:.6f}")
print(f"Проверка: x² - y = {x**2 - y:.2e}, x - y² = {x - y**2:.2e}")
Решение: x = 1.490217, y = 1.220744 Проверка: x² - y = 1.00e+00, x - y² = 0.00e+00
# Адекватное решение
def seidel_method(g_functions, initial_guess, tol=1e-6, max_iter=100):
"""
g_functions: список функций g_i, выражающих x_i через другие переменные.
initial_guess: начальное приближение (список).
tol: допустимая погрешность.
max_iter: максимальное число итераций.
"""
x = initial_guess.copy()
n = len(x)
for _ in range(max_iter):
x_prev = x.copy()
for i in range(n):
# Используем уже обновленные значения x[0], ..., x[i-1]
x[i] = g_functions[i](x)
# Проверка условия остановки
if max(abs(x[i] - x_prev[i]) for i in range(n)) < tol:
return x
raise RuntimeError("Метод не сошёлся за указанное число итераций")
# Для системы:
# x^2 - y = 1
# x - y^2 = 0
g_functions = [
lambda x: np.sqrt(1+x[1]), # x = (y+1)**0.5
lambda x: np.sqrt(x[0]) # y = x**0.5
]
initial_guess = [1.5, 1.5]
solution = seidel_method(g_functions, initial_guess)
print(f"Решение: x = {solution[0]:.5f}, y = {solution[1]:.5f}")
x_ans, y_ans = solution[0], solution[1]
print(f"Проверка: x² - y = {x_ans**2 - y_ans:.2e}, x - y^2 = {x_ans - y_ans**2:.2e}")
Решение: x = 1.49022, y = 1.22074 Проверка: x² - y = 1.00e+00, x - y^2 = 0.00e+00
Ответы на вопросы:
Гаусс-Зейдель: линейная сходимость, низкая вычислительная сложность, устойчив к округлениям. Ньютон: квадратичная сходимость, требует вычисления матрицы Якоби, поэтому более высокая сложность, чувствителен к начальному приближению, поэтому устойчивость низкая.
Абсолютной погрешностью ∆ приближенного значения называется модуль разности между точным и приближенным значениями этой величины Δ = |$a$ − $a_p$|. Относительной погрешностью приближенной величины $a_p$ называется отношение абсолютной погрешности приближенной величины к абсолютной величине ее точного значения:
$δ = \frac{|a − a_p|}{a}=\frac{Δ}{a}$.При операциях (сложение/вычитание) абсолютная ошибка накапливается аддитивно. В float-арифметике (IEEE 754) относительная ошибка одной операции ограничена машинным эпсилон (~1.11e-16 для float64). При умножении/делении относительные ошибки складываются. При сложении/вычитании относительная ошибка может резко возрасти, если результат близок к нулю.
Пример 2 (билет 14, ПМ23-6?) - метод Гаусса-Зейделя 2¶
# Метод Зейделя способ 2 (из колаба)
def seidel_method(g_functions, initial_guess, tol=1e-6, max_iter=100):
"""
g_functions: список функций g_i, выражающих x_i через другие переменные.
initial_guess: начальное приближение (список).
tol: допустимая погрешность.
max_iter: максимальное число итераций.
"""
x = initial_guess.copy()
n = len(x)
for _ in range(max_iter):
x_prev = x.copy()
for i in range(n):
# Используем уже обновленные значения x[0], ..., x[i-1]
x[i] = g_functions[i](x)
# Проверка условия остановки
if max(abs(x[i] - x_prev[i]) for i in range(n)) < tol:
return x
raise RuntimeError("Метод не сошёлся за указанное число итераций")
# Для системы:
# x^2 - y = 2
# x*y = 1
g_functions = [
lambda x: np.sqrt(2+x[1]), # x = (y+2)**0.5
lambda x: 1/x[0] # y = 1/x
]
initial_guess = [1.5, 0.7]
solution = seidel_method(g_functions, initial_guess)
print(f"Решение: x = {solution[0]:.5f}, y = {solution[1]:.5f}")
x_ans, y_ans = solution[0], solution[1]
print(f"Проверка: x² - y = {x_ans**2 - y_ans:.2e}, x*y = {x_ans*y_ans:.2e}")
Решение: x = 1.61803, y = 0.61803 Проверка: x² - y = 2.00e+00, x*y = 1.00e+00
Ответы на вопросы:
Метод Гаусса-Зейделя имеет более высокую устойчивость к ошибкам округления благодаря своей линейной природе и последовательному обновлению переменных, что минимизирует накопление погрешностей при каждой итерации. В отличие от него, метод Ньютона, несмотря на квадратичную сходимость, чувствителен к ошибкам округления из-за необходимости вычисления матрицы Якоби и её обращения, где даже малые погрешности могут значительно исказить результат. Ошибки округления в методе Ньютона особенно критичны, когда матрица Якоби близка к вырожденной, т.к. это приводит к усилению погрешностей. Гаусса-Зейдель, благодаря итеративному уточнению, менее подвержен таким эффектам, хотя и требует большего числа итераций для достижения той же точности. Таким образом, для задач, где важна устойчивость, надо использовать Гаусса-Зейделя, а Ньютон эффективен там, где допустимы точные начальные приближения и аналитические производные.
Липшицево отображение — отображение, увеличивающее расстояние между образами точек не более чем в L раз, где L называется константой Липшица данной функции. Условие Липшица — это математическое свойство функции, которое ограничивает скорость её изменения. Функция f удовлетворяет условию Липшица с константой L>0, если для всех $x_1$ и $x_2$ из её области определения выполняется:
$∣f(x_1)-f(x_2)∣ ≤ L \cdot ∣x_1−x_2∣$.
Константа Липшица характеризует степень гладкости функции, определяет максимальную скорость изменения её производной, и играет ключевую роль в анализе сходимости итерационных методов. Если функция удовлетворяет условию Липшица с малой константой, это гарантирует, что её поведение предсказуемо, и методы вроде Ньютона или простых итераций сходятся быстрее, так как колебания значений функции ограничены. Напротив, большая константа Липшица указывает на резкие изменения производной, что может замедлить сходимость или даже привести к расходимости, особенно в методах, чувствительных к локальным особенностям, таких как метод Ньютона. Для методов, основанных на сжатых отображениях, существование и малость константы Липшица являются критическими условиями сходимости, так как они определяют степень сжатия на каждой итерации.
Пример 3 (билет 30, ПМ23-5-6?) - метод Ньютона¶
import numpy as np
from scipy.sparse import csr_matrix
from scipy.sparse.linalg import spsolve, norm as spnorm
def newton_method(f, df, x0, n_iterations=100, tol=1e-6):
iters = np.zeros((n_iterations + 1, len(x0)))
iters[0, :] = x0 # Записываем начальное приближение в первую строку
for n in range(n_iterations):
fx = f(iters[n, :]) # Вычисляем значение вектор-функции f в текущей точке
fx_sparse = csr_matrix(fx[:, None]) # Преобразуем вектор fx в разреженный формат (для вычисления нормы)
if spnorm(fx_sparse) < tol: # Критерий остановки
return iters[:n+1, :]
J = df(iters[n, :]) # Вычисляем матрицу Якоби (производных) в текущей точке
J_sparse = csr_matrix(J) # Преобразуем матрицу Якоби в разреженный формат
try:
c = spsolve(J_sparse, -fx) # Пытаемся решить линейную систему J*c = -fx
except:
print(f"Матрица Якоби вырождена на итерации {n}")
return iters[:n+1, :]
iters[n+1, :] = iters[n, :] + c # Обновляем решение: x_{n+1} = x_n + c
return iters
def f(x):
f = np.zeros_like(x)
f[0] = x[0]**2 + x[1]**2 - 2
f[1] = x[0]*x[1] - 1
return f
def df(x):
df = np.zeros((len(x), len(x)))
df[0, 0] = 2*x[0]
df[0, 1] = 2*x[1]
df[1, 0] = x[1]
df[1, 1] = x[0]
return df
x0 = np.array([1.5, 1.0])
result = newton_method(f, df, x0, n_iterations=100)
print(f'Решение: {result[-1, :]}')
x_ans, y_ans = result[-1, 0], result[-1, 1]
print(f'Проверка: x**2+y**2 = {x_ans**2+y_ans**2:.2f}, x*y = {x_ans*y_ans:.2f}')
Решение: [1.00048828 0.99951172] Проверка: x**2+y**2 = 2.00, x*y = 1.00
Ответы на вопросы:
Ошибки округления в арифметике с плавающей точкой могут нарушить сходимость метода Ньютона, особенно когда вычисления приближаются к корню. На поздних итерациях, когда поправки становятся малыми (порядка 1e-6 - 1e-8), погрешности округления в вычислении производной или значения функции могут исказить направление шага. Например, если производная вычислена с ошибкой, отношение f(x)/f′(x) даст неверную поправку, что приведёт к колебаниям вокруг корня или даже расходимости. В особо критичных случаях (например, при f′(x)≈0) ошибки могут вызвать деление на ноль или переполнение.
Пример функции с чувствительностью к ошибкам. Функция $f(x)=x^3−2x+2$ при начальном приближении $x_0=0$ демонстрирует проблему: её производная $f′(x)=3x^2−2$ в точке x=0 равна −2, но на следующих итерациях метод зацикливается между 0 и 1 из-за дискретной природы вычислений. Здесь даже малые ошибки в расчёте f(x) или f′(x) (например, из-за округления) нарушают сходимость. Другой пример — $f(x)=sin(x^2)$ при $x ≈ π$ - из-за быстрых колебаний малые погрешности в вычислении производной приводят к большим ошибкам в поправке.
Пример 4 (билет 4, ПМ23-5-6?) - метод Ньютона с фиксированным якобианом¶
import numpy as np
from scipy.sparse import csr_matrix, lil_matrix
from scipy.sparse.linalg import spsolve, norm as spnorm
def newton_fixed_jac(f, x0, J_fixed, n_iterations=100, tol=1e-6):
iters = np.zeros((n_iterations + 1, len(x0))) # Инициализация массива для хранения итераций
iters[0, :] = x0
J_sparse = csr_matrix(J_fixed) # Преобразуем фиксированный якобиан в разреженный формат
for n in range(n_iterations):
fx = f(iters[n, :]) # Вычисляем значение функции
# Проверяем критерий остановки через разреженную норму
if spnorm(csr_matrix(fx[:, None])) < tol:
return iters[:n+1, :]
try:
# Решаем систему с фиксированным якобианом
c = spsolve(J_sparse, -fx)
except:
print(f"Ошибка решения на итерации {n}")
return iters[:n+1, :]
# Обновляем решение
iters[n+1, :] = iters[n, :] + c
return iters
# Определяем систему уравнений
def f(x):
return np.array([x[0]**2 - x[1] - 1, # x^2 - y = 1
x[0] - x[1]**2]) # x - y^2 = 0
# Фиксированный якобиан, вычисленный в точке (1.5, 1.5)
J_fixed = np.array([[3.0, -1.0], # [2x, -1] при x=1.5
[1.0, -3.0]]) # [1, -2y] при y=1.5
# Начальное приближение
x0 = np.array([1.5, 1.5])
# Решаем систему
result = newton_fixed_jac(f, x0, J_fixed)
# Выводим результаты
print(f'Решение: x = {result[-1, 0]:.6f}, y = {result[-1, 1]:.6f}')
print(f'Проверка: x^2-y-1 = {(result[-1,0]**2 - result[-1,1] - 1):.2e}')
print(f' x-y^2 = {(result[-1,0] - result[-1,1]**2):.2e}')
Решение: x = 1.490216, y = 1.220744 Проверка: x^2-y-1 = 2.64e-09 x-y^2 = -2.19e-07
Ответы на вопросы:
Связь константы Липшица с сходимостью: константа Липшица определяет скорость изменения функции - если она меньше 1, метод итераций гарантированно сходится, причем чем меньше константа, тем быстрее. Для метода Ньютона с фиксированным якобианом это означает, что сходимость линейная и зависит от точности начального приближения.
Накопление ошибок округления может исказить критерий остановки - вычисленная невязка может оказаться меньше истинной из-за потери точности. Если при вычислении f(x) происходит взаимное уничтожение погрешностей (например, при вычитании близких чисел), невязка окажется искусственно малой. Это приводит к преждевременной остановке, когда истинная погрешность ещё велика. Накопление ошибок в арифметических операциях может увеличить модуль невязки. Тогда итерации продолжаются дольше необходимого, хотя решение уже достигло допустимой точности.
Вид 4 - метод функциональной итерации¶
Пример 1 (билет ?, ПМ23-5-6?) -¶
def f_iter(f, x0, tol=1e-5, max_iter=100, alpha=1.0):
"""
Метод функциональной итерации с параметром ускорения
f: функция, корень которой ищем (f(x)=0)
x0: начальное приближение
tol: точность
max_iter: максимальное число итераций
alpha: параметр ускорения (0 < alpha < 2)
"""
def g(x):
return x - alpha * f(x) # Модифицированная итерационная функция
iterations = [x0]
for i in range(max_iter):
x_new = g(iterations[-1])
iterations.append(x_new)
# Критерий остановки по разнице между итерациями
if abs(x_new - iterations[-2]) < tol:break
return np.array(iterations)
# Исходная функция
def f(x):
return np.sin(x) - 0.5*x
x0 = 1.0 # Начальное приближение
result = f_iter(f, x0) # Решаем с параметром ускорения по умолчанию - alpha=1
print(f"Найденный корень: {result[-1]:.8f}")
print(f'Проверка: sin(x) - 0.5x = 0 = {np.sin(result[-1]) - 0.5*result[-1]}')
Найденный корень: 0.00000611 Проверка: sin(x) - 0.5x = 0 = 3.053566416158319e-06
Ответы на вопросы:
Параметр $\alpha$ в модифицированной итерационной формуле $x_{n+1}=x_n-\alpha f(x_n)$ регулирует размер шага: при $alpha>1$ метод ускоряется для пологих функций (когда $∣f′(x)∣≪1$), увеличивая шаг, а при $0<\alpha<1$ — замедляется для крутых функций (где $∣f ′(x)∣≈1$), предотвращая расходимость. Оптимальный $\alpha$ компенсирует производную, приближая скорость сходимости к квадратичной. Однако неудачный выбор $\alpha$ может привести к колебаниям или расходимости.
Функция $f(x)=arctan(x)$ при $\alpha=1$ сходится очень медленно - для значений точность 1е-5 нужно около 300 итераций из-за маленькой производной $(|f'(x)|\rightarrow 0$ при $|x|\rightarrow \infty)$. Ускорение сходимости достигается при $\alpha=1.3-1.5$, что компенсирует пологость функции. Без такого параметра метод функциональной итерации был бы неэффективным для этой задачи.
Пример 2 (билет 3, ПМ23-5-6?) -¶
def f_iter(f, x0, tol=1e-5, max_iter=100, alpha=1.0):
"""
Метод функциональной итерации с параметром ускорения
f: функция, корень которой ищем (f(x)=0)
x0: начальное приближение
tol: точность
max_iter: максимальное число итераций
alpha: параметр ускорения (0 < alpha < 2)
"""
def g(x):
return x - alpha * f(x) # Модифицированная итерационная функция
iterations = [x0]
for i in range(max_iter):
x_new = g(iterations[-1])
iterations.append(x_new)
# Критерий остановки по разнице между итерациями
if abs(x_new - iterations[-2]) < tol:break
return np.array(iterations)
# Исходная функция
def f(x):
return x**2 - np.log(x) - 1
x0 = 0.5 # Начальное приближение
result = f_iter(f, x0) # Решаем с параметром ускорения по умолчанию - alpha=1
print(f"Найденный корень: {result[-1]:.8f}")
print(f'Проверка: x^2 - ln(x) - 1 = 0 = {result[-1]**2 - np.log(result[-1]) - 1}')
Найденный корень: 1.00000000 Проверка: x^2 - ln(x) - 1 = 0 = 0.0
Ответы на вопросы:
Функция g(x) в методе функциональной итерации должна удовлетворять условию сжатия (|g'(x)| < 1 в окрестности корня), иначе метод может расходиться или колебаться. Например, для уравнения x = cos(x) хороший выбор — g(x) = cos(x), так как |sin(x)| < 1, что гарантирует сходимость. Плохой выбор g(x) может привести к увеличению ошибки на каждой итерации. Оптимальная g(x) минимизирует |g'(x)|, ускоряя сходимость.
Метод функциональной итерации требует только одного вычисления g(x) за шаг (сложность O(1)), но сходится линейно, а метод секущих (сложность O(n) из-за хранения истории точек) имеет сверхлинейную сходимость (~1.6). Функциональная итерация устойчивее к погрешностям округления, но секущие лучше работают для функций с быстрыми изменениями производной. Для многомерных задач функциональная итерация часто проще в реализации.
Эффективен для задач с явным устойчивым преобразованием g(x), например, для $sin(2x)-ln(x) = 0$. Тогда $\phi(x) = x - m(sin(2x)-ln(x))$. Подберем $m$ так, чтобы на отрезке, где по графику видно, что присутствует корень ($[1.3, 1.5]$) выполнялось условие $|\phi'(x)|<1$. Подобрали: $\phi(x)=x + 1/3(sin(2x)-ln(x))$, тогда схема итераций будет иметь вид $x_k = x_{k-1}+1/3(sin2x_{k-1}-ln(x_{k-1}))$.
2 задание¶
Вид 1 - наивное умножение матриц¶
Пример 1 (билет 5, ПМ23-4) - вывод результирующей матрицы¶
# Наивный метод умножения матриц
def matmul(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
n = 6
a = np.random.rand(n, n)
b = np.random.rand(n, n)
print(a)
print()
print(b)
print('\nОтвет:')
matmul(a, b)
[[0.6343854 0.2283542 0.78079846 0.57523485 0.68539804 0.45848853] [0.09746336 0.57635124 0.03483201 0.97758035 0.07188454 0.18826281] [0.96143104 0.69868373 0.40429305 0.60160329 0.78929402 0.81253199] [0.34967411 0.13995375 0.26687399 0.76480357 0.01254489 0.79931005] [0.09059638 0.9432162 0.4787282 0.16569662 0.7054304 0.84040067] [0.07969433 0.47400189 0.97693115 0.97677892 0.9229398 0.90360221]] [[0.98565467 0.31420819 0.58448143 0.45019765 0.81059223 0.6616072 ] [0.68831543 0.87719375 0.44335734 0.29870349 0.65809297 0.28173744] [0.82017118 0.31149602 0.2600052 0.66472012 0.85006987 0.23659394] [0.28284908 0.07550215 0.81387598 0.08603563 0.87757551 0.37920986] [0.00530739 0.3276801 0.83446359 0.03165778 0.95884374 0.63457135] [0.05306633 0.24988853 0.62416784 0.51189047 0.44118451 0.50687913]] Ответ:
array([[1.61352566, 1.02544937, 2.00132399, 1.17870624, 2.69251909, 1.55424902], [0.80822451, 0.69145479, 1.29467337, 0.42194182, 1.49779063, 0.74685369], [1.97761341, 1.5480069 , 2.63224737, 1.40294899, 3.22604319, 2.06944112], [0.91867961, 0.57736065, 1.4676433 , 0.85198034, 1.63825191, 1.03705315], [1.22637418, 1.45864385, 1.8436691 , 1.10752978, 2.29369366, 1.375404 ], [1.53519494, 1.34712001, 2.63987524, 1.40265138, 3.34780425, 1.83149886]])
Ответы на вопросы:
Объясните, как алгоритм Штрассена уменьшает количество умножений для матриц. Как это влияет на асимптотическую сложность? Алгоритм Штрассена разбивает матрицы на подматрицы и заменяет часть умножений сложениями, используя 7 умножений вместо 8 для рекурсивного перемножения подматриц. Это снижает асимптотическую сложность с $O(n^3)$ до $O(n^{log_27}) \approx O(n^{2.81})$.
Обсудите роль архитектуры памяти в оптимизации матричных операций. Как это влияет на производительность алгоритмов? Архитектура памяти влияет на производительность, так как эффективное использование кэша (например, блочная обработка матриц) уменьшает количество обращений к медленной оперативной памяти. Это особенно важно для алгоритмов с высокой локальностью данных, таких как умножение матриц, где оптимизация доступа к памяти может значительно ускорить выполнение.
Пример 2 (билет 29, ПМ23-4) - подсчет операций умножения¶
# Наивный метод умножения матриц
def matmul(a, b):
n = a.shape[0]
k = a.shape[1]
m = b.shape[1]
c = np.zeros((n, m))
operations = 0
for i in range(n):
for j in range(m):
for s in range(k):
c[i, j] += a[i, s] * b[s, j]
operations += 1
return c, operations
n = 4
a = np.random.rand(n, n)
b = np.random.rand(n, n)
res, c = matmul(a, b)
print(a)
print()
print(b)
print('\nКол-во операций умножения:', c)
print('\nОтвет:')
res
[[0.94357269 0.52955467 0.12243984 0.49844277] [0.2970558 0.37265632 0.92956913 0.0371651 ] [0.23672821 0.88956727 0.73950668 0.90644946] [0.67578725 0.11677413 0.70173168 0.93174876]] [[0.75971267 0.80955604 0.28276002 0.9142003 ] [0.32968135 0.35981438 0.26533125 0.41830757] [0.65207406 0.20387419 0.93485844 0.49741784] [0.39940876 0.1507599 0.0821538 0.05818264]] Кол-во операций умножения: 64 Ответ:
array([[1.17035068, 1.05452386, 0.56272492, 1.17403563], [0.96952688, 0.56968858, 1.05494167, 0.89200009], [1.31737614, 0.7991464 , 1.0687696 , 1.00911316], [1.38163204, 0.87263998, 0.95463593, 1.06991786]])
Ответы на вопросы:
Реализован наивный алгоритм перемножения матриц, подсчитано кол-во операций умножения. Как это кол-во изменилось бы при использовании алгоритма Штрассена? В наивном алгоритме для матриц n×n требуется $n^3$ умножений. Алгоритм Штрассена рекурсивно разбивает матрицы на подматрицы и выполняет 7 умножений вместо 8 на каждом уровне рекурсии, уменьшая общее количество умножений с $O(n^3)$ до $O(n^{log_27}) \approx O(n^{2.81})$. Это даёт значительное сокращение операций для больших матриц, хотя на практике из-за накладных расходов его применяют только для крупных размеров.
Как архитектура памяти влияет на производительность алгоритмов умножения матриц? Производительность зависит от эффективного использования кэша: если данные часто перезагружаются из оперативной памяти, возникают задержки. Блочные алгоритмы (например, разбиение матриц на подматрицы, помещающиеся в кэш) уменьшают промахи кэша и ускоряют вычисления. Оптимизация порядка доступа к элементам (например, обход по строкам) также улучшает локальность данных, что критично для современных процессоров с иерархией памяти.
Как суммирование по Кахану может улучшить точность? Суммирование по Кахану компенсирует ошибки округления при сложении чисел с плавающей запятой, накапливая и учитывая "потерянные" младшие разряды на каждом шаге. Это особенно полезно для больших последовательностей чисел, где стандартное суммирование может накапливать значительную погрешность. Метод требует дополнительных операций, но существенно повышает точность результата.
Пример 3 (билет 7, ПМ23-5-6?) - вывод результата¶
# Наивный метод умножения матриц
def matmul(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
n = 8
a = np.random.rand(n, n)
b = np.random.rand(n, n)
print(a)
print()
print(b)
print('\nОтвет:')
matmul(a, b)
[[0.40077288 0.3284152 0.96625737 0.85526935 0.44189115 0.93110174 0.15349173 0.38326883] [0.96140596 0.08331655 0.63361239 0.37397645 0.84139407 0.79651344 0.61312069 0.69437939] [0.77024312 0.93864652 0.75793779 0.93441175 0.64596212 0.35173443 0.89829603 0.24277838] [0.19746207 0.03642115 0.11123077 0.79724088 0.17220937 0.09466147 0.9683665 0.66714619] [0.28595614 0.58493206 0.22906904 0.86512651 0.43900414 0.55250199 0.35420713 0.65735462] [0.16115445 0.51550456 0.09324324 0.46720277 0.3235976 0.17773772 0.35966425 0.19192405] [0.82764832 0.18151739 0.43129484 0.87293672 0.84102896 0.837963 0.32889702 0.4612443 ] [0.59016761 0.08412471 0.46518386 0.32176017 0.811097 0.53683637 0.15117239 0.82613947]] [[0.32827954 0.16768798 0.2676136 0.86770171 0.53651055 0.26073433 0.97380965 0.77376743] [0.24681916 0.6222142 0.52440782 0.1248041 0.41732193 0.2672935 0.54673832 0.40498611] [0.35369527 0.29713994 0.55319137 0.82675876 0.52429059 0.5963656 0.7994478 0.47418235] [0.17085529 0.1657843 0.79468816 0.05853952 0.57459686 0.39786797 0.72419146 0.10557029] [0.19985515 0.29301079 0.63186373 0.30887139 0.11524299 0.27153806 0.49590289 0.13357773] [0.0158176 0.84459106 0.94122063 0.16848088 0.27972633 0.4526827 0.6168832 0.32203086] [0.14578816 0.95981429 0.31969939 0.02189325 0.61967582 0.32441314 0.38371695 0.97832206] [0.03656692 0.03114634 0.50908659 0.33100029 0.24913604 0.89133383 0.11879254 0.75385506]] Ответ:
array([[0.8399469 , 1.77559333, 2.89344882, 1.66125057, 1.85208818, 2.04170455, 2.85962772, 1.78954561], [0.91970851, 1.99270298, 2.77953555, 2.02769165, 1.97035852, 2.20646602, 2.98550672, 2.60975961], [1.18676024, 2.44942874, 3.01021135, 1.825626 , 2.5292202 , 2.11794648, 3.45673412, 2.69557272], [0.45085291, 1.3016355 , 1.61416441, 0.62568026, 1.45015787, 1.44316489, 1.47310636, 1.80824148], [0.6392312 , 1.57911284, 2.44280081, 1.01517662, 1.60312701, 1.78186843, 2.18045431, 1.73674768], [0.41988076, 1.04906099, 1.32077401, 0.50990715, 0.97663293, 0.87737766, 1.28259203, 1.02401813], [0.86434889, 1.80881625, 2.90906538, 1.70930719, 1.89754888, 1.99436161, 2.99717518, 2.06225916], [0.6568532 , 1.20477111, 2.20177772, 1.54375244, 1.32365164, 1.83046927, 2.11514859, 1.79717918]])
Ответы на вопросы:
Как алгоритм Штрассена оптимизирует умножение матриц и как это влияет на сложность в нотации big-O? Алгоритм Штрассена использует рекурсивное разбиение матриц на подматрицы и заменяет часть умножений сложениями, сокращая количество операций с 8 до 7 на каждом уровне рекурсии. Это уменьшает асимптотическую сложность с $O(n^3)$ до $O(n^{log_27}) \approx O(n^{2.81})$. Однако для маленьких матриц накладные расходы на рекурсию и дополнительные сложения делают его менее эффективным, чем наивный метод.
Почему для маленьких матриц алгоритм Штрассена может быть менее эффективен? Для малых матриц накладные расходы (рекурсия, управление подматрицами, дополнительные сложения) начинают преобладать над выигрышем от сокращения умножений. Метод Штрассена становится быстрее наивного алгоритма, если матрица большая:
$2 n^3 > 7 n^{log_2 7}$
$n > 667$Как архитектура памяти влияет на оптимизацию матричных операций? Эффективность умножения матриц сильно зависит от локальности данных: блочная обработка подматриц, которые помещаются в кэш, уменьшает количество обращений к оперативной памяти. Оптимизированный порядок обхода элементов (например, по строкам) также улучшает использование кэша, что критично для производительности на современных процессорах.
Как ошибки представления чисел в формате IEEE 754 влияют на точность умножения матриц? При умножении матриц ошибки округления накапливаются из-за ограниченной точности чисел с плавающей запятой. Особенно это заметно при работе с большими матрицами или плохо обусловленными задачами. Методы вроде компенсационного суммирования (Кахана) или использование повышенной точности могут частично решить эту проблему.
Пример 4 (билет 30, ПМ23-5-6?) - матрицы заданы¶
# Наивный метод умножения матриц
def matmul(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
n = 8
a = np.array([[3.0, 5.0, 2.0, 1.0],
[6.0, 4.0, 1.0, 5.0],
[1.0, 7.0, 3.0, 2.0],
[3.0, 2.0, 5.0, 4.0]])
b = np.array([[2.0, 5.0, 3.0, 0.0],
[3.0, 7.0, 4.0, 1.0],
[4.0, 3.0, 5.0, 3.0],
[4.0, 2.0, 3.0, 3.0]])
print(a)
print()
print(b)
print('\nОтвет:')
matmul(a, b)
[[3. 5. 2. 1.] [6. 4. 1. 5.] [1. 7. 3. 2.] [3. 2. 5. 4.]] [[2. 5. 3. 0.] [3. 7. 4. 1.] [4. 3. 5. 3.] [4. 2. 3. 3.]] Ответ:
array([[33., 58., 42., 14.], [48., 71., 54., 22.], [43., 67., 52., 22.], [48., 52., 54., 29.]])
# для проверки
a @ b
array([[33., 58., 42., 14.], [48., 71., 54., 22.], [43., 67., 52., 22.], [48., 52., 54., 29.]])
Ответы на вопросы:
Какова асимптотическая сложность наивного алгоритма и алгоритма Штрассена для матрицы nxn? Наивный алгоритм имеет сложность $O(n^3)$, так как требует тройного вложенного цикла. Алгоритм Штрассена снижает сложность до $O(n^{log_27}) \approx O(n^{2.81})$ за счёт рекурсивного разбиения и сокращения числа умножений.
Как Штрассен уменьшает количество операций? Алгоритм разбивает матрицы на 4 подматрицы и комбинирует их так, чтобы вместо 8 рекурсивных умножений выполнять 7, используя дополнительные сложения и вычитания. Это уменьшает общее число операций на каждом уровне рекурсии.
Почему для малых n преимущество незначительное? Для малых матриц накладные расходы (рекурсия, дополнительные сложения/вычитания) перевешивают выигрыш от сокращения умножений. Пороговый размер (обычно n > 667) зависит от реализации и аппаратных особенностей.
Как архитектура памяти влияет на производительность алгоритмов умножения матриц? Эффективность зависит от локальности данных: блочная обработка подматриц, помещающихся в кэш, уменьшает задержки доступа к оперативной памяти. Оптимизация порядка обхода (например, по строкам) также улучшает использование кэша, ускоряя вычисления.
Вид 2 - поиск наибольшего собственного значения методом степеней¶
Пример 1 (билет 18, ПМ23-4) -¶
A = np.array([[2, -1, 0],
[-1, 2, -1],
[0, -1, 2]])
x = np.array([[1], [1], [1]])
tol = 1e-5
max_iter = 100
lam_prev = 0
for i in range(max_iter):
# Умножение матрицы на вектор (замена A @ x)
Ax = np.array([
[A[0,0]*x[0,0] + A[0,1]*x[1,0] + A[0,2]*x[2,0]],
[A[1,0]*x[0,0] + A[1,1]*x[1,0] + A[1,2]*x[2,0]],
[A[2,0]*x[0,0] + A[2,1]*x[1,0] + A[2,2]*x[2,0]]])
# Нормализация (замена np.linalg.norm)
norm_Ax = np.sqrt(Ax[0,0]**2 + Ax[1,0]**2 + Ax[2,0]**2)
x = Ax / norm_Ax
# Вычисление λ (замена x.T @ A @ x и x.T @ x)
xT_Ax = (x[0,0]*(A[0,0]*x[0,0] + A[0,1]*x[1,0] + A[0,2]*x[2,0]) +
x[1,0]*(A[1,0]*x[0,0] + A[1,1]*x[1,0] + A[1,2]*x[2,0]) +
x[2,0]*(A[2,0]*x[0,0] + A[2,1]*x[1,0] + A[2,2]*x[2,0]))
xT_x = x[0,0]**2 + x[1,0]**2 + x[2,0]**2
lam = xT_Ax / xT_x # Рэлеевское отношение
if abs(lam - lam_prev) < tol: # Проверка условия сходимости
break
lam_prev = lam
print("Найденное максимальное собственное значение:", float(lam))
print("Соответствующий собственный вектор:")
print(x)
Найденное максимальное собственное значение: 3.4142134998513236 Соответствующий собственный вектор: [[ 0.50007433] [-0.70700164] [ 0.50007433]]
# Для проверки
A = np.array([[2, -1, 0], [-1, 2, -1], [0, -1, 2]])
x = np.array([[1, 1, 1]]).T
tol = 1e-5
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(float(lam))
print(x)
3.4142134998513236 [[ 0.50007433] [-0.70700164] [ 0.50007433]]
C:\Users\tanya\AppData\Local\Temp\ipykernel_13676\2079210799.py:19: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.) print(float(lam))
Ответы на вопросы:
Что такое спектральный радиус матрицы и как он связан с собственными значениями? Спектральный радиус матрицы — это максимальный модуль её собственных значений. Он показывает, насколько быстро могут расти или убывать величины при умножении на эту матрицу, что важно для анализа устойчивости и сходимости итерационных методов.
Как зазор между собственными значениями влияет на сходимость? Чем больше зазор (разница) между модулями наибольшего и остальных собственных значений, тем быстрее сходится итерационный процесс (например, степенной метод). Малый зазор замедляет сходимость, так как доминирующее значение слабо выражено.
Как круги Гершгорина помогают оценить собственные значения? Круги Гершгорина — это области на комплексной плоскости, в которых гарантированно лежат собственные значения матрицы. Их анализ позволяет локализовать спектр матрицы и оценить распределение собственных значений без точного вычисления.
Пример 2 (билет ?, ПМ23-5-6?) -¶
A = np.array([[10, 3, 4, 5, 6],
[3, 20, 7, 8, 9],
[4, 7, 30, 11, 12],
[5, 8, 11, 40, 13],
[6, 9, 12, 13, 50]])
x = np.array([[1], [1], [1], [1], [1]])
tol = 1e-4
max_iter = 1000
lam_prev = 0
for i in range(max_iter):
# Умножение матрицы на вектор (замена A @ x)
Ax = np.array([
[A[0,0]*x[0,0] + A[0,1]*x[1,0] + A[0,2]*x[2,0] + A[0,3]*x[3,0] + A[0,4]*x[4,0]],
[A[1,0]*x[0,0] + A[1,1]*x[1,0] + A[1,2]*x[2,0] + A[1,3]*x[3,0] + A[1,4]*x[4,0]],
[A[2,0]*x[0,0] + A[2,1]*x[1,0] + A[2,2]*x[2,0] + A[2,3]*x[3,0] + A[2,4]*x[4,0]],
[A[3,0]*x[0,0] + A[3,1]*x[1,0] + A[3,2]*x[2,0] + A[3,3]*x[3,0] + A[3,4]*x[4,0]],
[A[4,0]*x[0,0] + A[4,1]*x[1,0] + A[4,2]*x[2,0] + A[4,3]*x[3,0] + A[4,4]*x[4,0]]])
# Нормализация (замена np.linalg.norm)
norm_Ax = np.sqrt(Ax[0,0]**2 + Ax[1,0]**2 + Ax[2,0]**2 + Ax[3,0]**2 + Ax[4,0]**2)
x = Ax / norm_Ax
# Вычисление λ (замена x.T @ A @ x и x.T @ x)
xT_Ax = 0.0
for j in range(5):
for k in range(5):
xT_Ax += x[j,0] * A[j,k] * x[k,0]
xT_x = x[0,0]**2 + x[1,0]**2 + x[2,0]**2 + x[3,0]**2 + x[4,0]**2
lam = xT_Ax / xT_x # Рэлеевское отношение
if abs(lam - lam_prev) < tol: # Проверка условия сходимости
break
lam_prev = lam
print("Найденное максимальное собственное значение:", float(lam))
print("Соответствующий собственный вектор:")
print(x)
Найденное максимальное собственное значение: 71.35584219131783 Соответствующий собственный вектор: [[0.14895115] [0.26523459] [0.39839561] [0.51876575] [0.69255123]]
# Для проверки
A = np.array([[10, 3, 4, 5, 6],
[3, 20, 7, 8, 9],
[4, 7, 30, 11, 12],
[5, 8, 11, 40, 13],
[6, 9, 12, 13, 50]])
x = np.array([[1, 1, 1, 1, 1]]).T
tol = 1e-4
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(float(lam))
print(x)
71.35584219131782 [[0.14895115] [0.26523459] [0.39839561] [0.51876575] [0.69255123]]
C:\Users\tanya\AppData\Local\Temp\ipykernel_13676\178361654.py:24: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.) print(float(lam))
Ответы на вопросы:
Как зазор между собственными значениями влияет на сходимость? Чем больше зазор между наибольшим и остальными собственными значениями, тем быстрее сходится итерационный метод (например, степенной метод). Малый зазор приводит к медленной сходимости, так как второе по величине значение начинает существенно влиять на процесс.
В чем преимущество использования Line Profiler при анализе кода для вычисления собственных значений? Line Profiler позволяет точно определить, какие строки кода требуют наибольших вычислительных затрат, что помогает оптимизировать алгоритмы (например, ускорить итерационные методы или сократить избыточные операции). Это особенно полезно при работе с большими матрицами.
Как связана архитектура памяти с вычислением собственных значений? Эффективность алгоритмов зависит от доступа к данным: оптимизация работы с кэшем (например, блочная обработка матриц) ускоряет итерационные методы. Плохая локальность данных увеличивает время вычислений из-за частых обращений к оперативной памяти.
Пример 3 (билет 3, ПМ23-5-6?) -¶
A = np.array([[5, 1, 2, 3, 4],
[1, 6, 10, 11, 12],
[2, 10, 7, 13, 14],
[3, 11, 13, 8, 15],
[4, 12, 14, 15, 9]])
x = np.array([[1], [1], [1], [1], [1]])
tol = 1e-4
max_iter = 1000
lam_prev = 0
for i in range(max_iter):
# Умножение матрицы на вектор (замена A @ x)
Ax = np.array([
[A[0,0]*x[0,0] + A[0,1]*x[1,0] + A[0,2]*x[2,0] + A[0,3]*x[3,0] + A[0,4]*x[4,0]],
[A[1,0]*x[0,0] + A[1,1]*x[1,0] + A[1,2]*x[2,0] + A[1,3]*x[3,0] + A[1,4]*x[4,0]],
[A[2,0]*x[0,0] + A[2,1]*x[1,0] + A[2,2]*x[2,0] + A[2,3]*x[3,0] + A[2,4]*x[4,0]],
[A[3,0]*x[0,0] + A[3,1]*x[1,0] + A[3,2]*x[2,0] + A[3,3]*x[3,0] + A[3,4]*x[4,0]],
[A[4,0]*x[0,0] + A[4,1]*x[1,0] + A[4,2]*x[2,0] + A[4,3]*x[3,0] + A[4,4]*x[4,0]]])
# Нормализация (замена np.linalg.norm)
norm_Ax = np.sqrt(Ax[0,0]**2 + Ax[1,0]**2 + Ax[2,0]**2 + Ax[3,0]**2 + Ax[4,0]**2)
x = Ax / norm_Ax
# Вычисление λ (замена x.T @ A @ x и x.T @ x)
xT_Ax = 0.0
for j in range(5):
for k in range(5):
xT_Ax += x[j,0] * A[j,k] * x[k,0]
xT_x = x[0,0]**2 + x[1,0]**2 + x[2,0]**2 + x[3,0]**2 + x[4,0]**2
lam = xT_Ax / xT_x # Рэлеевское отношение
if abs(lam - lam_prev) < tol: # Проверка условия сходимости
break
lam_prev = lam
print("Найденное максимальное собственное значение:", float(lam))
print("Соответствующий собственный вектор:")
print(x)
Найденное максимальное собственное значение: 45.98518621508526 Соответствующий собственный вектор: [[0.12523968] [0.43016137] [0.48486562] [0.51596307] [0.54586047]]
# Для проверки
A = np.array([[5, 1, 2, 3, 4],
[1, 6, 10, 11, 12],
[2, 10, 7, 13, 14],
[3, 11, 13, 8, 15],
[4, 12, 14, 15, 9]])
x = np.array([[1, 1, 1, 1, 1]]).T
tol = 1e-4
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(float(lam))
print(x)
45.985186215085264 [[0.12523968] [0.43016137] [0.48486562] [0.51596307] [0.54586047]]
C:\Users\tanya\AppData\Local\Temp\ipykernel_13676\1254878436.py:24: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.) print(float(lam))
Ответы на вопросы:
Как метод степеней использует итерации для нахождения доминирующего собственного значения? Метод степеней последовательно умножает матрицу на начальный вектор, и через несколько итераций вектор выравнивается по направлению собственного вектора, соответствующего наибольшему по модулю собственному значению. Это значение затем оценивается как отношение норм результатов двух последовательных итераций.
Как отношение Релея улучшает оценку? Отношение Релея использует текущий приближённый собственный вектор для более точной оценки собственного значения, вычисляя $(v^TAv)/(v^Tv)$. Это даёт квадратичную сходимость вблизи истинного значения, улучшая точность по сравнению с простым методом степеней.
Как зазор между собственными значениями влияет на сходимость? Чем больше зазор между наибольшим и вторым по величине собственными значениями, тем быстрее сходится метод, так как влияние доминирующего значения становится более выраженным. Малый зазор замедляет сходимость, требуя больше итераций.
Как модификации, такие как сдвиги, решают эти проблемы? Сдвиги позволяют изменить спектр матрицы, делая целевое собственное значение доминирующим. Это ускоряет сходимость для конкретных значений и помогает в случаях, когда зазор между собственными значениями мал.
Вид 3 - QR-разложение¶
Пример 1 (билет 14, ПМ23-6?)¶
from scipy.sparse import eye
A = np.array([
[15, 2, 3, 4, 5],
[2, 25, 6, 7, 8],
[3, 6, 35, 9, 10],
[4, 7, 9, 45, 11],
[5, 8, 10, 11, 55]], dtype=float)
n = A.shape[0] # Получаем размерность матрицы
Q = np.zeros_like(A) # Создаем матрицу Q того же размера, что и A, заполненную нулями
R = np.zeros_like(A) # Создаем матрицу R того же размера, что и A, заполненную нулями
# Процесс Грама-Шмидта
for j in range(n):
v = A[:, j].copy() # Берем j-й столбец матрицы A
# Ортогонализация относительно предыдущих столбцов Q
for i in range(j):
# Вычисляем проекцию (заменяем @ и np.dot)
R[i, j] = sum(Q[k,i] * A[k,j] for k in range(n))
v -= R[i, j] * Q[:, i]
# Нормализация вектора (заменяем np.linalg.norm)
norm_v = np.sqrt(sum(v[k]**2 for k in range(n)))
R[j,j] = norm_v
Q[:,j] = v / norm_v
# Автоматическая проверка с помощью встроенной функции (для сравнения)
# РАЗНЫЕ ЗНАКИ - ЭТО НОРМ, ВСЁ ПРАВИЛЬНО
Q_auto, R_auto = np.linalg.qr(A)
print("\nQ (numpy.linalg.qr):\n", np.round(Q_auto, 6))
print("\nR (numpy.linalg.qr):\n", np.round(R_auto, 6))
print()
print(Q)
print()
print(R)
Q (numpy.linalg.qr): [[-0.898027 0.265701 0.206336 0.190293 -0.210156] [-0.119737 -0.913589 0.262185 0.207143 -0.198419] [-0.179605 -0.161731 -0.933863 0.195274 -0.177069] [-0.239474 -0.177272 -0.092669 -0.939346 -0.142366] [-0.299342 -0.192812 -0.089429 -0.019422 0.929969]] R (numpy.linalg.qr): [[-16.703293 -9.93816 -14.847372 -20.115794 -26.342111] [ 0. -26.062098 -13.868586 -16.886055 -20.152172] [ 0. 0. -32.221389 -10.897967 -12.147421] [ 0. 0. 0. -38.515585 -6.83969 ] [ 0. 0. 0. 0. 45.173465]] [[ 0.89802651 -0.26570121 -0.20633603 -0.19029252 -0.21015579] [ 0.11973687 0.91358859 -0.26218483 -0.20714283 -0.19841856] [ 0.1796053 0.16173117 0.93386263 -0.19527443 -0.17706906] [ 0.23947374 0.17727167 0.09266941 0.93934619 -0.1423664 ] [ 0.29934217 0.19281216 0.08942891 0.01942239 0.92996934]] [[16.70329309 9.93816005 14.84737163 20.11579383 26.34211096] [ 0. 26.06209844 13.86858565 16.88605477 20.15217181] [ 0. 0. 32.22138867 10.89796719 12.14742084] [ 0. 0. 0. 38.51558521 6.83968983] [ 0. 0. 0. 0. 45.17346535]]
Ответы на вопросы:
Как разложение по собственным векторам используется в методе главных компонент (PCA)? В PCA сначала строится ковариационная матрица данных, затем находятся её собственные векторы, соответствующие наибольшим собственным значениям. Эти векторы задают направления максимальной дисперсии данных и используются для проекции исходных данных в новое пространство меньшей размерности, сохраняя основную информацию.
Как это связано с вычислением SVD (сингулярного разложения)? SVD матрицы данных напрямую даёт главные компоненты: правые сингулярные векторы (столбцы V) соответствуют собственным векторам ковариационной матрицы, а сингулярные значения связаны с собственными значениями как $σᵢ² = λᵢ$. Таким образом, SVD позволяет вычислить PCA более эффективно, избегая явного построения ковариационной матрицы, что особенно полезно для больших матриц.
Вид 4 - умножение матриц Штрассеном¶
Пример 1 (билет 15, ПМ23-5) - умножение и график зависимости¶
def matrix_add(a, b):
"""Сложение матриц без использования np.add"""
return a + b
def matrix_sub(a, b):
"""Вычитание матриц без использования np.subtract"""
return a - b
def naive_multiply(a, b):
"""Наивное умножение матриц без использования @ или np.dot"""
n = a.shape[0]
result = np.zeros((n, n))
for i in range(n):
for j in range(n):
for k in range(n):
result[i,j] += a[i,k] * b[k,j]
return result
def strassen(A, B, leaf_size=64):
n = A.shape[0]
# Базовый случай - используем наивное умножение
if n <= leaf_size:
return naive_multiply(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:]
# Вычисляем 7 произведений по схеме Штрассена
M1 = strassen(matrix_add(a11, a22), matrix_add(b11, b22), leaf_size)
M2 = strassen(matrix_add(a21, a22), b11, leaf_size)
M3 = strassen(a11, matrix_sub(b12, b22), leaf_size)
M4 = strassen(a22, matrix_sub(b21, b11), leaf_size)
M5 = strassen(matrix_add(a11, a12), b22, leaf_size)
M6 = strassen(matrix_sub(a21, a11), matrix_add(b11, b12), leaf_size)
M7 = strassen(matrix_sub(a12, a22), matrix_add(b21, b22), leaf_size)
# Вычисляем блоки результирующей матрицы
c11 = matrix_add(matrix_sub(matrix_add(M1, M4), M7), M5)
c12 = matrix_add(M3, M5)
c21 = matrix_add(M2, M4)
c22 = matrix_add(matrix_sub(matrix_add(M1, M3), M2), M6)
# Собираем результирующую матрицу
C = np.zeros((n, n))
C[:mid, :mid] = c11
C[:mid, mid:] = c12
C[mid:, :mid] = c21
C[mid:, mid:] = c22
return C
A = np.array([
[3, 5, 2, 1],
[6, 4, 1, 5],
[1, 7, 3, 2],
[3, 2, 5, 4]])
B = np.array([
[2, 5, 3, 0],
[3, 7, 4, 1],
[4, 3, 5, 3],
[4, 2, 3, 3]])
# Умножаем матрицы методом Штрассена
result = strassen(A, B)
print("Результат умножения:")
print(result)
# Проверка корректности
print("\nПроверка:")
print(A @ B)
Результат умножения: [[33. 58. 42. 14.] [48. 71. 54. 22.] [43. 67. 52. 22.] [48. 52. 54. 29.]] Проверка: [[33 58 42 14] [48 71 54 22] [43 67 52 22] [48 52 54 29]]
# Анализ производительности
sizes = [2**i for i in range(1, 8)] # Размеры матриц от 2x2 до 128x128
times = []
for size in sizes:
# Генерируем случайные матрицы
a = np.random.rand(size, size)
b = np.random.rand(size, size)
# Замеряем время выполнения
start = time.time()
strassen_multiply(a, b)
times.append(time.time() - start)
# Строим график
plt.figure(figsize=(10, 6))
plt.plot(sizes, times, 'o-')
plt.xlabel('Размер матрицы')
plt.ylabel('Время выполнения (сек)')
plt.title('Производительность метода Штрассена')
plt.show()
Ответы на вопросы:
Как алгоритм Штрассена уменьшает количество умножений? Алгоритм Штрассена разбивает матрицы на подматрицы 2×2 и заменяет стандартные 8 умножений на 7 специально подобранных комбинаций сложений/умножений, используя предварительно вычисленные промежуточные значения. Это сокращает количество рекурсивных умножений на каждом уровне разбиения.
Влияние на асимптотическую сложность Благодаря сокращению операций с 8 до 7 на каждом уровне рекурсии, сложность уменьшается с $O(n^3)$ до $O(n^{log_27}) \approx O(n^{2.81})$, что даёт выигрыш для больших матриц (n>667).
Роль архитектуры памяти Алгоритм Штрассена требует дополнительной памяти для хранения промежуточных подматриц, что может ухудшить производительность из-за частых промахов кэша.
Применение Используется в высокопроизводительных вычислениях для больших матриц, графических движках (расчёты преобразований), библиотеках линейной алгебры. Ограничение: Из-за накладных расходов на малых матрицах часто комбинируется с наивным методом.
Пример 2 (билет 4, ПМ23-5-6) - просто Штрассен¶
def matrix_add(a, b):
"""Сложение матриц без использования np.add"""
return a + b
def matrix_sub(a, b):
"""Вычитание матриц без использования np.subtract"""
return a - b
def naive_multiply(a, b):
"""Наивное умножение матриц без использования @ или np.dot"""
n = a.shape[0]
result = np.zeros((n, n))
for i in range(n):
for j in range(n):
for k in range(n):
result[i,j] += a[i,k] * b[k,j]
return result
def strassen(A, B, leaf_size=64):
n = A.shape[0]
# Базовый случай - используем наивное умножение
if n <= leaf_size:
return naive_multiply(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:]
# Вычисляем 7 произведений по схеме Штрассена
M1 = strassen(matrix_add(a11, a22), matrix_add(b11, b22), leaf_size)
M2 = strassen(matrix_add(a21, a22), b11, leaf_size)
M3 = strassen(a11, matrix_sub(b12, b22), leaf_size)
M4 = strassen(a22, matrix_sub(b21, b11), leaf_size)
M5 = strassen(matrix_add(a11, a12), b22, leaf_size)
M6 = strassen(matrix_sub(a21, a11), matrix_add(b11, b12), leaf_size)
M7 = strassen(matrix_sub(a12, a22), matrix_add(b21, b22), leaf_size)
# Вычисляем блоки результирующей матрицы
c11 = matrix_add(matrix_sub(matrix_add(M1, M4), M7), M5)
c12 = matrix_add(M3, M5)
c21 = matrix_add(M2, M4)
c22 = matrix_add(matrix_sub(matrix_add(M1, M3), M2), M6)
# Собираем результирующую матрицу
C = np.zeros((n, n))
C[:mid, :mid] = c11
C[:mid, mid:] = c12
C[mid:, :mid] = c21
C[mid:, mid:] = c22
return C
A = np.array([
[4, 2, 2, 0],
[6, 1, 9, 1],
[1, 3, 3, 2],
[2, 0, 5, 4]])
B = np.array([
[2, 5, 8, 0],
[3, 2, 4, 1],
[3, 1, 5, 2],
[4, 6, 5, 3]])
# Умножаем матрицы методом Штрассена
result = strassen(A, B)
print("Результат умножения:")
print(result)
# Проверка корректности
print("\nПроверка:")
print(A @ B)
Результат умножения: [[ 20. 26. 50. 6.] [ 46. 47. 102. 22.] [ 28. 26. 45. 15.] [ 35. 39. 61. 22.]] Проверка: [[ 20 26 50 6] [ 46 47 102 22] [ 28 26 45 15] [ 35 39 61 22]]
Ответы на вопросы:
Как архитектура памяти компьютера влияет на производительность алгоритма Штрассена? Алгоритм Штрассена чувствителен к организации памяти: его рекурсивная структура вызывает частые промахи кэша из-за случайного доступа к подматрицам. Оптимизации (блочная обработка, подбор размера под кэш) могут улучшить локальность данных и ускорить вычисления.
Как ошибки представления чисел в IEEE 754 влияют на точность умножения матриц? Округление при операциях с плавающей запятой накапливается, особенно в алгоритме Штрассена из-за дополнительных сложений/вычитаний. Это может увеличить погрешность результата по сравнению с наивным методом, где меньше промежуточных вычислений.
3 задание¶
Вид 1 - сгенерировать сигнал, ДПФ/БПФ, амплитудный спектр¶
Пример 1 (билет 4, ПМ23-4) - поиск значимых компонент¶
Я запуталась с условием( Как может быть сигнал длины 64, если n перечислены конкретно и их 8 - от 0 до 7?
Я сделала 2 способа, но если кто-то подскажет, что имеется в виду, будет очень круто!
# Генерация сигнала длины 64
n = np.arange(64) # n = 0, 1, ... , 63
x = np.cos(2*np.pi*n/8) + 0.3*np.sin(2*np.pi*2*n/8)
# Визуализация сигнала
plt.figure(figsize=(10, 4))
n_plot = np.linspace(0, 64, 300)
x_plot = np.cos(2*np.pi*n_plot/8) + 0.3*np.sin(2*np.pi*2*n_plot/8)
plt.plot(n_plot, x_plot)
plt.ylabel('Amplitude')
plt.xlabel('Time samples (n)')
plt.title('Generated Signal (64 points)')
plt.show()
# Реализация ДПФ
def DFT(x):
N = len(x)
X = np.zeros(N, dtype=complex)
for k in range(N):
for n in range(N):
X[k] += x[n] * np.exp(-2j*np.pi*k*n/N)
return X
X = DFT(x)
# Построение спектра (sr = N для упрощения)
N = len(X)
sr = N # Частота дискретизации = длина сигнала
freq = np.arange(N) # Ось частот: 0, 1, 2, ..., N-1
# Односторонний спектр
n_oneside = N // 2
f_oneside = freq[:n_oneside]
X_oneside = X[:n_oneside] / n_oneside
plt.figure(figsize=(8, 6))
plt.stem(f_oneside, np.abs(X_oneside), 'b', markerfmt=' ', basefmt='-b')
plt.xlabel('Frequency (bins)')
plt.ylabel('Amplitude |X(freq)|')
plt.title('One-Sided Amplitude Spectrum')
plt.grid()
plt.show()
# Наиболее значимые компоненты
print("Наиболее значимые частотные компоненты:")
for k in range(n_oneside):
amp = np.abs(X[k]) / n_oneside
if amp > 0.1:
print(f"Бин {k}: частота {freq[k]}, амплитуда {amp:.2f}")
Наиболее значимые частотные компоненты: Бин 8: частота 8, амплитуда 1.00 Бин 16: частота 16, амплитуда 0.30
# Генерация сигнала
n = np.arange(8) # n = 0,1,...,7
x = np.cos(2*np.pi*n/8) + 0.3*np.sin(2*np.pi*2*n/8)
# Визуализация сигнала
plt.figure(figsize=(10, 4))
n_plot = np.linspace(0, 7, 100)
x_plot = np.cos(2*np.pi*n_plot/8) + 0.3*np.sin(2*np.pi*2*n_plot/8)
plt.plot(n_plot, x_plot, 'r')
plt.ylabel('Amplitude')
plt.xlabel('Time samples (n)')
plt.title('Generated Signal')
plt.show()
# Реализация ДПФ без использования @
def DFT(x):
N = len(x)
X = np.zeros(N, dtype=complex)
for k in range(N):
for n in range(N):
X[k] += x[n] * np.exp(-2j*np.pi*k*n/N)
return X
X = DFT(x)
N = len(X)
sr = 8
n = np.arange(N)
T = N/sr
freq = n/T
n_oneside = N // 2
f_oneside = freq[:n_oneside]
X_oneside = X[:n_oneside]/n_oneside
plt.figure(figsize = (8, 6))
plt.stem(f_oneside, abs(X_oneside), 'b', markerfmt = ' ', basefmt = '-b')
plt.xlim(0, 10)
plt.xlabel('Frequency, HZ')
plt.ylabel('Amplitude |X(freq)|')
plt.title('One-Sided Amplitude Spectrum')
plt.show()
# Нахождение наиболее значимых компонентов
print("Наиболее значимые частотные компоненты:")
for k, amp in enumerate(np.abs(X_oneside)):
if amp > 0.1: # Порог значимости
print(f"Частота {f_oneside[k]:.2f} (бины {k}): амплитуда {amp:.2f}")
Наиболее значимые частотные компоненты: Частота 1.00 (бины 1): амплитуда 1.00 Частота 2.00 (бины 2): амплитуда 0.30
Ответы на вопросы:
Когда длина сигнала ограничена, ДПФ не может хорошо различать близкие частоты — они сливаются в один широкий пик. Улучшить разрешение можно: увеличивая длину сигнала (больше периодов для анализа), применяя оконные функции (Ханна, Хэмминга), используя метод дополнения нулями для интерполяции спектра.
БПФ в анализе сезонности временных рядов БПФ быстро вычисляет спектр временного ряда, выявляя скрытые периодические компоненты (например, суточные, годовые циклы). Пики в спектре соответствуют частотам сезонности. Например, для месячных данных пик на частоте 1/12 указывает на годовой цикл. Фильтрация шума и выделение значимых гармоник упрощает моделирование тенденций.
Влияние длины сигнала на точность. Короткие сигналы: Низкое частотное разрешение, большая погрешность в определении частот. Длинные сигналы: Повышают разрешение, но требуют больше вычислений. Однако если сигнал не содержит полных периодов, возникает погрешность из-за утечки спектра, даже при zero-padding. Компромисс: выбор длины, кратной основным периодам (например, целое число лет для климатических данных), минимизирует ошибки.
Пример 2 (билет 29, ПМ23-4) - поиск амплитуд компонент с указанными частотами¶
# Генерация сигнала
n = np.arange(16) # n = 0,1,...,15
x = np.cos(2*np.pi*2*n/16) + 0.4*np.sin(2*np.pi*3*n/16)
# Визуализация сигнала
plt.figure(figsize=(6, 3))
n_plot = np.linspace(0, 16, 100)
x_plot = np.cos(2*np.pi*2*n_plot/16) + 0.4*np.sin(2*np.pi*3*n_plot/16)
plt.plot(n_plot, x_plot, 'r')
plt.ylabel('Amplitude')
plt.xlabel('Time samples (n)')
plt.title('Generated Signal')
plt.show()
# Реализация ДПФ без использования @
def DFT(x):
N = len(x)
X = np.zeros(N, dtype=complex)
for k in range(N):
for n in range(N):
X[k] += x[n] * np.exp(-2j*np.pi*k*n/N)
return X
X = DFT(x)
N = len(X)
sr = N
n = np.arange(N)
T = N/sr
freq = n/T
n_oneside = N // 2
f_oneside = freq[:n_oneside]
X_oneside = X[:n_oneside]/n_oneside
plt.figure(figsize = (6, 4))
plt.stem(f_oneside, abs(X_oneside), 'b', markerfmt = ' ', basefmt = '-b')
plt.xlim(0, 10)
plt.xlabel('Frequency, HZ')
plt.ylabel('Amplitude |X(freq)|')
plt.title('One-Sided Amplitude Spectrum')
plt.show()
# Находим амплитуды для интересующих нас частот
target_freqs = [2/16, 3/16]
target_bins = [int(f * N) for f in target_freqs] # Соответствующие бины ДПФ
print("Амплитуды компонент:")
for freq, bin in zip(target_freqs, target_bins):
amplitude = abs(X[bin]) / (N/2) # Для одностороннего спектра делим на N/2
print(f"Частота {freq:.4f} (бин {bin}): амплитуда {amplitude:.4f}")
Амплитуды компонент: Частота 0.1250 (бин 2): амплитуда 1.0000 Частота 0.1875 (бин 3): амплитуда 0.4000
Ответы на вопросы:
Как частота дискретизации влияет на разрешение частотного спектра в ДПФ? Частота дискретизации определяет максимальную частоту, которую можно увидеть в спектре. Чем выше частота дискретизации, тем шире диапазон частот, но само разрешение по частоте зависит от длины сигнала, а не от частоты дискретизации. Например, если у нас сигнал записан 1 секунду с частотой 1000 Гц, разрешение по частоте будет 1 Гц (1/длину сигнала), и мы увидим частоты от 0 до 500 Гц.
Как шум может влиять на точность вычисления частот? Шум добавляет ложные пики в спектр или "размазывает" истинные частоты, делая их менее чёткими. Чтобы улучшить точность, можно: — увеличить длину сигнала (это повысит разрешение и поможет отличить истинные частоты от шума), — использовать усреднение спектров или специальные методы фильтрации.
Как быстрое преобразование Фурье (БПФ) уменьшает вычислительную сложность по сравнению с ДПФ? БПФ использует алгоритм, который разбивает сигнал на мелкие части и считает спектр для них по отдельности, а потом объединяет. Это позволяет сократить количество операций с O(N²) (как в ДПФ) до O(N log N), что намного быстрее, особенно для длинных сигналов. Например, для 1024 точек ДПФ потребует около миллиона операций, а БПФ — всего около 10 тысяч.
Как это связано с разбиением сигнала? БПФ делит сигнал на чётные и нечётные отсчёты, затем рекурсивно применяет то же самое к этим частям. Это похоже на разделение большой задачи на множество мелких, которые решаются быстрее. Благодаря такому подходу БПФ и работает так эффективно.
Пример 3 (билет 14, ПМ23-6?) - поиск значимых компонент, повторка с ПМ23-4!¶
Это абсолютная повторка с примером 1, код выше
Пример 4 (билет 4, ПМ23-5-6?) - БПФ и амплитуды компонент с частотами¶
# Параметры сигнала
N = 16 # Длина сигнала
n = np.arange(N)
freq1, freq2 = 2/16, 4/16 # Заданные частоты
amplitude1, amplitude2 = 1.0, 0.3 # Амплитуды компонент
noise_amplitude = 0.05 # Амплитуда шума
# Генерация сигнала с шумом
signal = (
amplitude1 * np.sin(2 * np.pi * freq1 * n) +
amplitude2 * np.cos(2 * np.pi * freq2 * n) +
noise_amplitude * np.random.randn(N) # Добавление шума
)
# Визуализация сигнала
plt.figure(figsize=(10, 4))
n_plot = np.linspace(0, 16, 100)
x_plot = np.sin(2*np.pi*2*n_plot/16) + 0.3*np.cos(2*np.pi*4*n_plot/16)
plt.plot(n_plot, x_plot, label='Сигнал с шумом')
plt.xlabel('Отсчёты (n)')
plt.ylabel('Амплитуда')
plt.title('Сгенерированный сигнал')
plt.grid()
plt.legend()
plt.show()
# БПФ
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])
# Или замена np.concatenate
#first_half = X_even + factor[:int(N/2)] * X_odd
#second_half = X_even + factor[int(N/2):] * X_odd
#X = np.empty(N, dtype=complex)
#X[:int(N/2)] = first_half
#X[int(N/2):] = second_half
return X
X = FFT(signal)
N = len(X)
sr = N
n = np.arange(N)
T = N/sr
freq = n/T
n_oneside = N // 2
f_oneside = freq[:n_oneside]
X_oneside = X[:n_oneside]/n_oneside
plt.figure(figsize = (6, 4))
plt.stem(f_oneside, abs(X_oneside), 'b', markerfmt = ' ', basefmt = '-b')
plt.xlim(0, 10)
plt.xlabel('Frequency, HZ')
plt.ylabel('Amplitude |X(freq)|')
plt.title('One-Sided Amplitude Spectrum')
plt.show()
# Находим амплитуды для интересующих нас частот
target_freqs = [2/16, 4/16]
target_bins = [int(f * N) for f in target_freqs] # Соответствующие бины ДПФ
print("Амплитуды компонент:")
for freq, bin in zip(target_freqs, target_bins):
amplitude = abs(X[bin]) / (N/2) # Для одностороннего спектра делим на N/2
print(f"Частота {freq:.4f} (бин {bin}): амплитуда {amplitude:.4f}")
Амплитуды компонент: Частота 0.1250 (бин 2): амплитуда 1.0198 Частота 0.2500 (бин 4): амплитуда 0.3073
Ответы на вопросы:
Как частота дискретизации влияет на разрешение частотного спектра в ДПФ? Частота дискретизации определяет максимальную частоту, которую можно увидеть в спектре. Чем выше частота дискретизации, тем шире диапазон частот, но само разрешение по частоте зависит от длины сигнала, а не от частоты дискретизации. Например, если у нас сигнал записан 1 секунду с частотой 1000 Гц, разрешение по частоте будет 1 Гц (1/длину сигнала), и мы увидим частоты от 0 до 500 Гц.
Как шум может влиять на точность вычисления частот? Шум добавляет ложные пики в спектр или "размазывает" истинные частоты, делая их менее чёткими.
Чтобы улучшить точность, можно: — увеличить длину сигнала (это повысит разрешение и поможет отличить истинные частоты от шума), — использовать усреднение спектров или специальные методы фильтрации.
БПФ можно использовать для удаления сезонности из временных рядов. Сначала БПФ применяется к временному ряду. В полученном спектре ищутся пики на частотах, соответствующих сезонности: Годовой цикл → пик на частоте 1/12 (если данные месячные). Квартальный цикл → пик на 1/3 и т.д. Далее сезонные частоты фильтруются. В спектре "зануляются" амплитуды на выявленных сезонных частотах (например, обнуляем пик на 1/12). Обратное БПФ преобразует спектр обратно в временной ряд, но уже без сезонных колебаний.
Пример 5 (билет ?, ПМ23-5-6?) - БПФ и амплитуды компонент с частотами¶
# Параметры сигнала
N = 8 # Длина сигнала
n = np.arange(N)
freq1, freq2 = 1/8, 2/8 # Заданные частоты
amplitude1, amplitude2 = 1.0, 0.2 # Амплитуды компонент
# Генерация сигнала
signal = (
amplitude1 * np.sin(2 * np.pi * freq1 * n) +
amplitude2 * np.cos(2 * np.pi * freq2 * n)
)
# Визуализация сигнала
plt.figure(figsize=(6, 3))
n_plot = np.linspace(0, 16, 100)
x_plot = np.sin(2*np.pi*n_plot/8) + 0.2*np.cos(2*np.pi*2*n_plot/8)
plt.plot(n_plot, x_plot)
plt.xlabel('Отсчёты (n)')
plt.ylabel('Амплитуда')
plt.title('Сгенерированный сигнал')
plt.grid()
plt.show()
# БПФ
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])
# Или замена np.concatenate
#first_half = X_even + factor[:int(N/2)] * X_odd
#second_half = X_even + factor[int(N/2):] * X_odd
#X = np.empty(N, dtype=complex)
#X[:int(N/2)] = first_half
#X[int(N/2):] = second_half
return X
X = FFT(signal)
N = len(X)
sr = N
n = np.arange(N)
T = N/sr
freq = n/T
n_oneside = N // 2
f_oneside = freq[:n_oneside]
X_oneside = X[:n_oneside]/n_oneside
plt.figure(figsize = (6, 4))
plt.stem(f_oneside, abs(X_oneside), 'b', markerfmt = ' ', basefmt = '-b')
plt.xlim(0, 10)
plt.xlabel('Frequency, HZ')
plt.ylabel('Amplitude |X(freq)|')
plt.title('One-Sided Amplitude Spectrum')
plt.show()
# Находим амплитуды для интересующих нас частот
target_freqs = [1/8, 2/8]
target_bins = [int(f * N) for f in target_freqs] # Соответствующие бины ДПФ
print("Амплитуды компонент:")
for freq, bin in zip(target_freqs, target_bins):
amplitude = abs(X[bin]) / (N/2) # Для одностороннего спектра делим на N/2
print(f"Частота {freq:.4f} (бин {bin}): амплитуда {amplitude:.4f}")
Амплитуды компонент: Частота 0.1250 (бин 1): амплитуда 1.0000 Частота 0.2500 (бин 2): амплитуда 0.2000
Вопросы - повторка примера 1
Ответы на вопросы:
Когда длина сигнала ограничена, ДПФ не может хорошо различать близкие частоты — они сливаются в один широкий пик. Улучшить разрешение можно: увеличивая длину сигнала (больше периодов для анализа), применяя оконные функции (Ханна, Хэмминга), используя метод дополнения нулями для интерполяции спектра.
БПФ в анализе сезонности временных рядов БПФ быстро вычисляет спектр временного ряда, выявляя скрытые периодические компоненты (например, суточные, годовые циклы). Пики в спектре соответствуют частотам сезонности. Например, для месячных данных пик на частоте 1/12 указывает на годовой цикл. Фильтрация шума и выделение значимых гармоник упрощает моделирование тенденций.
Влияние длины сигнала на точность. Короткие сигналы: Низкое частотное разрешение, большая погрешность в определении частот. Длинные сигналы: Повышают разрешение, но требуют больше вычислений. Однако если сигнал не содержит полных периодов, возникает погрешность из-за утечки спектра, даже при zero-padding. Компромисс: выбор длины, кратной основным периодам (например, целое число лет для климатических данных), минимизирует ошибки.
Вид 2 - метод Рунге-Кутты¶
Пример 1 (билет 18, ПМ23-4) - для 1 уравнения¶
# Определение функции правой части уравнения
def f(t, y):
return -y + t
# Точное решение
def exact_solution(t):
return t - 1 + np.exp(-t)
# Метод Рунге-Кутты 4-го порядка
def runge_kutta(f, x_end, y0, N):
h = x_end / N
x = np.linspace(0.0, x_end, N + 1)
y = np.zeros(N + 1)
y[0] = y0
for n in range(N):
k1 = h * f(x[n], y[n])
k2 = h * f(x[n] + h / 2, y[n] + k1 / 2)
k3 = h * f(x[n] + h / 2, y[n] + k2 / 2)
k4 = h * f(x[n] + h, y[n] + k3)
y[n + 1] = y[n] + (k1 + 2 * k2 + 2 * k3 + k4) / 6
return x, y
# Параметры решения
x_end = 1.0
y0 = 0.0
N = 10 # Число шагов (h = 0.1)
# Решение методом Рунге-Кутты
x_rk, y_rk = runge_kutta(f, x_end, y0, N)
# Точное решение
x_exact = np.linspace(0.0, x_end, 100)
y_exact = exact_solution(x_exact)
# Визуализация результатов
plt.figure(figsize=(10, 6))
plt.plot(x_rk, y_rk, 'bo-', label='Метод Рунге-Кутты (4-го порядка)')
plt.plot(x_exact, y_exact, 'r-', label='Точное решение')
plt.xlabel('t')
plt.ylabel('y(t)')
plt.title('Решение дифференциального уравнения')
plt.legend()
plt.grid()
plt.show()
Ответы на вопросы:
Что такое локальная и глобальная ошибки в численных методах для ОДУ? Локальная ошибка — это отклонение численного решения от точного на одном шаге интегрирования, вызванное приближёнными вычислениями. Глобальная ошибка — это суммарное накопленное отклонение после всех шагов, которое включает как локальные ошибки, так и их распространение по мере вычислений. Например, в методе Эйлера локальная ошибка на каждом шаге пропорциональна квадрату шага, а глобальная растёт линейно с числом шагов.
Как порядок точности метода влияет на эти ошибки? Чем выше порядок точности метода (например, метод Рунге-Кутты 4-го порядка), тем быстрее уменьшаются ошибки при уменьшении шага. Для метода с порядком p локальная ошибка пропорциональна $h^{p+1}$, а глобальная — $h^p$. Это значит, что увеличение порядка точности позволяет значительно снизить ошибки даже при относительно больших шагах, что экономит вычислительные ресурсы.
Объясни понятие устойчивости численных методов для ОДУ. Устойчивость метода означает, что малые возмущения (например, ошибки округления или начальных данных) не приводят к экспоненциальному росту погрешности в процессе вычислений. Например, явный метод Эйлера может быть неустойчивым для жёстких систем, так как ошибки быстро накапливаются, тогда как неявные методы (например, неявный Эйлер) остаются устойчивыми даже при больших шагах, контролируя рост погрешностей.
Пример 2 (билет 30, ПМ23-5-6?) - для системы¶
# Определение системы ОДУ
def harmonic_oscillator(t, state):
x, y = state
dxdt = y
dydt = -x
return np.array([dxdt, dydt])
# Метод Рунге-Кутты 4-го порядка для системы ОДУ
def runge_kutta_system(f, t_end, initial_state, h):
num_steps = int(t_end / h)
t = np.linspace(0, t_end, num_steps + 1)
state = np.zeros((num_steps + 1, len(initial_state)))
state[0] = initial_state
for i in range(num_steps):
k1 = f(t[i], state[i])
k2 = f(t[i] + h/2, state[i] + h/2 * k1)
k3 = f(t[i] + h/2, state[i] + h/2 * k2)
k4 = f(t[i] + h, state[i] + h * k3)
state[i+1] = state[i] + (h/6) * (k1 + 2*k2 + 2*k3 + k4)
return t, state
# Параметры решения
t_end = 10.0
h = 0.1
initial_state = np.array([1.0, 0.0]) # x(0) = 1, y(0) = 0
# Решение системы
t, state = runge_kutta_system(harmonic_oscillator, t_end, initial_state, h)
x = state[:, 0]
y = state[:, 1]
# Построение фазового портрета
plt.figure(figsize=(5, 4))
plt.plot(x, y, 'b-', label='Фазовый портрет')
plt.xlabel('x (положение)')
plt.ylabel('y (скорость)')
plt.title('Фазовый портрет гармонического осциллятора')
plt.grid()
plt.legend()
plt.axis('equal') # Одинаковый масштаб осей
plt.show()
# В качестве ответа? графики x(t) и y(t)
plt.figure(figsize=(6, 4))
plt.plot(t, x, 'r-', label='x(t) (положение)')
plt.plot(t, y, 'g-', label='y(t) (скорость)')
plt.xlabel('Время t')
plt.ylabel('Значение')
plt.title('Решение системы ОДУ')
plt.grid()
plt.legend()
plt.show()
Ответы на вопросы:
Как фазовый портрет помогает анализировать динамические системы? Фазовый портрет — это графическое представление траекторий системы в фазовом пространстве, которое показывает устойчивые точки, предельные циклы и общее поведение системы. По нему можно определить, стремится ли система к равновесию, колеблется или хаотически изменяется.
Как численные методы могут искажать фазовый портрет из-за ошибок округления? Численные методы (например, Эйлера или Рунге-Кутты) накапливают ошибки округления на каждом шаге, что может искажать траектории на фазовом портрете. Например, вместо замкнутого предельного цикла могут появиться спирали, а устойчивые точки — сместиться.
Сравнение явных и неявных методов по устойчивости: Явные методы (например, Эйлера, Рунге-Кутты) проще в реализации, но требуют малого шага для устойчивости. Они могут "развалить" фазовый портрет для жёстких систем. Неявные методы (например, неявный Эйлер) устойчивы даже при больших шагах, сохраняют структуру фазового портрета, но требуют решения уравнений на каждом шаге, что сложнее вычислительно.
Пример 3 (билет 3, ПМ23-5-6?) - сравнение эйлера и рунге-кутты¶
# Определение функции правой части уравнения
def f(t, y):
return 0.5*y*(1-y/2)
#a) Методом Эйлера
def euler_method(f, x_end, y0, N):
h = x_end/N
x = np.linspace(0.0, x_end, N+1)
y = np.zeros(N + 1)
y[0] = y0
for n in range(N):
y[n+1] = y[n] + h*f(x[n], y[n])
return x, y
#б) Методом Рунге-Кутты
def runge_kutta(f, x_end, y0, N):
h = x_end / N
x = np.linspace(0.0, x_end, N + 1)
y = np.zeros(N + 1)
y[0] = y0
for n in range(N):
k1 = h * f(x[n], y[n])
k2 = h * f(x[n] + h / 2, y[n] + k1 / 2)
k3 = h * f(x[n] + h / 2, y[n] + k2 / 2)
k4 = h * f(x[n] + h, y[n] + k3)
y[n + 1] = y[n] + (k1 + 2 * k2 + 2 * k3 + k4) / 6
return x, y
# Параметры решения
x_end = 5.0
y0 = 0.1
N = 50 # Число шагов (h = 0.1)
# Эйлер
x_50, y_50 = euler_method(f, 5, y0, 50)
print(f'Решение в t = 5 при h = 0.1 - {y_50[-1]}')
# Рунге-Кутта
x_rk, y_rk = runge_kutta(f, x_end, y0, N)
print(f'Решение в t = 5 при h = 0.1 - {y_rk[-1]}')
# Построение графиков для сравнения
plt.figure(figsize=(10, 6))
plt.plot(x_euler, y_euler, 'b-', label='Метод Эйлера (h=0.1)')
plt.plot(x_rk, y_rk, 'r--', label='Метод Рунге-Кутты 4-го порядка (h=0.1)')
plt.xlabel('t')
plt.ylabel('y(t)')
plt.title('Сравнение методов решения ОДУ')
plt.legend()
plt.grid()
plt.show()
Решение в t = 5 при h = 0.1 - 0.7624241148345196 Решение в t = 5 при h = 0.1 - 0.7813674924963677
Ответы на вопросы:
Что такое согласованность и устойчивость численных методов для ОДУ? Согласованность означает, что метод при уменьшении шага интегрирования стремится к точному решению (локальная ошибка стремится к нулю). Устойчивость — это способность метода контролировать рост ошибок на всём интервале интегрирования. Например, метод Эйлера согласован, но может быть неустойчивым для жёстких систем при больших шагах.
Как они связаны с сходимостью? Сходимость требует одновременно согласованности и устойчивости: Согласованность гарантирует, что на одном шаге метод приближает решение верно. Устойчивость обеспечивает, что ошибки не накапливаются катастрофически. Только при выполнении обоих условий метод будет сходиться к точному решению при уменьшении шага.
Как накопление ошибок округления влияет на устойчивость метода Эйлера? Явный метод Эйлера особенно чувствителен: ошибки округления на каждом шаге могут экспоненциально расти для жёстких систем. Это нарушает устойчивость — решение "разваливается" даже при малых шагах. Неявный Эйлер устойчивее: округления подавляются, так как метод учитывает будущие значения.
Вид 3 - метод Адамса-Мултона¶
Пример 1 (билет 7, ПМ23-5-6?) -¶
# Определение функции правой части уравнения
def f(t, y):
return -0.2 * y + np.cos(t)
# Метод Адамса-Мултона 5-го порядка
def am5_method(f, x_end, y0, N):
h = x_end / N
x = np.linspace(0.0, x_end, N + 1)
y = np.zeros((N + 1, len(y0)))
fn = np.zeros_like(y)
y[0, :] = y0
# Инициализация методом Рунге-Кутты 4-го порядка
for n in range(4):
fn[n, :] = f(x[n], y[n, :])
k1 = h * fn[n, :]
k2 = h * f(x[n] + h/2, y[n] + k1/2)
k3 = h * f(x[n] + h/2, y[n] + k2/2)
k4 = h * f(x[n] + h, y[n] + k3)
y[n+1, :] = y[n, :] + (k1 + 2*(k2 + k3) + k4)/6
# Коэффициенты метода Адамса-Мултона
b_am5 = np.array([251, 646, -264, 106, -19]) / 720 # Корректирующие
b_ab = np.array([1901, -2774, 2616, -1274, 251]) / 720 # Прогнозирующие
for n in range(4, N):
fn[n, :] = f(x[n], y[n, :])
# Прогноз (Адамс-Башфорт)
yp = y[n, :] + h * (b_ab[0]*fn[n, :] + b_ab[1]*fn[n-1, :] +
b_ab[2]*fn[n-2, :] + b_ab[3]*fn[n-3, :] + b_ab[4]*fn[n-4, :])
# Коррекция (Адамс-Мултон)
y[n+1, :] = y[n, :] + h * (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 euler_method(f, x_end, y0, N):
h = x_end / N
x = np.linspace(0.0, x_end, N + 1)
y = np.zeros(N + 1)
y[0] = y0
for n in range(N):
y[n+1] = y[n] + h * f(x[n], y[n])
return x, y
# Параметры решения
x_end = 10.0
y0 = 1.0
N = 100 # h = 0.1
# Решение обоими методами
x_am, y_am = am5_method(f, x_end, np.array([y0]), N)
x_euler, y_euler = euler_method(f, x_end, y0, N)
# Визуализация
plt.figure(figsize=(12, 6))
plt.plot(x_am, y_am[:, 0], 'b-', label='Адамс-Мултон 5-го порядка')
plt.plot(x_euler, y_euler, 'r--', label='Метод Эйлера')
plt.xlabel('Время t')
plt.ylabel('y(t)')
plt.title('Сравнение методов решения ОДУ: затухающий осциллятор')
plt.legend()
plt.grid()
plt.show()
# Сравнение в конечной точке
print(f'Метод Адамса-Мултона (h=0.1): y(10) = {y_am[-1, 0]:.6f}')
print(f'Метод Эйлера (h=0.1): y(10) = {y_euler[-1]:.6f}')
Метод Адамса-Мултона (h=0.1): y(10) = -0.575148 Метод Эйлера (h=0.1): y(10) = -0.543322
Ответы на вопросы:
Как порядок точности метода Адамса-Мултона влияет на его точность по сравнению с методом Эйлера? Метод Адамса-Мултона имеет более высокий порядок точности (обычно 2–4), чем метод Эйлера (порядок 1), что означает, что его локальная ошибка уменьшается быстрее при сокращении шага. Например, для шага h ошибка Эйлера пропорциональна h^2, а у Адамса-Мултона — h^3 или h^5, что даёт значительно более точные результаты даже при меньшем числе вычислений.
Как локальная ошибка усреднения влияет на глобальную ошибку? Локальная ошибка (на одном шаге) накапливается с каждым шагом интегрирования, формируя глобальную ошибку. Чем выше порядок метода, тем медленнее растёт глобальная ошибка. Например, у метода Эйлера глобальная ошибка растёт линейно с числом шагов, а у методов высокого порядка (как Адамс-Мултон) — значительно медленнее.
Что такое слабая и строгая устойчивость численных методов? Слабая устойчивость означает, что метод устойчив только для малых шагов (например, явный метод Эйлера). Строгая устойчивость (как у неявных методов) гарантирует устойчивость даже при больших шагах, что критично для жёстких систем. Например, неявный Эйлер строго устойчив, поэтому подходит для задач с быстро меняющимися решениями.
Вид 4 - центральная разность¶
Пример 1 (билет 15, ПМ23-5) -¶
# Определение функции
def f(x):
return x**2 * np.exp(-x)
# Точное значение второй производной
def exact_second_derivative(x):
return (2 - 4*x + x**2) * np.exp(-x)
# Метод центральной разности для второй производной
def central_difference_second(f, x, h):
return (f(x + h) - 2*f(x) + f(x - h)) / (h**2)
# Параметры вычисления
x = 1.0
h = 0.1
# Численное значение второй производной
approx = central_difference_second(f, x, h)
# Точное значение
exact = exact_second_derivative(x)
# Вывод результатов
print(f"Численное значение f''(1): {approx:.6f}")
print(f"Точное значение f''(1): {exact:.6f}")
print(f"Погрешность: {np.abs(approx - exact):.6f}")
Численное значение f''(1): -0.366345 Точное значение f''(1): -0.367879 Погрешность: 0.001535
Ответы на вопросы:
Какова точность центральной разностной схемы для аппроксимации второй производной? Центральная разностная схема для второй производной f′′(x) имеет второй порядок точности O(h^2), так как использует симметричные точки f(x+h) и f(x−h). Погрешность пропорциональна h^2, что делает метод точнее односторонних разностных аналогов.
Как ошибка аппроксимации зависит от шага h? Ошибка уменьшается квадратично с уменьшением шага, но слишком малые значения h могут увеличить вычислительную погрешность из-за округлений. Оптимальный шаг выбирают балансируя между ошибкой аппроксимации и вычислительной погрешностью.
Невошедшее¶
# --------------------------------------------------- Метод половинного деления (бисекции) ----------------------------------------------------
def bisection(f, a, b, tol):
if np.sign(f(a)) == np.sign(f(b)):
raise Exception("Заданные a и b не локализуют корень")
m = (a+b)/2
if np.abs(f(m)) < tol:
return m
elif np.sign(f(a)) == np.sign(f(m)):
return bisection(f, m, b, tol)
elif np.sign(f(b)) == np.sign(f(m)):
return bisection(f, a, m, tol)
f = lambda x: x**2 - 2
r1 = bisection(f, 0, 2, 0.1)
print('x1 =', r1)
r2 = bisection(f, 0, 2, 0.001)
print('x2 =', r2)
print('f(x1) =', f(r1))
print('f(x2) =', f(r2))
# ------------------------------------------------------------- Метод хорд (секущих). -------------------------------------------------------------
def chord_method(f, x0, b, n_iterations = 100):
def g(x):
return x - b*f(x)
iters = np.zeros((n_iterations+1,))
iters[0] = x0
for n in range(n_iterations):
iters[n+1] = g(iters[n])
return iters
def f1(x):
return x - np.cos(x)
result1 = chord_method(f1, 0.0, 1.08)
for i in range(6):
print(f"Результат после {i} итераций - {result1[i]}")
def chord_method2(f, x0, h=1e-5, n_iterations=100):
iters = np.zeros((n_iterations + 1,))
iters[0] = x0
for n in range(n_iterations):
denominator = f(iters[n]) - f(iters[n] - h)
if denominator == 0:
break # Чтобы избежать деления на ноль
iters[n + 1] = iters[n] - (f(iters[n]) * h) / denominator
return iters[:n + 2] # Обрезаем массив до реального числа итераций
def f1(x):
return x - np.cos(x)
result = chord_method2(f1, x0=0.0, h=1e-5, n_iterations=10)
for i in range(6):
print(f"Результат после {i} итераций: {result[i]}")
# --------------------------------------------------- Модифицированный метод Ньютона. -----------------------------------------------------------
f = lambda x: x**2 - 2
f_prime = lambda x: 2*x
def modified_newton(f, df, x0, tol, df_x0=None):
if df_x0 is None: # Вычисляем производную только на первом шаге
df_x0 = df(x0)
if abs(f(x0)) < tol:
return x0
else:
return modified_newton(f, df, x0 - f(x0)/df_x0, tol, df_x0) # Передаём df_x0 вглубь
estim = modified_newton(f, f_prime, 1.5, 1e-6)
print('Результат:', estim)
print('Истинное значение: ', np.sqrt(2))
print("Погрешность:", estim - np.sqrt(2))
# -------------------------------------------------------------- Метод дихотомии. ------------------------------------------------------------------
def dihotomia(f, a, b, tol):
if (b - a) < tol:
return (a + b) / 2
c = (a + b) / 2
f_left = f(c - tol/2)
f_right = f(c + tol/2)
if f_left == f_right:
return c
elif f_left < f_right:
return dihotomia(f, a, c, tol)
elif f_left > f_right:
return dihotomia(f, c, b, tol)
f = lambda x: x**2 - 2
r1 = dihotomia(f, 0, 2, 0.1)
print('x1 =', r1)
r2 = dihotomia(f, -1, 2, 0.001)
print('x2 =', r2)
print('f(x1) =', f(r1))
print('f(x2) =', f(r2))
# ---------------------------------- Метод функциональной итерации для решения систем нелинейных уравнений. ---------------------------------------
def simple_iteration_system(G, x0, eps=1e-6, max_iter=1000):
"""
Модифицированная версия без использования np.linalg.norm
"""
x = x0.copy()
for _ in range(max_iter):
x_next = G(x)
# Вычисление евклидовой нормы через сумму квадратов
diff = x_next - x
norm_sq = sum(diff[i]**2 for i in range(len(diff)))
if norm_sq < eps**2: # Сравнение квадратов для избежания sqrt
return x_next
x = x_next
raise RuntimeError("Метод не сошёлся за указанное число итераций")
# Пример использования
G = lambda x: np.array([np.sin(x[1]), np.cos(x[0])])
x0 = np.array([0.5, 0.5])
root = simple_iteration_system(G, x0)
print(f"1. Метод простых итераций: x = {root[0]:.5f}, y = {root[1]:.5f}")
# ------------------------------------------------- Модифицированный метод Ньютона в двумерном случае. --------------------------------------------
# Метод Ньютона (модифицированный)
# --------------------------
def newton_system(F, J, x0, tol=1e-6, max_iter=100):
"""
Модифицированная версия с ручным решением СЛАУ
"""
x = np.array(x0, dtype=float)
for _ in range(max_iter):
F_val = F(x)
# Проверка нормы невязки через сумму квадратов
if sum(f**2 for f in F_val) < tol**2:
return x
J_val = J(x)
a, b, c, d = J_val[0,0], J_val[0,1], J_val[1,0], J_val[1,1]
det = a*d - b*c
if abs(det) < 1e-12:
raise ValueError("Вырожденная матрица Якоби")
# Решение системы методом Крамера
e, f = -F_val
dx = (e*d - b*f) / det
dy = (a*f - e*c) / det
x += np.array([dx, dy])
raise RuntimeError("Метод не сошёлся")
# ------------------------------------------------------------ Линейная интерполяция. --------------------------------------------------------------
def linear_interpolation(x_values, y_values, x_new):
for i in range(len(x_values) - 1):
if x_values[i] <= x_new <= x_values[i+1]:
y_new = y_values[i] + (y_values[i+1]-y_values[i])*(x_new-x_values[i])/(x_values[i+1]-x_values[i])
return y_new
return None
x = [0, 1, 2]
y = [1, 3, 2]
y_hat = linear_interpolation(x, y, 1.5)
print(y_hat)
plt.figure(figsize=(6, 3))
plt.plot(x, y, '-ob', label = "Заданные точки")
plt.plot(1.5, y_hat, 'ro', label = "Интерполированная точка")
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True)
plt.legend()
plt.show()
from scipy.interpolate import interp1d # для проверки себя
x = [0, 1, 2]
y = [1, 3, 2]
f = interp1d(x, y)
y_hat = f(1.5)
print(y_hat)
# ---------------------------------- Вычисление собственых значений с помощью характеристического многочлена. ---------------------------------------
A = np.array([[5, 2],
[3, 4]])
params = np.poly(A) # Функция ищет коэффициенты хар. многочлена матрицы
lambdas = np.roots(params) # Функция ищет корни уравнения по переданным коэффициентам
print(f'Коэффициенты: {params}')
print(f'Лямбды: {lambdas}')
# ----------------------------------------------------------- Степенной метод со сдвигами. ----------------------------------------------------------
def shifted_power_iteration(A, mu=0, max_iter=1000, tol=1e-10): # Сходится к соб. значению, ближайшему к mu
n = A.shape[0]
I = np.eye(n)
# Решаем (A - μI)v_{k+1} = v_k (обратный степенной метод)
v = np.random.rand(n)
v = v / norm(v)
lambda_prev = 0
for _ in range(max_iter):
try:
# Решаем систему (A - μI)w = v
w = np.linalg.solve(A - mu * I, v)
except np.linalg.LinAlgError:
raise ValueError("Матрица (A - μI) вырождена, выберите другой сдвиг.")
v = w / norm(w)
# Оценка λ через Рэлеевское отношение
lambda_est = v.T @ A @ v
if abs(lambda_est - lambda_prev) < tol:
break
lambda_prev = lambda_est
#lambda_est: оценка наибольшего собственного значения,
#v: соответствующий собственный вектор.
return lambda_est, v
# ----------------------------------------------------------------- Метод вращений. -----------------------------------------------------------------
def jacobi_eigen_iterative(matrix, max_iter=1000, epsilon=1e-10): # матрица должна быть симметричной
n = matrix.shape[0]
eigenvectors = np.eye(n)
for _ in range(max_iter):
max_val, p, q = 0, 0, 0 # Находим самый большой элемент, его потом под гильотину
for i in range(n):
for j in range(i + 1, n):
if abs(matrix[i, j]) > max_val:
max_val, p, q = abs(matrix[i, j]), i, j
if max_val < epsilon:
break # Если самый большой недиагональный элемент оказался похож на ноль, то выходим
theta = 0.5 * np.arctan2(2 * matrix[p, q], matrix[p, p] - matrix[q, q]) # Угол поворота
c, s = np.cos(theta), np.sin(theta) # Значения для Якобиана
rotation = np.eye(n) # Якобиан
rotation[p, p], rotation[q, q] = c, c
rotation[p, q], rotation[q, p] = -s, s
matrix = rotation.T @ matrix @ rotation # Пользуемся свойством подобия
eigenvectors = eigenvectors @ rotation
eigenvalues = np.diag(matrix)
idx = eigenvalues.argsort()[::-1]
return eigenvalues[idx], eigenvectors[:, idx]
# ------------------------------------------------------------------ QR алгоритм. -------------------------------------------------------------------
@njit
def norm(k):
return np.sqrt((k * k).sum())
@njit
def QR_dec(A):
n, m = A.shape
Q = np.zeros((n, m),dtype=np.float64)
for i in range(m):
u = A[:, i].copy() # Берем очередной столбец A
for j in range(i):
proj = (Q[:, j] @ u) * Q[:, j] # Проецируем на Q вектор u
u -= proj
u_norm = norm(u)
if u_norm > 1e-12:
Q[:, i] = u / u_norm # else столбец Q будет чётко нулевым
R = Q.T @ A
return Q, R
@njit
def QR_step(Ak):
Q, R = QR_dec(Ak)
return R @ Q
def QR_step_shift(Ak):
mu = Ak[-1, -1] # Берем последний элемент как приближение для сдвига
Q, R = QR_dec(Ak - mu * np.eye(Ak.shape[0])) # Важно: если все элементы диагонали одинаковые то такой сдвиг даст вырожденную матрицу :3
return R @ Q + mu * np.eye(Ak.shape[0])
def QR_alg(A, tol=1e-9, miter=1000):
Ak = A.copy()
n = A.shape[0]
for _ in range(miter):
a = abs(np.diag(Ak,k=-1))
if np.all(a<tol):
break
Ak = QR_step_shift(Ak)
return np.array([Ak[i, i] for i in range(n)])
#eigenvalues = QR_alg(H,miter = 5_000, tol= 1e-12)
#true_eigenvalues = np.linalg.eigvals(H)
# ----------------------------------------------------------- Разложение Шура, теорема Шура. -------------------------------------------------------
#4. Разложение Шура
@njit
def schur_decomposition(A, max_iter=1000, tol=1e-10): # Только для квадратных матриц (см т. Шура)
n = A.shape[0]
#Q - унитарная матрица (n x n),
Q = np.eye(n, dtype=np.float64) # Начинаем с единичной матрицы
#T - верхняя квазитреугольная матрица Шура (n x n).
T = A.copy()
for _ in range(max_iter):
# QR-разложение текущей матрицы T
Q_k, R_k = QR_dec(T)
# Обновляем T = R_k Q_k
T = R_k @ Q_k
# Накопление Q: Q = Q @ Q_k
Q = Q @ Q_k
# Проверка на сходимость (нижний треугольник близок к нулю)
off_diag_norm = norm(np.tril(T, -1)) # Норма поддиагональных элементов
if off_diag_norm < tol:
break
return Q, T
# ---------------------------------------------------------- Метод предиктора-корректора Эйлера. ----------------------------------------------------
def euler_pc_method(f, x_end, y0, N):
h = x_end/N
x = np.linspace(0.0, x_end, N+1)
y = np.zeros((N + 1, len(y0)))
y[0, :] = y0
for n in range(N):
fn = f(x[n], y[n, :])
yp = y[n, :] + h * fn
y[n+1, :] = y[n, :] + h/2 * (fn + f(x[n+1], yp))
return x, y
def simple(x, y):
return -np.sin(x)
x_5, y_5 = euler_pc_method(simple, 0.5, [1.0], 5)
x_50, y_50 = euler_pc_method(simple, 0.5, [1.0], 50)
print(f'Решение в x = 0.5 при h = 0.1 - {y_5[-1, 0]}')
print(f'Решение в x = 0.5 при h = 0.01 - {y_50[-1, 0]}')
print('Истинное решение = ', np.cos(0.5))
def simple2(t, f):
dfdt = np.zeros_like(f)
dfdt[0] = -f[1]
dfdt[1] = f[0]
return dfdt
y0 = np.array([1, 0])
t_0_1, f_0_1 = euler_pc_method(simple2, 50.0, y0, 500)
t_0_01, f_0_01 = euler_pc_method(simple2, 50.0, y0, 5000)
fig = plt.figure(figsize = (10, 5))
ax1 = fig.add_subplot(121)
ax1.plot(f_0_1[:, 0], f_0_1[:, 1], 'b-', label= '$h = 0.1$')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax2 = fig.add_subplot(122)
ax2.plot(f_0_01[:, 0], f_0_01[:, 1], 'b-', label= '$h = 0.1$')
ax2.set_xlabel('x')
ax2.set_ylabel('y')
plt.show()
# ------------------------------------------------------------ Методы Адамса-Башфорта. -------------------------------------------------------------
def ab5_method(f, x_end, y0, N):
h = x_end/N
x = np.linspace(0.0, x_end, N+1)
y = np.zeros((N + 1, len(y0)))
fn = np.zeros_like(y)
y[0, :] = y0
for n in range(4):
fn[n, :] = f(x[n], y[n, :])
k1 = h * fn[n, :]
k2 = h * f(x[n] + h/2, y[n] + k1/2)
k3 = h * f(x[n] + h/2, y[n] + k2/2)
k4 = h * f(x[n] + h, y[n] + k3)
y[n+1, :] = y[n, :] + (k1 + 2*(k2 + k3) + k4)/6
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]])
coef_b = np.array([1, 1/2, 5/6, 9/4, 251/30])
b = np.linalg.solve(coeff_A, coef_b)
for n in range(4, N):
fn[n, :] = f(x[n], y[n, :])
y[n+1, :] = y[n, :] + h * (b[0]*fn[n, :] + b[1]*fn[n-1, :] + b[2]*fn[n-2, :] +
b[3]*fn[n-3, :] + b[4]*fn[n-4, :])
return x, y
def simple(x, y):
return -np.sin(x)
x_5, y_5 = ab5_method(simple, 0.5, [1.0], 5)
x_50, y_50 = ab5_method(simple, 0.5, [1.0], 50)
print(f'Ошибка при x = 0.5 при h = 0.1 - {np.abs(y_5[-1, 0] - np.cos(0.5))}')
print(f'Ошибка при x = 0.5 при h = 0.01 - {np.abs(y_50[-1, 0] - np.cos(0.5))}')
print('Истинное решение = ', np.cos(0.5))
# ------------------------------------------------------- Обратное дискретное преобразование Фурье. -------------------------------------------------
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 = (e @ X) / N
return x
def IFFT(X, normalize=True):
N = len(X)
if N <= 1:
return X
if N % 2 != 0:
X = np.pad(X, (0, 1), 'constant')
N = len(X)
X_even = proper_IFFT(X[::2], False)
X_odd = proper_IFFT(X[1::2], False)
factor = np.exp(2j * np.pi * np.arange(N) / N)
x = np.concatenate([X_even + factor[:N//2] * X_odd,
X_even + factor[N//2:] * X_odd])
return x / N if normalize else x