격자 회전 ↔ 선형변환 문서의 그림 생성 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
# 격자 회전 ↔ 선형변환 문서의 그림을 만든다: 36_grid-rotation-linear_fig1.svg
# 실행: ~/.venvs/vault-plots/bin/python 36_grid-rotation-linear_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 = "36_grid-rotation-linear"
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)
from matplotlib.patches import Rectangle
A = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
FLIP = A[::-1] # 위아래 뒤집기
ROT = [list(r) for r in zip(*A[::-1])] # 뒤집은 뒤 전치 = 시계 방향 90°
MARK = {1: C[1], 2: C[0], 3: C[2]} # 첫 줄 세 칸을 색으로 따라간다
def draw(ax, g, title, mirror=None):
n = len(g)
for r in range(n):
for c in range(n):
v = g[r][c]
ax.add_patch(Rectangle((c, r), 1, 1, facecolor=MARK.get(v, "none"), alpha=0.3 if v in MARK else 1,
edgecolor=INK, lw=0.8))
ax.text(c + 0.5, r + 0.5, str(v), ha="center", va="center", fontsize=12)
if mirror == "row": # 가운데 행을 지나는 가로 거울
ax.plot([-0.2, n + 0.2], [1.5, 1.5], color=C[3], lw=1.6, ls="--")
if mirror == "diag": # 왼쪽 위에서 오른쪽 아래로 가는 대각선 거울
ax.plot([-0.2, n + 0.2], [-0.2, n + 0.2], color=C[3], lw=1.6, ls="--")
ax.set_xlim(-0.3, n + 0.3)
ax.set_ylim(n + 0.3, -0.3)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(title, fontsize=10, color=INK)
fig, axes = plt.subplots(1, 3, figsize=(6.6, 2.6))
draw(axes[0], A, "a\n거울: 가로줄", mirror="row")
draw(axes[1], FLIP, "a[::-1]\n거울: 대각선", mirror="diag")
draw(axes[2], ROT, "zip(*a[::-1])\n= 시계 방향 90°")
for x in [0.355, 0.655]:
fig.text(x, 0.42, "→", fontsize=18, ha="center", va="center", color=INK)
fig.subplots_adjust(wspace=0.35)
save(fig, 1)
if __name__ == "__main__":
# 문서 표: 뒤집기 [[7,8,9],[4,5,6],[1,2,3]], 시계 방향 [[7,4,1],[8,5,2],[9,6,3]]
assert FLIP == [[7, 8, 9], [4, 5, 6], [1, 2, 3]]
assert ROT == [[7, 4, 1], [8, 5, 2], [9, 6, 3]]
# 칸 번호 식: (r, c) → (c, n−1−r). 반사 두 행렬의 곱이 회전 행렬 R_{-90°}
n = 3
for r in range(n):
for c in range(n):
assert ROT[c][n - 1 - r] == A[r][c]
T = np.array([[0, 1], [1, 0]])
F = np.array([[-1, 0], [0, 1]])
assert (T @ F == np.array([[0, 1], [-1, 0]])).all()
print("ALL CHECKS PASSED")