Python 3.12.15
NumPy 2.5.4
Matplotlib 3.11.2
Numba 0.68.0
Численные методы для физиков
Весь курс написан на Python. Сегодня — инструменты, без которых дальше не обойтись, на двух задачах с точным ответом:
Что берём с собой: рабочая среда и ноутбук, массивы NumPy, конечная точность чисел, генератор случайных чисел с seed, графики, измерение времени, Numba.
Ноутбук лекции: открыть · скачать .ipynb
Частица делает \(N\) шагов \(s_i=\pm1\) с вероятностью \(1/2\): \[ x_N=\sum_{i=1}^{N}s_i . \]
Бросаем \(n\) точек равномерно в квадрат \([0,1]^2\) и считаем долю \(k/n\) точек внутри четверти круга \(x^2+y^2<1\): \[ \frac{k}{n}\;\to\;p=\frac{\pi}{4},\qquad \hat\pi = 4\,\frac{k}{n}. \]
Блуждание. Шаги независимы, \(\langle s_i\rangle=0\), \(\langle s_i s_j\rangle=\delta_{ij}\): \[ \langle x_N\rangle=0,\qquad \langle x_N^2\rangle=\sum_{i,j}\langle s_is_j\rangle=N . \] Среднее по \(M\) блужданиям само случайно: \(\operatorname{Var}x_N^2=2N^2-2N\), поэтому относительная ошибка \(\langle x_N^2\rangle/N\) равна \(\sqrt{2/M}\).
Число \(\pi\). \(k\) — биномиальная величина с вероятностью \(p=\pi/4\): \[ \sigma_{\hat\pi}=4\sqrt{\frac{p(1-p)}{n}}\approx\frac{1.64}{\sqrt n}. \]
python -m pip install numpy matplotlib numba jupyterlab.venv, pyenv, conda): версии пакетов не смешиваются между проектами.jupyter lab, Shift+Enter выполняет ячейку.Python 3.12.15
NumPy 2.5.4
Matplotlib 3.11.2
Numba 0.68.0
Результат воспроизводим, только если известно, чем он получен.
140 ms ± 726 μs per loop (mean ± std. dev. of 3 runs, 1 loop each)
589 μs ± 124 μs per loop (mean ± std. dev. of 3 runs, 10 loops each)
Цикл Python на каждом шаге проверяет типы; NumPy выполняет цикл в скомпилированном коде над непрерывным блоком памяти.
после b[0] = 100: a = [100. 0. 0. 0. 0. 0.]
после c[1] = -1: a = [100. 0. 0. 0. 0. 0.]
Изменение среза меняет исходный массив. Нужна независимая копия — .copy().
[[ 0. 1. 2.]
[10. 11. 12.]]
[[ 0. 5. 10.]
[ 5. 0. 5.]
[10. 5. 0.]]
Размерности сравниваются справа; размерность 1 растягивается.
\(\varepsilon\) — наименьшее число, для которого \(1+\varepsilon\ne1\). Сравнивать вещественные числа на точное равенство нельзя.
последовательно (cumsum): 1.087937e+06
np.sum (попарно): 1.0000001e+06
float64: 1000000.0149011612
Точный ответ \(10^6\). Когда сумма велика, прибавка \(0.1\) округляется; np.sum складывает попарно, и ошибка растёт медленнее.
seedОдин генератор на расчёт, seed записан в коде: результат повторяется.
rng = np.random.default_rng(seed=1)
n_walkers, n_steps = 2000, 1000
steps = rng.choice(np.array([-1, 1], dtype=np.int8), size=(n_walkers, n_steps))
x = np.cumsum(steps, axis=1, dtype=np.int64) # x[k, n-1] — положение k-го блуждания после n шагов
print("x.shape =", x.shape)
print("<x_N> =", x[:, -1].mean())
print("<x_N^2> =", (x[:, -1] ** 2).mean(), " (эталон", n_steps, ")")x.shape = (2000, 1000)
<x_N> = -0.347
<x_N^2> = 1032.122 (эталон 1000 )
Отклонение \(\langle x_N^2\rangle\) от \(N\) сравниваем со статистической ошибкой \(\sqrt{2/M}\approx3\%\), а не с нулём.
\(x_N\) той же чётности, что \(N\): значения идут через 2, вероятность вдвое больше плотности.
def pi_numpy(n, rng):
xy = rng.random((n, 2))
inside = (xy ** 2).sum(axis=1) < 1.0
return 4.0 * inside.mean()
rng = np.random.default_rng(seed=3)
for n in [10**2, 10**4, 10**6]:
est = pi_numpy(n, rng)
print(f"n = {n:>8}: pi ~ {est:.5f}, ошибка {est - np.pi:+.5f}, "
f"ожидаемая {1.64 / np.sqrt(n):.5f}")n = 100: pi ~ 3.20000, ошибка +0.05841, ожидаемая 0.16400
n = 10000: pi ~ 3.13760, ошибка -0.00399, ожидаемая 0.01640
n = 1000000: pi ~ 3.14054, ошибка -0.00105, ожидаемая 0.00164
Ещё один верный знак стоит в 100 раз больше точек. При \(n=10^8\) массив координат занял бы 1.6 ГБ.
import time
def timed(f, *args):
t0 = time.perf_counter()
result = f(*args)
return result, time.perf_counter() - t0
_, t_first = timed(pi_numba, 10**6, 5)
_, t_second = timed(pi_numba, 10**6, 5)
_, t_numpy = timed(pi_numpy, 10**6, np.random.default_rng(5))
est, t_big = timed(pi_numba, 10**8, 5)
print(f"Numba, первый вызов (с компиляцией), n=1e6: {t_first:8.3f} с")
print(f"Numba, второй вызов, n=1e6: {t_second:8.3f} с")
print(f"NumPy, n=1e6: {t_numpy:8.3f} с")
print(f"Numba, n=1e8: pi ~ {est:.6f}, ошибка {est - np.pi:+.6f}, время {t_big:.2f} с")Numba, первый вызов (с компиляцией), n=1e6: 0.656 с
Numba, второй вызов, n=1e6: 0.020 с
NumPy, n=1e6: 0.030 с
Numba, n=1e8: pi ~ 3.141380, ошибка -0.000213, время 2.00 с
Numba нужна для циклов, которые не векторизуются (шаг зависит от предыдущего) или не помещаются в память.
0.1 + 0.2 != 0.3: вещественные числа сравниваются с допуском (np.isclose).float32 теряет точность при накоплении; по умолчанию — float64.seed даёт неповторяемый результат; у Numba свой генератор.float64.seed.