Juliaで行列の一般化固有値問題を解く:gges3!の基本と活用法

2025-05-27

まず、基本的なことから始めましょう。

  • ! (bang/破壊的メソッド)
    Juliaでは、関数名の最後に!が付いている場合、その関数が引数として渡されたオブジェクトを「インプレース(in-place)」で変更する、つまり元のオブジェクトを破壊的に書き換えることを意味します。gges3!()も同様に、入力された行列を計算結果で上書きする可能性があります。
  • gges3
    これはLAPACKライブラリ内の特定のルーチン名です。LAPACKのルーチン名は通常、特定の目的を持つ線形代数問題を解決するために設計されています。ggesは「Generalized Eigenvalue problems, Schur form」を意味することが多く、行列の一般化固有値問題(Generalized Eigenvalue Problem)をシュール分解(Schur decomposition)の形で解くためのものです。末尾の3は、LAPACKのルーチンが改訂されて、より新しいバージョンであることを示唆している可能性があります。
  • LAPACK
    これは "Linear Algebra Package" の略で、高性能な線形代数計算のためのFortranライブラリの集合体です。JuliaのLinearAlgebraモジュールは、多くの線形代数操作において、このLAPACKライブラリを内部で呼び出して高速な計算を実現しています。
  • LinearAlgebra
    これはJuliaの標準ライブラリの一つで、線形代数に関する様々な関数や型を提供します。行列の演算、固有値・固有ベクトル計算、行列分解などが含まれます。

gges3!()の機能

gges3!()は、一般化固有値問題(Generalized Eigenvalue Problem)を解くための関数です。具体的には、2つの行列 AB が与えられたときに、以下の形式の一般化固有値問題

Ax=λBx

を解き、その結果をシュール形式(Schur form)で返します。

シュール形式とは?

シュール分解(Schur decomposition)は、正方行列を上三角行列(シュール形式)に変換する行列分解の一種です。一般化シュール分解では、2つの行列 A と B をそれぞれ S と T という上三角行列に同時に変換します。

A=QSZH B=QTZH

ここで、Q と Z はユニタリー行列(または直交行列)です。

gges3!()は、これらの S, T, Q, Z などの情報を計算して返します。これらの情報から、一般化固有値 λ を求めることができます(S の対角要素と T の対角要素の比として得られます)。

主な用途

gges3!()は、以下のような場面で利用されます。

  • 行列の安定性解析
    特定の線形システムの安定性を評価するために、そのシステムの一般化固有値を解析することがあります。
  • 一般化固有値問題の解決
    制御システム、振動解析、安定性解析など、様々な工学や科学の分野で現れる一般化固有値問題を解くために使用されます。
  • LAPACKの知識
    LinearAlgebra.LAPACK内の関数を直接使用する場合、LAPACKのルーチンがどのように動作するか(例えば、Fortranの列優先順序など)についての基本的な理解があると、より効果的に利用できます。
  • 破壊的メソッド
    前述の通り、!が付いているため、入力された行列が変更される可能性があります。元の行列を保持したい場合は、事前にcopy()などで複製を作成しておく必要があります。
  • 引数
    gges3!()は、どのような引数を取るか(例えば、行列AとB、結果を格納する配列など)は、Juliaのドキュメントで確認する必要があります。LAPACKルーチンは通常、多くのオプション引数を持つため、用途に応じて適切なものを指定する必要があります。


LAPACKException とその意味

gges3!()の呼び出し中に最もよく遭遇するエラーは、LAPACKExceptionです。これは、LAPACKルーチン自体が内部でエラーを検知した場合に発生します。通常、エラーメッセージには括弧内に整数が記載されています(例: LAPACKException(N))。

  • info > 0 (正の数)
    これは、計算上の問題が発生したことを示します。具体的な意味は、呼び出されたLAPACKルーチンによって異なりますが、gges3の場合、通常は以下のいずれかを意味します。
    • 非収束 (Non-convergence)
      固有値計算の反復プロセスが指定された最大反復回数内に収束しなかった場合。これは、問題自体が病的に悪条件であるか、数値的に不安定である場合に発生します。
    • 特異性 (Singularity) / ランク落ち (Rank-deficient)
      行列が特異(逆行列が存在しない)であったり、ランクが落ちていたりする場合。一般化固有値問題では、B が特異である場合に問題となることがあります。
    • トラブルシューティング
      • 問題の条件数
        入力行列 AB の条件数(condition number)を確認してください。条件数が非常に大きい(悪条件な)行列は、数値的に不安定になりやすく、収束問題を引き起こすことがあります。cond(A)cond(B) を使用して確認できます。
      • スケーリング
        行列の要素が非常に大きかったり小さかったりする場合、スケーリングを行うことで数値的な安定性が向上し、収束しやすくなることがあります。
      • 異なるアルゴリズムの検討
        gges3が収束しない場合、同じ目的を達成するための別のLAPACKルーチンや、より高レベルのJuliaの線形代数関数を検討することも一つの手です。ただし、gges3は一般化固有値問題とシュール形式に特化しているため、代替を見つけるのが難しい場合もあります。
      • 行列の構造の確認
        特定の構造を持つ行列(例えば、ほぼ特異な行列、非常に疎な行列など)は、特定のアルゴリズムで問題を引き起こすことがあります。
  • info < 0 (負の数)
    これは、-info 番目の引数が不正な値だったことを示します。例えば、行列の次元が不適切だったり、特定のオプション引数に許可されていない値が渡されたりした場合に発生します。
    • トラブルシューティング
      • 引数の確認
        gges3!()に渡しているすべての引数の型、次元、値がLAPACKの要件を満たしているか、JuliaのドキュメントやLAPACKのggesルーチンの詳細な説明(NetlibのLAPACKサイトなど)で確認してください。特に、行列のサイズや先行次元(leading dimension)に関する制約は重要です。
      • データの健全性
        入力行列にNaN (Not a Number) や Inf (Infinity) などの不正な値が含まれていないか確認してください。これらの値が存在すると、LAPACKルーチンが予期せぬ動作をしたり、エラーを報告したりする可能性があります。
  • info = 0
    問題なく計算が完了しました。

環境に起因する問題

  • 特定のハードウェア/OSでの問題
    稀に、特定のCPUアーキテクチャやオペレーティングシステム上で、LAPACKルーチンが予期せぬ挙動を示すことがあります。これは非常に稀ですが、可能性として念頭に置くべきです。
    • トラブルシューティング
      • 別の環境での試行
        もし可能であれば、別のマシンやOSで同じコードを試してみて、問題が環境固有のものかどうかを特定します。
      • Juliaコミュニティへの報告
        環境固有の問題が疑われる場合は、JuliaのDiscourseフォーラムやGitHubのリポジトリで問題を報告し、コミュニティの助けを求めるのが最善です。
  • BLAS/LAPACKライブラリの問題
    Juliaは内部でBLAS (Basic Linear Algebra Subprograms) やLAPACKライブラリを呼び出しますが、これらのライブラリのバージョンや設定に問題がある場合、予期せぬエラーが発生することがあります。特に、MKL (Intel Math Kernel Library) などの最適化されたBLAS/LAPACKライブラリを使用している場合、設定が競合したり、互換性の問題が生じたりすることが稀にあります。
    • トラブルシューティング
      • Juliaの再インストール
        まずは、Juliaをクリーンに再インストールしてみるのが最も簡単な解決策となる場合があります。これにより、デフォルトで提供される安定したBLAS/LAPACK構成に戻せます。
      • MKL.jlなどの確認
        もしMKL.jlのようなパッケージを使用している場合、その設定やバージョンが現在のJuliaのバージョンと互換性があるか確認してください。場合によっては、MKL.jlを再ビルドしたり、一時的に使用を停止したりすることで問題が解決することがあります。
      • BLASスレッド数
        マルチスレッドBLASライブラリを使用している場合、スレッド数の設定がパフォーマンスや安定性に影響を与えることがあります。BLAS.set_num_threads(N)でスレッド数を調整してみるのも一つの手です。

コーディング上の一般的な誤り

  • メモリ割り当ての問題
    大規模な行列を扱う場合、メモリ不足が原因でエラーが発生することがあります。
    • トラブルシューティング
      システムのメモリ使用量を確認し、必要に応じてメモリを増設するか、より小さな問題サイズで試行してください。
  • 破壊的メソッドの理解不足
    !が付いているため、入力行列が変更されます。もし、元の行列を保持したい場合は、事前にcopy(A)copy(B)などで複製を作成してから関数に渡す必要があります。
  • 引数の順序や型
    gges3!()のような低レベルのLAPACKラッパーは、引数の順序や型が非常に厳格です。誤った順序で引数を渡したり、期待される型と異なる型の引数を渡したりすると、エラーが発生します。
    • トラブルシューティング
      Juliaのドキュメントを再確認し、引数のシグネチャ(Signature)と正確に一致しているかを確認してください。
  1. エラーメッセージを読み解く
    LAPACKException(N)Nが何を示しているかを最初に理解しようとします。LAPACKのドキュメント(特にggesルーチンの詳細)を参照すると、正のNの値が何を示しているかが記載されていることがあります。負のNは引数の問題です。
  2. 最小の再現可能な例 (MWE) の作成
    問題を特定するために、エラーが発生する最小限のコードスニペットを作成します。これにより、問題の原因がコードのどの部分にあるかを絞り込みやすくなります。
  3. 入力データの確認
    入力行列 AB の値、型、次元、数値的な健全性(NaN/Infの有無、条件数など)を徹底的に確認します。
  4. Juliaのバージョンとパッケージの確認
    使用しているJuliaのバージョンと、関連するパッケージ(LinearAlgebraMKL.jlなど)のバージョンが最新であるか、または互換性のあるバージョンであるかを確認します。


ここでは、いくつかの使用例とその結果の解釈について説明します。

基本的な使用例

一般化固有値問題 Ax=lambdaBx を解く最も基本的な例です。

using LinearAlgebra

# 2つの行列 A と B を定義
A = [
    1.0  2.0
    3.0  4.0
]

B = [
    5.0  6.0
    7.0  8.0
]

# gges3! を呼び出す
# この関数は、いくつかの戻り値をタプルで返します。
# S: 一般化シュール分解の上三角行列 (Aに対応)
# T: 一般化シュール分解の上三角行列 (Bに対応)
# Q: 左のユニタリー変換行列
# Z: 右のユニタリー変換行列
# α: 一般化固有値の分子部分 (通常は複素数)
# β: 一般化固有値の分母部分 (通常は実数)
# info: LAPACKからのステータスコード (0は成功)
# rconde: 固有値の条件数 (オプション)
# rcondv: 固有ベクトルの条件数 (オプション)

# 注: gges3! は非常に多くの戻り値を持ちます。
# ここでは、よく使われるものを抽出して変数に代入します。
# 実際には、gges3! の定義をREPLで ?LinearAlgebra.LAPACK.gges3! と入力して確認するのが確実です。
# ただし、直接アクセスするLAPACKルーチンは、Juliaの標準ライブラリの中でも特に低レベルであり、
# 一般的な用途ではより高レベルな関数 (例: eigen(A, B)) を使うことが推奨されます。
# gges3! の戻り値は、元のLAPACK関数の出力に非常に近いです。
# 標準的な呼び出し形式は以下のようになります(ただし、完全なシグネチャはJuliaのバージョンに依存します)。

# 実際には、以下のようにたくさんの戻り値があります。
# (S, T, Q, Z, α, β, info, rconde, rcondv) = LinearAlgebra.LAPACK.gges3!(
#     'N', # JOBVS: Qを計算するか ('N' for no, 'V' for yes)
#     'N', # JOBVS: Zを計算するか ('N' for no, 'V' for yes)
#     'N', # SORT: 固有値をソートするか ('N' for no, 'S' for yes)
#     nothing, # SELECT: 固有値ソートのための関数 (SORT='S'の場合)
#     A, B # 入力行列 (破壊的に変更される可能性がある)
# )

# よりシンプルで、よく使われるパターンで呼び出してみます。
# gges3! は様々なオーバーロードが存在するため、正確な呼び出し方はJuliaのバージョンやコンテキストに依存しますが、
# 一般的なLAPACKの呼び出し規約に従います。
# ここでは、QとZも計算するようにします。

# 作業用メモリの準備 (LAPACKルーチンによっては必要)
# しかし、Juliaのラッパーは通常、内部で適切にメモリを管理します。
# ここでは、最も一般的なLAPACKラッパーの挙動として、入力行列を直接渡す形を想定します。

# 'V' を指定すると、Q と Z (左・右の変換行列) も計算されます。
# 'N' を指定すると、それらは計算されず、より高速です。
# ここでは、全ての結果を見たいので 'V' を指定します。
jobvsl = 'V' # Compute left Schur vectors (Q)
jobvsr = 'V' # Compute right Schur vectors (Z)
sort = 'N'   # Do not sort eigenvalues

# gges3! は破壊的メソッドなので、入力行列をコピーしてから渡します。
Ac = copy(A)
Bc = copy(B)

# 戻り値の型と順序は、実際のLAPACKルーチンとそのJuliaラッパーの定義によりますが、
# 一般的には (α, β, Q, Z, S, T) のような形で返されることが多いです。
# Julia 1.6以降のLinearAlgebra.jlの内部実装を見ると、
# gges3! は以下の戻り値を持つことが多いようです。
# (S, T, Q, Z, ALPHA, BETA, INFO)
# ただし、ALPHA, BETA は通常、固有値の分子と分母に対応します。

try
    # gges3! のシグネチャは複雑なため、正確な引数を定義することは難しい場合があります。
    # しかし、多くの場合、行列 A と B を渡すだけで、残りの引数はデフォルト値で処理されます。
    # ここでは、REPLで `?LinearAlgebra.LAPACK.gges3!` と入力して確認できるシグネチャを参考にします。
    # 一般的な `gges` ラッパーのシグネチャは `gges!(jobvsl, jobvsr, select, A, B)` のようです。

    # select引数は、固有値をソートする場合に利用する関数です。今回はソートしないので nothing。
    S, T, Q, Z, alpha, beta, info = LinearAlgebra.LAPACK.gges3!(
        jobvsl, jobvsr, nothing, Ac, Bc
    )

    if info == 0
        println("=== gges3!() 成功 ===")
        println("元の行列 A:\n", A)
        println("元の行列 B:\n", B)
        println("\nS (一般化シュール分解 A の上三角行列):\n", S)
        println("\nT (一般化シュール分解 B の上三角行列):\n", T)
        println("\nQ (左変換行列):\n", Q)
        println("\nZ (右変換行列):\n", Z)

        println("\n一般化固有値 (lambda = alpha ./ beta):")
        generalized_eigenvalues = alpha ./ beta
        println(generalized_eigenvalues)

        # 検証: A * Z = Q * S および B * Z = Q * T が成り立つか
        # 厳密には A * Z = Q * S かつ B * Z = Q * T は成り立ちません。
        # 正しくは A = Q * S * Z' および B = Q * T * Z' です。
        # つまり、A * Z = Q * S および B * Z = Q * T が成り立つはずです。
        # Z' は Z の共役転置 (adjoint) ですが、実数行列の場合 Z' は Z の転置 (transpose) です。
        # LAPACKの慣例では Z は右の変換行列で、Z^H (共役転置) を右から乗じることで
        # シュール形式に変換されます。つまり S = Q^H * A * Z, T = Q^H * B * Z です。
        # したがって、A = Q * S * Z^H と B = Q * T * Z^H を検証します。

        println("\n--- 検証 ---")
        println("norm(A - Q * S * Z'): ", norm(A - Q * S * Z'))
        println("norm(B - Q * T * Z'): ", norm(B - Q * T * Z'))

        # 一般化固有値の計算
        # SとTは上三角行列なので、対角要素S[i,i]とT[i,i]の比が一般化固有値です。
        calculated_eigenvalues = [S[i, i] / T[i, i] for i in 1:size(S, 1)]
        println("\n対角要素から計算した一般化固有値:\n", calculated_eigenvalues)

        # alpha ./ beta と一致することを確認
        println("alpha ./ beta:\n", alpha ./ beta)


    else
        println("=== gges3!() エラー: info = ", info, " ===")
        # info の値に基づいてエラーをデバッグします。
        # info < 0: 入力引数のエラー
        # info > 0: 計算上の問題(非収束など)
    end

catch e
    if isa(e, LAPACKException)
        println("LAPACKException: ", e.info)
        println("LAPACKルーチンでエラーが発生しました。info値を確認してください。")
    else
        rethrow(e) # その他のエラーは再スロー
    end
end

コードの説明

  1. using LinearAlgebra: 線形代数モジュールをインポートします。
  2. 行列 AB の定義: 一般化固有値問題を解く対象となる2つの正方行列を定義します。
  3. Ac = copy(A) / Bc = copy(B): gges3!()は破壊的メソッド(関数名の末尾に!が付いている)なので、元の行列ABを変更します。そのため、元の値を保持したい場合は、事前にcopy()関数で複製を作成してから関数に渡す必要があります。
  4. jobvsl, jobvsr, sort, select: これらの引数は、LAPACKのdgges (倍精度実数) または zgges (倍精度複素数) ルーチンに渡されるオプションに対応します。
    • jobvsl (Char): 左のシュールベクトル(行列Q)を計算するかどうかを指定します。
      • 'N' (No): 計算しない。
      • 'V' (Vectors): 計算する。
    • jobvsr (Char): 右のシュールベクトル(行列Z)を計算するかどうかを指定します。
      • 'N' (No): 計算しない。
      • 'V' (Vectors): 計算する。
    • select (Function or nothing): 固有値をソートする場合に、ソートの基準となる関数を指定します。sort'S'の場合に有効です。今回はソートしないのでnothingを渡します。
  5. LinearAlgebra.LAPACK.gges3!(...)の呼び出し: 定義した引数と共にgges3!関数を呼び出します。
  6. 戻り値: gges3!()は複数の値をタプルとして返します。
    • S: A に対応する上三角行列。
    • T: B に対応する上三角行列。
    • Q: 左側のユニタリー(または直交)変換行列。
    • Z: 右側のユニタリー(または直交)変換行列。
    • alpha (Vector{ComplexF64}): 一般化固有値の分子部分(Sの対角要素)。S[i,i]に対応。
    • beta (Vector{Float64}): 一般化固有値の分母部分(Tの対角要素)。T[i,i]に対応。
    • info (Int): LAPACKルーチンからのステータスコード。0は成功を意味します。負の数や正の数はエラーを示します(前の説明を参照)。
  7. 一般化固有値の計算: 一般化固有値 lambda_i は alpha[i] / beta[i] として計算されます。
  8. 検証: A = Q * S * Z' および B = Q * T * Z' が成り立つことをnorm()関数を使って検証します。Z'Zの共役転置です。浮動小数点演算の性質上、厳密にゼロにはなりませんが、非常に小さい値になるはずです。

複素数行列での使用例

gges3!()は複素数行列も扱うことができます。

using LinearAlgebra

A_complex = [
    1.0 + 1.0im  2.0 - 1.0im
    3.0 + 0.0im  4.0 + 2.0im
]

B_complex = [
    5.0 + 0.0im  6.0 + 1.0im
    7.0 - 1.0im  8.0 + 0.0im
]

Ac_complex = copy(A_complex)
Bc_complex = copy(B_complex)

try
    S_c, T_c, Q_c, Z_c, alpha_c, beta_c, info_c = LinearAlgebra.LAPACK.gges3!(
        'V', 'V', nothing, Ac_complex, Bc_complex
    )

    if info_c == 0
        println("\n=== 複素数行列での gges3!() 成功 ===")
        println("元の行列 A_complex:\n", A_complex)
        println("元の行列 B_complex:\n", B_complex)
        println("\nS_c:\n", S_c)
        println("\nT_c:\n", T_c)
        println("\nQ_c:\n", Q_c)
        println("\nZ_c:\n", Z_c)

        println("\n一般化固有値 (lambda = alpha_c ./ beta_c):")
        generalized_eigenvalues_c = alpha_c ./ beta_c
        println(generalized_eigenvalues_c)

        println("\n--- 検証 (複素数) ---")
        # 複素数の場合、Zの共役転置は Z' ではなく Zc' (adjoint) を使う
        println("norm(A_complex - Q_c * S_c * Z_c'): ", norm(A_complex - Q_c * S_c * Z_c'))
        println("norm(B_complex - Q_c * T_c * Z_c'): ", norm(B_complex - Q_c * T_c * Z_c'))

    else
        println("=== 複素数行列での gges3!() エラー: info = ", info_c, " ===")
    end

catch e
    if isa(e, LAPACKException)
        println("LAPACKException (Complex): ", e.info)
    else
        rethrow(e)
    end
end

gges3!select引数を使うと、計算された固有値を特定の条件でソートし、それに応じてシュール形式のブロックの順序を制御できます。これは、システム安定性解析などで、特定の固有値(例えば、実部が負のもの)だけを抽出したい場合に有用です。

例として、実部が0より小さい固有値だけをシュール形式の左上に集める場合を考えます。

using LinearAlgebra

A_sort = [
    1.0  -1.0
    2.0  -2.0
]

B_sort = [
    1.0  0.0
    0.0  1.0
]

Ac_sort = copy(A_sort)
Bc_sort = copy(B_sort)

# 固有値をソートするための関数を定義
# alpha, beta, info は LAPACK の gges で返される値に対応
# この関数は、ソートしたい固有値 (alpha[j]/beta[j]) が条件を満たす場合に true を返します。
# ここでは、実部が0未満の固有値を選択します。
# LAPACKの select function のシグネチャは (αr, αi, β) に対応します。
# αr: real part of alpha
# αi: imaginary part of alpha
# β: beta value
my_select_function(αr, αi, β) = real((αr + αi*im) / β) < 0.0

try
    S_s, T_s, Q_s, Z_s, alpha_s, beta_s, info_s = LinearAlgebra.LAPACK.gges3!(
        'V', 'V', 'S', # 'S'を指定してソートを有効にする
        my_select_function, # ソート基準関数を渡す
        Ac_sort, Bc_sort
    )

    if info_s == 0
        println("\n=== select 引数を使った gges3!() 成功 ===")
        println("元の行列 A_sort:\n", A_sort)
        println("元の行列 B_sort:\n", B_sort)
        println("\nS_s (ソートされたシュール形式の A):\n", S_s)
        println("\nT_s (ソートされたシュール形式の B):\n", T_s)

        println("\nソートされた一般化固有値:")
        generalized_eigenvalues_s = alpha_s ./ beta_s
        println(generalized_eigenvalues_s)

        println("\n--- 検証 (ソート) ---")
        println("norm(A_sort - Q_s * S_s * Z_s'): ", norm(A_sort - Q_s * S_s * Z_s'))
        println("norm(B_sort - Q_s * T_s * Z_s'): ", norm(B_sort - Q_s * T_s * Z_s'))

    else
        println("=== select 引数を使った gges3!() エラー: info = ", info_s, " ===")
    end

catch e
    if isa(e, LAPACKException)
        println("LAPACKException (Sorted): ", e.info)
    else
        rethrow(e)
    end
end

重要な注意点:

  • LAPACKルーチンはFortranで記述されているため、行優先・列優先の違い(Juliaは列優先)や、引数の厳密な型要件(Char型など)に注意する必要があります。Juliaのラッパーはこれらの差異を吸収しようとしますが、低レベルな部分ではLAPACKの慣例が強く残ります。
  • gges3!()の正確なシグネチャや戻り値の形式は、Juliaのバージョンや特定のBLAS/LAPACKバックエンドによって若干異なる場合があります。最も確実な方法は、JuliaのREPLで ?LinearAlgebra.LAPACK.gges3! と入力して、現在の環境でのドキュメント文字列を確認することです。
  • LinearAlgebra.LAPACK.gges3!()は、Juliaのより高レベルなLinearAlgebra.eigen()LinearAlgebra.schur()のような関数が内部で利用する低レベルなLAPACKラッパーです。通常、これらの高レベルな関数の方が使いやすく、エラーハンドリングもより洗練されています。


しかし、Julia には、より高レベルで使いやすい代替手段がいくつか提供されています。これらの方法は、ほとんどのユースケースにおいて gges3!() を直接呼び出すよりも推奨されます。

LinearAlgebra.eigen(A, B)

これは、一般化固有値問題 Ax=lambdaBx の固有値固有ベクトルを直接計算するための、最も一般的で推奨される方法です。

  • 欠点:
    • シュール分解の上三角行列 S,T や変換行列 Q,Z は直接返されません。これらが必要な場合は、schur(A, B) を使用します。
  • 利点:
    • 非常に使いやすいインターフェース。
    • 一般化固有値とその対応する固有ベクトルを直接取得できる。
    • 通常、内部で gges や他の最適なLAPACKルーチンを自動的に呼び出すため、パフォーマンスも優れています。
  • 戻り値: GeneralizedEigen 型のオブジェクトを返します。このオブジェクトは、vals (固有値のベクトル) と vectors (固有ベクトルを行列の列として含む行列) のフィールドを持ちます。
  • 機能: 行列 A と B が与えられたときに、一般化固有値 lambda と対応する右固有ベクトル x を計算します。

使用例

using LinearAlgebra

A = [1.0  2.0;
     3.0  4.0]

B = [5.0  6.0;
     7.0  8.0]

# 一般化固有値分解を実行
G = eigen(A, B)

println("--- eigen(A, B) の例 ---")
println("一般化固有値 (lambda):")
println(G.values)

println("\n一般化固有ベクトル:")
println(G.vectors)

# 検証: A * x = lambda * B * x
# G.vectors の各列が固有ベクトル x に対応
# G.values の各要素が対応する固有値 lambda に対応
# 例: 最初の固有ベクトルと固有値で検証
lambda1 = G.values[1]
x1 = G.vectors[:, 1]

println("\n検証 (最初の固有値と固有ベクトル):")
println("A * x1:\n", A * x1)
println("lambda1 * B * x1:\n", lambda1 * B * x1)
println("差のノルム: ", norm(A * x1 - lambda1 * B * x1)) # 非常に小さい値になるはず

LinearAlgebra.schur(A, B)

これは、一般化固有値問題 Ax=lambdaBx の一般化シュール分解を直接計算するための方法です。

  • 欠点:
    • eigen(A, B) と異なり、固有ベクトルを直接返しません。必要な場合は、シュール分解の結果から別途計算する必要があります。
  • 利点:
    • gges3!() と同じ一般化シュール分解の結果を、より安全で高レベルなインターフェースで取得できる。
    • 数値的に安定した方法で固有値と関連する部分空間を取得できる。
    • 特に、行列が非対称であるか、固有ベクトルが不安定である場合に有用です。
  • 戻り値: GeneralizedSchur 型のオブジェクトを返します。このオブジェクトは、S, T, Q, Z, α (アルファ), β (ベータ) のフィールドを持ちます。α ./ β が一般化固有値に相当します。
  • 機能: 行列 A と B を、変換行列 Q,Z を用いて上三角行列 S,T に分解します(A=QSZH,B=QTZH)。

使用例

using LinearAlgebra

A = [1.0  2.0;
     3.0  4.0]

B = [5.0  6.0;
     7.0  8.0]

# 一般化シュール分解を実行
GS = schur(A, B)

println("\n--- schur(A, B) の例 ---")
println("S (一般化シュール分解 A の上三角行列):\n", GS.S)
println("\nT (一般化シュール分解 B の上三角行列):\n", GS.T)
println("\nQ (左変換行列):\n", GS.Q)
println("\nZ (右変換行列):\n", GS.Z)

println("\n一般化固有値 (alpha ./ beta):\n", GS.alpha ./ GS.beta)

# 検証: A = Q * S * Z' および B = Q * T * Z' が成り立つか
println("\n--- 検証 ---")
println("norm(A - GS.Q * GS.S * GS.Z'): ", norm(A - GS.Q * GS.S * GS.Z'))
println("norm(B - GS.Q * GS.T * GS.Z'): ", norm(B - GS.Q * GS.T * GS.Z'))

スパース行列の場合: Arpack.eigs(A, B, ...) または KrylovKit.eigsolve(...)

大規模なスパース行列の場合、密行列用の eigen()schur() は計算コストが高すぎることがあります。このような場合、反復法に基づくパッケージを使用します。

  • KrylovKit.eigsolve(...):
    • KrylovKit.jl は、Krylov 部分空間法に基づく汎用的な線形代数ソルバーを提供するパッケージです。
    • 特定の領域(例: 実部が最大の固有値など)の少数の固有値/固有ベクトルを求めるのに非常に強力です。
    • 線形写像として定義された演算子を直接扱えるため、明示的に行列を構築する必要がない場合もあります。
    • 一般化固有値問題にも対応しています。
  • Arpack.eigs(A, B, ...):
    • Arpack.jl パッケージは、ARPACK ライブラリの Julia ラッパーです。
    • 大規模な行列の少数の(特定の基準で最大または最小の)固有値と固有ベクトルを計算するのに適しています。
    • 一般化固有値問題にも対応しています (eigs(A, B, ...))。
    • 利点: スパース性や大規模な問題に非常に効率的。
    • 欠点: 全ての固有値を求めるのには適していません。

使用例 (概念)

# using Arpack # Arpack.jl パッケージをインストールして使用

# A_sparse = sparse(rand(1000, 1000))
# B_sparse = sparse(I, 1000, 1000) # 単位行列

# # 最大絶対値の固有値2つを求める
# λ, X, nconv, niter, nmult, resid = eigs(A_sparse, B_sparse, nev=2, which=:LM)

# println("Arpack.eigs を使った例:")
# println("固有値:", λ)
# println("固有ベクトル:", X)


# using KrylovKit # KrylovKit.jl パッケージをインストールして使用

# # A_op と B_op は、行列 A と B の作用(行列-ベクトル積)を定義する関数
# # eigsolve は線形写像を直接扱う
# λ_kk, X_kk, info_kk = eigsolve(x -> A_sparse * x, x -> B_sparse * x,
#                                  rand(1000), 2, :LM)

# println("KrylovKit.eigsolve を使った例:")
# println("固有値:", λ_kk)
# println("固有ベクトル:", X_kk)

もし行列 B が正定値対称行列(またはエルミート行列)である場合、一般化固有値問題 Ax=lambdaBx を標準的な固有値問題に変換して解くことができます。

  • 欠点: Bが正定値対称行列であるという強い制約があります。また、変換にコレスキー分解と三角行列の逆行列計算が必要になります。
  • 利点: 変換後の問題は標準的な固有値問題になり、より最適化されたアルゴリズム(特にCが対称になる場合)を利用できる可能性があります。
  • 手順:
    1. B のコレスキー分解を行う: B=LLH (ここで L は下三角行列)。
    2. 問題を L−1AL−Hy=lambday に変換する。ここで y=LHx。
    3. 新しい行列 C=L−1AL−H の通常の固有値分解 Cy=lambday を実行する。
    4. 元の固有ベクトル x は x=L−Hy から再構築する。

使用例

using LinearAlgebra

A = [1.0  2.0;
     3.0  4.0]

# B は正定値対称行列である必要がある
B_pd = [5.0  1.0;
        1.0  2.0]

# 1. B のコレスキー分解
C = cholesky(B_pd)
L = C.L

# 2. 新しい行列 C_prime を構築 (C_prime = L \ A / L')
# L \ A は L^(-1) * A と同じ
# (L') は L^H (L の共役転置)。実数の場合は転置 L^T
C_prime = L \ A / L'

println("\n--- Bが正定値対称の場合の代替法 ---")
println("変換された行列 C_prime:\n", C_prime)

# 3. C_prime の標準固有値分解を実行
F_prime = eigen(C_prime)

println("C_prime の固有値:\n", F_prime.values)
println("C_prime の固有ベクトル:\n", F_prime.vectors)

# 4. 元の一般化固有ベクトル x を再構築 (x = L' \ y)
generalized_eigenvectors_reconstructed = L' \ F_prime.vectors

println("\n再構築された一般化固有ベクトル:\n", generalized_eigenvectors_reconstructed)

# 元の eigen(A, B) と比較
G_orig = eigen(A, B)
println("\n元の eigen(A, B) の固有値:\n", G_orig.values)
println("元の eigen(A, B) の固有ベクトル:\n", G_orig.vectors)

# ここで得られた固有ベクトルは、スケーリングが異なる可能性があるため、
# 直接一致しない場合がありますが、方向は同じになります。

ほとんどの場合、LinearAlgebra.eigen(A, B) または LinearAlgebra.schur(A, B) を使用することが、LinearAlgebra.LAPACK.gges3!() を直接呼び出すよりも推奨されます。これらはより高レベルで安全なインターフェースを提供し、内部で最適なLAPACKルーチン(通常は gges)を自動的に呼び出すため、パフォーマンスも損なわれません。