diagonalize2d.py ダウンロード/コピー

diagonalize2d.py をダウンロード

diagonalize2d.py
diagonalize2d.py
  1#!/usr/bin/env python3
  2"""
  3diagonalize2d.py
  4
  5概要:
  6    2x2実数行列の対角化を可視化するスクリプト。
  7
  8詳細説明:
  9    このスクリプトは以下の機能を提供します:
 10    1. 元の基底ベクトル (a1, a2) とそれらの行列Sによる像 (S a1, S a2) を描画します。
 11    2. 2x2実数行列Sの固有値と固有ベクトルを計算します。
 12    3. 行列が実数上で対角化可能でない場合、メッセージを表示して終了します。
 13    4. 行列が実数上で対角化可能である場合、実固有ベクトル (v1, v2) とそれらの像 (S v1, S v2) を描画します。
 14    5. 対角化前後の基底ベクトル間の角度を出力します。
 15
 16解釈:
 17    - Sが対称行列の場合、対角化基底は直交するので、角度は90度を保ちます。
 18    - Sが非対称であるが実数上で対角化可能な場合、対角化基底は一般に直交しません。
 19    - 複素固有値が現れる場合、実数の主軸基底は存在しません。
 20
 21Usage:
 22    python diagonalize2d.py S11 S12 S21 S22
 23
 24関連リンク:
 25    :doc:`diagonalize2d_usage`
 26"""
 27
 28from __future__ import annotations
 29
 30import argparse
 31import math
 32
 33import matplotlib.pyplot as plt
 34import numpy as np
 35
 36
 37EPS = 1e-10
 38
 39
 40def unit(v: np.ndarray) -> np.ndarray:
 41    """
 42    概要:
 43        与えられたベクトルを単位ベクトルに正規化する。
 44
 45    詳細説明:
 46        ベクトルのL2ノルムが非常に小さい(EPS以下)場合、ゼロベクトルとみなしValueErrorを発生させる。
 47
 48    :param v: 正規化する2Dベクトル(numpy.ndarray形式)。
 49    :returns: 正規化された単位ベクトル(numpy.ndarray形式)。
 50    :raises ValueError: ゼロベクトルを正規化しようとした場合。
 51    """
 52    n = np.linalg.norm(v)
 53    if n < EPS:
 54        raise ValueError("Zero vector cannot be normalized.")
 55    return v / n
 56
 57
 58def angle_deg(v: np.ndarray) -> float:
 59    """
 60    概要:
 61        2Dベクトルが+X軸となす角度を度数で計算する。
 62
 63    詳細説明:
 64        角度の範囲は (-180, 180] 度となる。
 65
 66    :param v: 角度を計算する2Dベクトル(numpy.ndarray形式)。
 67    :returns: +X軸となす角度(度数)。
 68    """
 69    return math.degrees(math.atan2(float(v[1]), float(v[0])))
 70
 71
 72def angle_between_deg(v1: np.ndarray, v2: np.ndarray) -> float:
 73    """
 74    概要:
 75        2つの2Dベクトル間の最小角度を度数で計算する。
 76
 77    詳細説明:
 78        計算される角度の範囲は [0, 180] 度となる。内積とアークコサインを用いて計算される。
 79
 80    :param v1: 1つ目の2Dベクトル(numpy.ndarray形式)。
 81    :param v2: 2つ目の2Dベクトル(numpy.ndarray形式)。
 82    :returns: 2つのベクトル間の最小角度(度数)。
 83    """
 84    u1 = unit(v1)
 85    u2 = unit(v2)
 86    c = float(np.clip(np.dot(u1, u2), -1.0, 1.0))
 87    return math.degrees(math.acos(c))
 88
 89
 90def is_parallel(v1: np.ndarray, v2: np.ndarray, tol: float = 1e-9) -> bool:
 91    """
 92    概要:
 93        2つの2Dベクトルが平行であるかをチェックする。
 94
 95    詳細説明:
 96        2Dベクトル `(x1, y1)` と `(x2, y2)` が平行である場合、`x1*y2 - y1*x2` (2D外積のZ成分) は0になる。
 97        この値が指定された許容誤差 `tol` よりも小さい絶対値であれば、平行とみなす。
 98
 99    :param v1: 1つ目の2Dベクトル(numpy.ndarray形式)。
100    :param v2: 2つ目の2Dベクトル(numpy.ndarray形式)。
101    :param tol: 平行とみなすための許容誤差。
102    :returns: 2つのベクトルが平行であればTrue、そうでなければFalse。
103    """
104    return abs(float(v1[0] * v2[1] - v1[1] * v2[0])) < tol
105
106
107def classify_real_diagonalizable(S: np.ndarray):
108    """
109    概要:
110        2x2行列が実数上で対角化可能であるかを判定する。
111
112    詳細説明:
113        行列Sの固有値と固有ベクトルを計算し、以下の条件で対角化可能性を判定する。
114        1. 固有値が複素数を含む場合、実数上で対角化不可能。
115        2. 実数固有値のみの場合でも、固有ベクトルが線形独立でない場合、実数上で対角化不可能。
116        それ以外の場合は、実数上で対角化可能と判定する。
117
118    :param S: 判定対象の2x2実数行列(numpy.ndarray形式)。
119    :returns: (tuple)
120        - `diagonalizable_over_R` (bool): 実数上で対角化可能であればTrue、そうでなければFalse。
121        - `message` (str): 対角化可能性に関する説明メッセージ。
122        - `eigenvalues` (numpy.ndarray): 計算された固有値。複素数を含む場合がある。
123        - `eigenvectors` (numpy.ndarray): 計算された固有ベクトル。複素数を含む場合がある。
124    """
125    vals, vecs = np.linalg.eig(S)
126
127    if np.max(np.abs(vals.imag)) > 1e-9:
128        return False, "Complex eigenvalues: no real principal axes.", vals, vecs
129
130    vals = vals.real
131    vecs = vecs.real
132
133    rank = np.linalg.matrix_rank(vecs)
134    if rank < 2:
135        return False, "Real eigenvalues found, but eigenvectors are not independent: not diagonalizable over R.", vals, vecs
136
137    return True, "Diagonalizable over the real numbers.", vals, vecs
138
139
140def sort_eigensystem(vals: np.ndarray, vecs: np.ndarray):
141    """
142    概要:
143        固有値と固有ベクトルをソートし、表示用に正規化する。
144
145    詳細説明:
146        固有値を昇順にソートし、それに対応するように固有ベクトルの順序も変更する。
147        さらに、各固有ベクトルの最初の非ゼロ成分が正になるように符号を調整し、表示を統一する。
148
149    :param vals: ソート対象の固有値(numpy.ndarray形式)。
150    :param vecs: ソート対象の固有ベクトル(numpy.ndarray形式、各列が1つの固有ベクトル)。
151    :returns: (tuple)
152        - `vals` (numpy.ndarray): ソートされ、符号調整後の固有値。
153        - `vecs` (numpy.ndarray): ソートされ、符号調整後の固有ベクトル。
154    """
155    idx = np.argsort(vals)
156    vals = vals[idx]
157    vecs = vecs[:, idx]
158
159    for j in range(2):
160        v = unit(vecs[:, j])
161        if abs(float(v[0])) > EPS:
162            if float(v[0]) < 0:
163                v = -v
164        else:
165            if float(v[1]) < 0:
166                v = -v
167        vecs[:, j] = v
168
169    return vals, vecs
170
171
172def plot_vectors(
173    ax,
174    basis_vectors,
175    image_vectors,
176    basis_labels,
177    image_labels,
178    title,
179):
180    """
181    概要:
182        指定された軸上に基底ベクトルとそれらの像を描画する。
183
184    詳細説明:
185        プロットの軸範囲は自動的に調整され、全てのベクトルが見えるように設定される。
186        基底ベクトルは黒い実線矢印、像ベクトルは赤い実線矢印で表示される。
187        軸の目盛り、ラベル、タイトルは非表示になり、グリッドと枠線も描画されない。
188
189    :param ax: 描画対象のMatplotlib Axesオブジェクト。
190    :param basis_vectors: 基底ベクトルのリスト(各要素はnumpy.ndarray形式)。
191    :param image_vectors: 基底ベクトルの像のリスト(各要素はnumpy.ndarray形式)。
192    :param basis_labels: 基底ベクトルのラベルのリスト(文字列)。
193    :param image_labels: 像ベクトルのラベルのリスト(文字列)。
194    :param title: プロットのタイトル(未使用、コード内でコメントアウトされている)。
195    :returns: なし
196    """
197    all_vecs = basis_vectors + image_vectors
198    max_norm = max(np.linalg.norm(v) for v in all_vecs)
199    lim = max(1.2, 1.25 * max_norm)
200
201    for v, label in zip(basis_vectors, basis_labels):
202        ax.arrow(
203            0, 0, float(v[0]), float(v[1]),
204            length_includes_head=True,
205            head_width=0.06 * lim,
206            head_length=0.08 * lim,
207            linewidth=1.8,
208            color='black',
209        )
210        ax.text(float(v[0]) * 1.05, float(v[1]) * 1.05, label, fontsize=11)
211
212    for v, label in zip(image_vectors, image_labels):
213        ax.arrow(
214            0, 0, float(v[0]), float(v[1]),
215            length_includes_head=True,
216            head_width=0.03 * lim,
217            head_length=0.04 * lim,
218            linewidth=1.5,
219#            linestyle="--",
220            color='red',
221        )
222        ax.text(float(v[0]) * 1.05, float(v[1]) * 1.05, label, fontsize=11)
223
224    ax.set_xlim(-lim, lim)
225    ax.set_ylim(-lim, lim)
226    ax.set_aspect("equal", adjustable="box")
227#    ax.set_title(title)
228#    ax.set_xlabel("x")
229#    ax.set_ylabel("y")
230#    ax.grid(True, alpha=0.3)
231
232    ax.set_xticks([])
233    ax.set_yticks([])
234    ax.set_xticklabels([])
235    ax.set_yticklabels([])
236
237    ax.grid(False)
238    ax.set_xlabel("")
239    ax.set_ylabel("")
240    ax.set_title("")
241
242    # 枠線(spines)を消す
243    for spine in ax.spines.values():
244        spine.set_visible(False)
245
246    # 軸線(axhline, axvline)を消したい場合はコメントアウト
247    # ax.axhline(0, color="0.75", lw=1)
248    # ax.axvline(0, color="0.75", lw=1)
249
250
251def main():
252    """
253    概要:
254        2x2実数行列の対角化を可視化し、結果を出力するメイン関数。
255
256    詳細説明:
257        コマンドライン引数から行列の要素を受け取り、以下の処理を実行する。
258        1. 元の基底ベクトルとその像を計算し、角度情報と共に表示する。
259        2. 行列が実数上で対角化可能かを判定し、その結果と理由を表示する。
260        3. 行列が対称であるかを判定する。
261        4. 実数上で対角化可能であれば、固有値と正規化された固有ベクトルを計算・表示し、
262           元の基底と固有ベクトルによる対角化基底の角度を比較する。
263           さらに、対角化された行列 `P^{-1} S P` を表示する。
264        5. 結果を2つのサブプロット(元の基底と対角化基底)または1つのサブプロット(対角化不可能時)に描画し、
265           指定されたファイル名で保存して表示する。
266
267    :returns: なし
268    """
269    parser = argparse.ArgumentParser(
270        description="Visualize diagonalization of a 2x2 real matrix."
271    )
272    parser.add_argument("S11", type=float)
273    parser.add_argument("S12", type=float)
274    parser.add_argument("S21", type=float)
275    parser.add_argument("S22", type=float)
276    parser.add_argument(
277        "--save",
278        type=str,
279        default="diagonalize2d.png",
280        help="Output figure filename (default: diagonalize2d.png)",
281    )
282    args = parser.parse_args()
283
284    S = np.array([[args.S11, args.S12], [args.S21, args.S22]], dtype=float)
285
286    a1 = np.array([1.0, 0.0])
287    a2 = np.array([0.0, 1.0])
288    Sa1 = S @ a1
289    Sa2 = S @ a2
290
291    print("=== Input matrix S ===")
292    print(S)
293    print()
294
295    print("=== Original basis ===")
296    print(f"a1 = {a1}, angle = {angle_deg(a1):.6f} deg")
297    print(f"a2 = {a2}, angle = {angle_deg(a2):.6f} deg")
298    print(f"angle(a1, a2) = {angle_between_deg(a1, a2):.6f} deg")
299    print()
300
301    print("=== Images of original basis ===")
302    print(f"S a1 = {Sa1}, parallel to a1? {is_parallel(a1, Sa1)}")
303    print(f"S a2 = {Sa2}, parallel to a2? {is_parallel(a2, Sa2)}")
304    print()
305
306    diag_ok, msg, vals, vecs = classify_real_diagonalizable(S)
307    print("=== Diagonalization test over R ===")
308    print(msg)
309    print()
310
311    symmetric = np.allclose(S, S.T, atol=1e-10)
312    print(f"Symmetric matrix? {symmetric}")
313    print()
314
315    if diag_ok:
316        vals, vecs = sort_eigensystem(vals.real, vecs.real)
317        v1 = vecs[:, 0]
318        v2 = vecs[:, 1]
319        Sv1 = S @ v1
320        Sv2 = S @ v2
321
322        print("=== Eigenvalues / eigenvectors ===")
323        print(f"lambda1 = {vals[0]:.12g}")
324        print(f"v1      = {v1}")
325        print(f"angle(v1) = {angle_deg(v1):.6f} deg")
326        print()
327        print(f"lambda2 = {vals[1]:.12g}")
328        print(f"v2      = {v2}")
329        print(f"angle(v2) = {angle_deg(v2):.6f} deg")
330        print()
331
332        ang_orig = angle_between_deg(a1, a2)
333        ang_diag = angle_between_deg(v1, v2)
334
335        print("=== Angle comparison ===")
336        print(f"angle(original basis)      = {ang_orig:.6f} deg")
337        print(f"angle(diagonalizing basis) = {ang_diag:.6f} deg")
338        if symmetric:
339            print("For a symmetric matrix, this should stay 90 deg (orthogonal diagonalization).")
340        else:
341            print("For a non-symmetric matrix, the diagonalizing basis need not be orthogonal.")
342        print()
343
344        P = np.column_stack([v1, v2])
345        D = np.linalg.inv(P) @ S @ P
346        print("=== P^{-1} S P ===")
347        print(D)
348        print()
349
350        fig, axes = plt.subplots(1, 2, figsize=(11, 5))
351        ax_left, ax_right = axes
352
353        plot_vectors(
354            ax_left,
355            basis_vectors=[a1, a2],
356            image_vectors=[Sa1, Sa2],
357            basis_labels=["a1", "a2"],
358            image_labels=["S a1", "S a2"],
359            title="Original basis",
360        )
361        plot_vectors(
362            ax_right,
363            basis_vectors=[v1, v2],
364            image_vectors=[Sv1, Sv2],
365            basis_labels=["v1", "v2"],
366            image_labels=["S v1", "S v2"],
367            title="Diagonalizing basis (real eigenvectors)",
368        )
369    else:
370        print("=== Eigenvalues (possibly complex) ===")
371        print(vals)
372        print()
373
374        fig, ax = plt.subplots(1, 1, figsize=(5.5, 5))
375        plot_vectors(
376            ax,
377            basis_vectors=[a1, a2],
378            image_vectors=[Sa1, Sa2],
379            basis_labels=["a1", "a2"],
380            image_labels=["S a1", "S a2"],
381            title="Original basis (not diagonalizable over R)",
382        )
383
384    fig.suptitle("2D matrix visualization", fontsize=14)
385    fig.tight_layout()
386    fig.savefig(args.save, dpi=160)
387    print(f"Saved figure: {args.save}")
388    plt.show()
389
390
391if __name__ == "__main__":
392    main()