야코비 방법과 가우스-자이델 방법 문서의 그림 생성 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

# 야코비 방법과 가우스-자이델 방법 문서의 그림을 만든다: 20_jacobi-gauss-seidel_fig1.svg
# 실행: ~/.venvs/vault-plots/bin/python 20_jacobi-gauss-seidel_plot.py  (matplotlib, numpy 필요)
import math
import os

import matplotlib
matplotlib.use("svg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import font_manager

HERE = os.path.dirname(os.path.abspath(__file__))
PREFIX = "20_jacobi-gauss-seidel"
INK = "#888888"                                         # 밝은 테마와 어두운 테마 모두에서 보이는 회색
C = ["#3b82c4", "#d9622b", "#3a9a5b", "#9b59b6", "#c49a1b"]
_fonts = {f.name for f in font_manager.fontManager.ttflist}
FONT = next((n for n in ["Apple SD Gothic Neo", "NanumGothic", "Noto Sans CJK KR"] if n in _fonts), "DejaVu Sans")
plt.rcParams.update({
    "font.family": FONT, "mathtext.fontset": "dejavusans",
    "font.size": 11, "axes.unicode_minus": False, "svg.fonttype": "path", "svg.hashsalt": PREFIX,
    "axes.edgecolor": INK, "axes.labelcolor": INK, "xtick.color": INK, "ytick.color": INK,
    "text.color": INK, "legend.frameon": False,
    "axes.facecolor": "none", "figure.facecolor": "none", "savefig.transparent": True,
    "axes.spines.top": False, "axes.spines.right": False,
})


def save(fig, k):
    fig.savefig(os.path.join(HERE, f"{PREFIX}_fig{k}.svg"), bbox_inches="tight", metadata={"Date": None})
    plt.close(fig)


A = np.array([[4, -1, 1], [4, -8, 1], [-2, 1, 5]], dtype=float)
b = np.array([7, -21, 15], dtype=float)
TRUE = np.array([2, 4, 3], dtype=float)
X0 = np.array([1, 2, 2], dtype=float)


def jacobi_step(A, b, x):
    D = np.diag(A)
    return (b - (A @ x - D * x)) / D


def gs_step(A, b, x):
    x = x.copy()
    for i in range(len(b)):
        x[i] = (b[i] - A[i] @ x + A[i, i] * x[i]) / A[i, i]
    return x


def errors(step, A, b, n):
    x, out = X0.copy(), [np.max(np.abs(X0 - TRUE))]
    xs = [x]
    for _ in range(n):
        x = step(A, b, x)
        xs.append(x)
        out.append(np.max(np.abs(x - TRUE)))
    return np.array(out), xs


N = 25
eJ, xJ = errors(jacobi_step, A, b, N)
eG, xG = errors(gs_step, A, b, N)
As, bs = A[[1, 0, 2]], b[[1, 0, 2]]                     # 첫 두 식을 바꾼 순서: 대각 우세가 깨진다
eS, xS = errors(jacobi_step, As, bs, N)

fig, ax = plt.subplots(figsize=(6, 3.6))
k = np.arange(N + 1)
ax.semilogy(k, eJ, "o-", color=C[0], ms=3, lw=1.5, label="야코비")
ax.semilogy(k, eG, "s-", color=C[1], ms=3, lw=1.5, label="가우스-자이델")
ax.semilogy(k, eS, "^-", color=C[3], ms=3, lw=1.5, label="야코비, 식 순서를 바꿈")
ax.set_ylim(1e-12, 1e5)
ax.yaxis.set_major_locator(matplotlib.ticker.LogLocator(base=10, numticks=20))
ax.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda v, _: f"$10^{{{round(math.log10(v))}}}$"))
ax.set_yticks([1e-12, 1e-8, 1e-4, 1, 1e4])
ax.set_xlabel("반복 횟수 $k$")
ax.set_ylabel("참값과의 가장 큰 차이")
ax.legend(loc="lower left", fontsize=9)
save(fig, 1)

if __name__ == "__main__":
    # 표의 3회차 값, 같은 반복에서 가우스-자이델 오차가 더 작음, 순서를 바꾸면 30번 안에 1000을 넘음
    assert np.allclose(xJ[3], [1.9625, 3.925, 2.9625]) and np.allclose(xG[3], [1.995625, 3.99609375, 2.99903125])
    assert all(eG[i] < eJ[i] for i in range(1, 15))
    _, xS30 = errors(jacobi_step, As, bs, 30)
    assert np.max(np.abs(xS30[30])) > 1000
    print("ALL CHECKS PASSED")