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

draw_band_fs.py をダウンロード

draw_band_fs.py
draw_band_fs.py
   1"""
   2バンド構造、状態密度 (DOS)、フェルミエネルギー (EF)、有効質量 (m*)、およびフェルミ面を
   3様々な次元とモデルでシミュレートしプロットするスクリプト。
   4
   5詳細説明:
   6このスクリプトは、1次元から3次元までの単純な電子モデル(自由電子、タイトバインディング、
   7またはほぼ自由電子)に基づき、以下の機能を提供する。
   8- バンド構造の計算とプロット(1D直線パスまたは高対称点パスに沿って)。
   9- DOSの計算とプロット(ガウスブロードニングまたはヒストグラム)。
  10- 電子数 `nelec` からDOS積分を通じてフェルミエネルギー `EF` を決定する機能、
  11  または `EF` を固定値として指定する機能。
  12- 1Dバンドプロットに重ねて有効質量 `m*(k)` を表示する機能。
  13- 1D、2D、3Dのフェルミ面をプロットする機能(3Dにはscikit-imageが必要)。
  14
  15利用可能なモデル:
  16- 'free': 自由電子モデル。
  17- 'tb': タイトバインディングモデル。
  18- 'nfe': ほぼ自由電子 (NFE) モデル(逆格子ベクトル間のカップリングVを考慮)。
  19
  20関連リンク:
  21:doc:`draw_band_fs_usage`
  22"""
  23#!/usr/bin/env python3
  24# -*- coding: utf-8 -*-
  25
  26import numpy as np
  27import matplotlib.pyplot as plt
  28import matplotlib.colors as mcolors
  29import argparse
  30from mpl_toolkits.mplot3d.art3d import Poly3DCollection
  31
  32# skimage.measure is imported lazily in plot_3d_fermi_surface to avoid crash if not installed
  33
  34NORMALIZATION_UNIT = 2 * np.pi  # k_int * 2π -> k_phys (a=1)
  35
  36
  37# =============================================================================
  38# Utilities
  39# =============================================================================
  40
  41def reduce_to_first_bz(k_int: float) -> float:
  42    """k_int を第1BZ [-0.5, 0.5) に還元(周期1)
  43
  44    詳細説明:
  45    内部単位で表現されたk値を、周期境界条件を適用して第1ブリルアンゾーン
  46    ([-0.5, 0.5) の範囲) にマッピングし直します。
  47
  48    :param k_int: 内部単位のk値。
  49    :type k_int: float
  50    :returns: 第1BZに還元されたk値。
  51    :rtype: float
  52    """
  53    return ((k_int + 0.5) % 1.0) - 0.5
  54
  55
  56def build_g_vecs(dim: int) -> np.ndarray:
  57    """dim 次元の最小セットのG: 0 と ±e_i を物理単位で返す
  58
  59    詳細説明:
  60    指定された次元における逆格子ベクトルGの最小セットを構築します。
  61    これは、0ベクトルと、各軸方向の単位ベクトル (±e_i) を含みます。
  62    結果は物理単位 (2π/a) に正規化されます。
  63
  64    :param dim: 空間次元 (1, 2, or 3)。
  65    :type dim: int
  66    :returns: Gベクトル群の配列。各行がGベクトル。
  67    :rtype: np.ndarray
  68    """
  69    ints = [np.zeros(dim, dtype=float)]
  70    for i in range(dim):
  71        v = np.zeros(dim, dtype=float)
  72        v[i] = 1.0
  73        ints.append(v.copy())
  74        ints.append(-v.copy())
  75    return np.array(ints, dtype=float) * NORMALIZATION_UNIT
  76
  77
  78def nband_of_model(dim: int, mode: str) -> int:
  79    """このtoyモデルでのバンド本数(スピンは含めない)
  80
  81    詳細説明:
  82    指定されたモデルと空間次元に基づき、スピン縮退を含まないバンドの総数を返します。
  83    - 'tb': タイトバインディングモデルは1バンド。
  84    - 'free'/'nfe': 自由電子モデルおよびほぼ自由電子モデルは、0ベクトルと±e_iベクトル
  85      によって決定される数のバンド(1 + 2 * dim)を持ちます。
  86
  87    :param dim: 空間次元 (1, 2, or 3)。
  88    :type dim: int
  89    :param mode: モデルの種類 ('tb', 'free', 'nfe')。
  90    :type mode: str
  91    :returns: モデルのバンド本数。
  92    :rtype: int
  93    :raises ValueError: 未知のモデルが指定された場合。
  94    """
  95    if mode == "tb":
  96        return 1
  97    if mode in ("free", "nfe"):
  98        return 1 + 2 * dim  # G = 0, ±e_i
  99    raise ValueError("bad mode")
 100
 101
 102def determine_energy_range(E_samples: np.ndarray, E_range_user=None):
 103    """エネルギー軸レンジを決める
 104
 105    詳細説明:
 106    プロットやDOS/EF計算のためのエネルギー軸の最小値と最大値を決定します。
 107    ユーザーが `E_range_user` を指定した場合はそれを採用し、
 108    指定がない場合は、提供されたエネルギーサンプル `E_samples` の
 109    最小値と最大値に5%のパディングを加えて自動的に決定します。
 110
 111    :param E_samples: サンプルされたエネルギー値の配列。
 112    :type E_samples: np.ndarray
 113    :param E_range_user: ユーザー指定のエネルギー範囲 (Emin, Emax)。Noneの場合は自動決定。
 114    :type E_range_user: tuple[float, float], optional
 115    :returns: 決定されたエネルギー範囲 (Emin, Emax)。
 116    :rtype: tuple[float, float]
 117    """
 118    if E_range_user is not None:
 119        Emin, Emax = map(float, E_range_user)
 120        return Emin, Emax
 121
 122    Emin = float(np.min(E_samples))
 123    Emax = float(np.max(E_samples))
 124    pad = 0.05 * (Emax - Emin if Emax > Emin else 1.0)
 125    return Emin - pad, Emax + pad
 126
 127
 128# =============================================================================
 129# Band energies at k
 130# =============================================================================
 131
 132def get_band_energies_at_k_dim(k_vec_int: np.ndarray, model: str, dim: int, args) -> np.ndarray:
 133    """指定されたk点におけるバンドエネルギーを計算する
 134
 135    詳細説明:
 136    内部単位で与えられたkベクトル `k_vec_int` に対し、指定された `model`
 137    と `dim` に基づいてバンドエネルギー(固有値)を計算します。
 138    - 'tb': タイトバインディングモデル。
 139    - 'free': 自由電子モデル。Gベクトルに対応する運動エネルギーを直接計算しソート。
 140    - 'nfe': ほぼ自由電子モデル。Gベクトル間の周期ポテンシャル `args.v` を考慮した
 141             ハミルトニアン行列を構築し、固有値を計算。
 142
 143    :param k_vec_int: 内部単位 (2π/a normalized) のkベクトル (dim,)。
 144    :type k_vec_int: np.ndarray
 145    :param model: モデルの種類 ('tb', 'free', 'nfe')。
 146    :type model: str
 147    :param dim: 空間次元 (1, 2, or 3)。
 148    :type dim: int
 149    :param args: コマンドライン引数オブジェクト (特に 'nfe' モデルの `args.v` に必要)。
 150    :type args: argparse.Namespace
 151    :returns: そのk点での固有エネルギーの配列 (Nband,)。
 152    :rtype: np.ndarray
 153    :raises ValueError: 未知のモデルが指定された場合。
 154    """
 155    k_phys = k_vec_int * NORMALIZATION_UNIT
 156    G_VECS = build_g_vecs(dim)
 157
 158    if model == "tb":
 159        return np.array([tb_energy(k_phys, dim)], dtype=float)
 160
 161    if model == "free":
 162        # 自由電子モデル: 行列は作らず、各Gに対してエネルギーを直接計算
 163        energies = []
 164        for g in G_VECS:
 165            dk = k_phys - g
 166            # E = |k - G|^2 (定数係数は省略またはNORMALIZATION_UNITに内包)
 167            energies.append(np.sum(dk**2))
 168        return np.sort(np.array(energies, dtype=float))
 169
 170    if model == "nfe":
 171        # ほぼ自由電子モデル: 逆格子ベクトル間のカップリングを考慮
 172        num_G = len(G_VECS)
 173        H = np.zeros((num_G, num_G), dtype=float)
 174
 175        # 対角成分: |k-G|^2
 176        for i in range(num_G):
 177            dk = k_phys - G_VECS[i]
 178            H[i, i] = float(np.sum(dk**2))
 179
 180        # 非対角成分: 周期ポテンシャル V_G による散乱
 181        for i in range(num_G):
 182            for j in range(i + 1, num_G):
 183                H[i, j] = args.v
 184                H[j, i] = args.v
 185
 186        return np.linalg.eigvalsh(H)
 187
 188    raise ValueError(f"Unknown model: {model}")
 189
 190
 191def tb_energy(k_phys: np.ndarray, dim: int) -> float:
 192    """dim次元TB (t=1, onsite=0)
 193
 194    詳細説明:
 195    指定された空間次元 `dim` におけるタイトバインディング (TB) モデルのエネルギーを計算します。
 196    ホッピング積分 t=1、オンサイトエネルギーを0と仮定しています。
 197    - 1D: -cos(k_x a)
 198    - 2D: -(cos(k_x a) + cos(k_y a))
 199    - 3D: -(cos(k_x a) + cos(k_y a) + cos(k_z a))
 200    ここで k_phys は k*a の物理単位です。
 201
 202    :param k_phys: 物理単位のkベクトル。
 203    :type k_phys: np.ndarray
 204    :param dim: 空間次元 (1, 2, or 3)。
 205    :type dim: int
 206    :returns: 計算されたタイトバインディングエネルギー。
 207    :rtype: float
 208    :raises ValueError: `dim` が1, 2, 3以外の場合。
 209    """
 210    if dim == 1:
 211        return -float(np.cos(k_phys[0]))
 212    elif dim == 2:
 213        return -float(np.cos(k_phys[0]) + np.cos(k_phys[1]))
 214    elif dim == 3:
 215        return -float(np.cos(k_phys[0]) + np.cos(k_phys[1]) + np.cos(k_phys[2]))
 216    else:
 217        raise ValueError("dim must be 1, 2, or 3")
 218
 219
 220# =============================================================================
 221# BZ sampling -> DOS -> EF(nelec)
 222# =============================================================================
 223
 224def sample_energies_in_bz(args, dim: int) -> np.ndarray:
 225    """dim 次元 BZ を格子サンプルして、全バンドのEを1次元配列で返す(状態列)
 226
 227    詳細説明:
 228    指定された次元 `dim` のブリルアンゾーン内を `args.ef_res` で指定された
 229    解像度で均一にサンプリングし、各k点における全バンドのエネルギーを計算します。
 230    結果として得られるエネルギー値は1次元配列にフラット化されます。
 231    `--mode tb` の場合は特別な高速パスを使用します。
 232
 233    :param args: コマンドライン引数オブジェクト (ef_res, modeなどに必要)。
 234    :type args: argparse.Namespace
 235    :param dim: 空間次元 (1, 2, or 3)。
 236    :type dim: int
 237    :returns: 計算された全エネルギー値の1次元配列。
 238    :rtype: np.ndarray
 239    :raises ValueError: `dim` が1, 2, 3以外の場合。
 240    """
 241    n = args.ef_res
 242    k = np.linspace(-0.5, 0.5, n, endpoint=False)
 243
 244    if dim == 1:
 245        pts = k[:, None]
 246    elif dim == 2:
 247        KX, KY = np.meshgrid(k, k, indexing="ij")
 248        pts = np.stack([KX.ravel(), KY.ravel()], axis=1)
 249    elif dim == 3:
 250        KX, KY, KZ = np.meshgrid(k, k, k, indexing="ij")
 251        pts = np.stack([KX.ravel(), KY.ravel(), KZ.ravel()], axis=1)
 252    else:
 253        raise ValueError("dim must be 1,2,3")
 254
 255    if args.mode == "tb":
 256        k_phys = pts * NORMALIZATION_UNIT
 257        if dim == 1:
 258            E = -np.cos(k_phys[:, 0])
 259        elif dim == 2:
 260            E = -(np.cos(k_phys[:, 0]) + np.cos(k_phys[:, 1]))
 261        elif dim == 3:
 262            E = -(np.cos(k_phys[:, 0]) + np.cos(k_phys[:, 1]) + np.cos(k_phys[:, 2]))
 263        else:
 264            raise ValueError("dim must be 1,2,3")
 265        return np.asarray(E, dtype=float)
 266
 267    evals_all = []
 268    for kv in pts:
 269        evals_all.append(get_band_energies_at_k_dim(kv, args.mode, dim, args))
 270    return np.asarray(evals_all, dtype=float).ravel()
 271
 272
 273def compute_dos(E_samples: np.ndarray, nE: int, sigma: float, E_range, scale_states: float):
 274    """DOS をガウスブロードニングで作る
 275
 276    詳細説明:
 277    与えられたエネルギーサンプル `E_samples` から、ガウスブロードニング
 278    またはヒストグラム法を用いて状態密度 (DOS) を計算します。
 279    `sigma` が0以下の場合はヒストグラムとして計算され、それ以外の場合は
 280    ガウス関数を用いて平滑化されたDOSが計算されます。
 281    結果のDOSは `scale_states` に正規化され、DOSの積分が `scale_states` に
 282    ほぼ等しくなります。
 283
 284    :param E_samples: サンプルされたエネルギー値の配列。
 285    :type E_samples: np.ndarray
 286    :param nE: DOSを計算するためのエネルギーグリッドの点数。
 287    :type nE: int
 288    :param sigma: ガウスブロードニングの幅。0以下の場合はヒストグラムとして計算。
 289    :type sigma: float
 290    :param E_range: DOS計算を行うエネルギー範囲 (Emin, Emax)。
 291    :type E_range: tuple[float, float]
 292    :param scale_states: DOSをスケーリングする係数(通常はスピン縮退×バンド数)。
 293    :type scale_states: float
 294    :returns: エネルギーグリッドと対応するDOS値の配列。
 295    :rtype: tuple[np.ndarray, np.ndarray]
 296    """
 297    E_samples = np.asarray(E_samples, dtype=float)
 298    Emin, Emax = map(float, E_range)
 299    E_grid = np.linspace(Emin, Emax, nE)
 300
 301    if sigma is None or sigma <= 0:
 302        hist, edges = np.histogram(E_samples, bins=nE, range=(Emin, Emax), density=True)
 303        centers = 0.5 * (edges[:-1] + edges[1:])
 304        return centers, hist * scale_states
 305
 306    inv = 1.0 / (np.sqrt(2.0 * np.pi) * sigma)
 307    dos = np.zeros_like(E_grid, dtype=float)
 308
 309    block = 20000
 310    for i in range(0, len(E_samples), block):
 311        Ei = E_samples[i:i + block]
 312        x = (E_grid[:, None] - Ei[None, :]) / sigma
 313        dos += inv * np.exp(-0.5 * x * x).sum(axis=1)
 314
 315    dos /= len(E_samples)       # ∫dos dE ≈ 1
 316    dos *= scale_states         # ∫dos dE ≈ scale_states
 317    return E_grid, dos
 318
 319
 320def ef_from_dos(nelec: float, E_grid: np.ndarray, dos: np.ndarray, warn: bool = True) -> float:
 321    """DOS を積分して N(E)=∫DOS dE を作り、N(EF)=nelec となる EF を内挿で求める。
 322
 323    詳細説明:
 324    計算された状態密度 (DOS) をエネルギー `E_grid` に沿って積分し、
 325    累積状態数 `N(E)` を構築します。
 326    次に、指定された電子数 `nelec` に対応するフェルミエネルギー (EF) を
 327    `N(E)` から線形内挿で求めます。
 328    `nelec` がDOSの積分範囲外にある場合、EFは範囲の端にクランプされ、
 329    `warn` がTrueの場合は警告メッセージが表示されます。
 330
 331    :param nelec: 単位胞あたりの電子数。
 332    :type nelec: float
 333    :param E_grid: DOSのエネルギーグリッドの配列。
 334    :type E_grid: np.ndarray
 335    :param dos: 対応するDOS値の配列。
 336    :type dos: np.ndarray
 337    :param warn: `nelec` がDOS範囲外の場合に警告を表示するかどうか。
 338    :type warn: bool
 339    :returns: 計算されたフェルミエネルギー (EF)。
 340    :rtype: float
 341    """
 342    E_grid = np.asarray(E_grid, dtype=float)
 343    dos = np.asarray(dos, dtype=float)
 344
 345    dE = np.diff(E_grid)
 346    avg = 0.5 * (dos[:-1] + dos[1:])
 347    Ncum = np.concatenate([[0.0], np.cumsum(avg * dE)])
 348
 349    # clamp target
 350    nelec_clamped = float(np.clip(nelec, Ncum[0], Ncum[-1]))
 351
 352    if warn:
 353        if nelec < Ncum[0] - 1e-12:
 354            print(f"[WARN] nelec={nelec} is below integrated minimum ({Ncum[0]:.6g}). "
 355                  f"EF is clamped to Emin={E_grid[0]:.6g}.")
 356        if nelec > Ncum[-1] + 1e-12:
 357            print(f"[WARN] nelec={nelec} exceeds integrated states in DOS range ({Ncum[-1]:.6g}). "
 358                  f"EF is clamped to Emax={E_grid[-1]:.6g}. Consider widening --E_ef_range or omit it.")
 359
 360    EF = float(np.interp(nelec_clamped, Ncum, E_grid))
 361    return EF
 362
 363
 364# =============================================================================
 365# kF along 1D path and root solving
 366# =============================================================================
 367
 368def refine_root_bisection(func, a, b, tol=1e-10, max_iter=80):
 369    """関数 func の根を二分法で求める
 370
 371    詳細説明:
 372    与えられた関数 `func` の、区間 `[a, b]` 内での根を二分法アルゴリズムで探索します。
 373    `func(a)` と `func(b)` の符号が異なる場合にのみ根が存在すると仮定します。
 374    指定された許容誤差 `tol` または最大反復回数 `max_iter` に達するまで探索を続けます。
 375
 376    :param func: 根を探索する関数。`func(x)` は数値を返す必要があります。
 377    :type func: callable
 378    :param a: 探索区間の下限。
 379    :type a: float
 380    :param b: 探索区間の上限。
 381    :type b: float
 382    :param tol: 許容誤差。`abs(fmid)` または `abs(hi - lo)` がこの値以下になると探索を終了します。
 383    :type tol: float, optional
 384    :param max_iter: 最大反復回数。この回数を超えると探索を終了し、現在の区間の中間値を返します。
 385    :type max_iter: int, optional
 386    :returns: 根が見つかった場合はその値、`func(a)` と `func(b)` の符号が同じ場合はNone。
 387    :rtype: float or None
 388    """
 389    fa = func(a)
 390    fb = func(b)
 391    if fa == 0.0:
 392        return a
 393    if fb == 0.0:
 394        return b
 395    if fa * fb > 0:
 396        return None
 397
 398    lo, hi = a, b
 399    flo = fa
 400    for _ in range(max_iter):
 401        mid = 0.5 * (lo + hi)
 402        fmid = func(mid)
 403        if abs(fmid) < tol or abs(hi - lo) < tol:
 404            return mid
 405        if flo * fmid <= 0:
 406            hi = mid
 407        else:
 408            lo = mid
 409            flo = fmid
 410    return 0.5 * (lo + hi)
 411
 412
 413def find_kf_crossings_1d(k_grid_int, energies_1d, EF, dim, args):
 414    """1D直線パスでのkF点(直線プロット用)
 415
 416    詳細説明:
 417    1Dパスに沿ったバンドエネルギー `energies_1d` をフェルミエネルギー `EF` と比較し、
 418    `E(k) - EF = 0` となるkF点を二分法で探索します。
 419    見つかったkF点は第1ブリルアンゾーンに還元され、`args.kf_merge_tol` で指定された
 420    許容範囲内の近接するkF点はマージされます。
 421
 422    :param k_grid_int: 1Dパスのk点グリッド (内部単位)。
 423    :type k_grid_int: np.ndarray
 424    :param energies_1d: 各k点におけるバンドエネルギーの配列 (Nk, Nband)。
 425    :type energies_1d: np.ndarray
 426    :param EF: フェルミエネルギー。
 427    :type EF: float
 428    :param dim: 空間次元。バンドエネルギー計算のために使用されます。
 429    :type dim: int
 430    :param args: コマンドライン引数オブジェクト (kf_tol, kf_merge_tol, modeなどに必要)。
 431    :type args: argparse.Namespace
 432    :returns: (バンドインデックス, kF点) のタプルのリスト。
 433    :rtype: list[tuple[int, float]]
 434    """
 435    Nk, Nb = energies_1d.shape
 436    crossings = []
 437
 438    def get_Ek_band(kx_int, ib):
 439        k_vec = np.zeros(dim, dtype=float)
 440        k_vec[0] = kx_int
 441        return float(get_band_energies_at_k_dim(k_vec, args.mode, dim, args)[ib])
 442
 443    for ib in range(Nb):
 444        Ek = energies_1d[:, ib] - EF
 445        for i in range(Nk - 1):
 446            a = k_grid_int[i]
 447            b = k_grid_int[i + 1]
 448            fa = Ek[i]
 449            fb = Ek[i + 1]
 450            if fa == 0.0:
 451                crossings.append((ib, a))
 452                continue
 453            if fa * fb > 0:
 454                continue
 455            root = refine_root_bisection(lambda x: get_Ek_band(x, ib) - EF, a, b, tol=args.kf_tol)
 456            if root is not None:
 457                crossings.append((ib, root))
 458
 459    # reduce to 1st BZ and merge per band
 460    reduced = [(ib, reduce_to_first_bz(kx)) for (ib, kx) in crossings]
 461    reduced.sort(key=lambda x: (x[0], x[1]))
 462
 463    merged = []
 464    for ib, kx in reduced:
 465        if not merged:
 466            merged.append([ib, kx])
 467        else:
 468            ib0, k0 = merged[-1]
 469            if ib == ib0 and abs(kx - k0) < args.kf_merge_tol:
 470                merged[-1][1] = 0.5 * (k0 + kx)
 471            else:
 472                merged.append([ib, kx])
 473
 474    return [(ib, kx) for ib, kx in merged]
 475
 476
 477# =============================================================================
 478# Effective mass overlay
 479# =============================================================================
 480
 481def effective_mass_1d(k_int: np.ndarray, E: np.ndarray) -> np.ndarray:
 482    """1Dパス上で有効質量 m*(k) = 1 / (d^2E/d(ka)^2) を数値評価。
 483
 484    詳細説明:
 485    1次元パスに沿ったバンドエネルギー `E` とk点 `k_int` を用いて、
 486    電子の有効質量 `m*(k)` を数値的に計算します。
 487    有効質量は運動量 `ka` に対するエネルギーの二階微分 `d^2E/d(ka)^2` の逆数として定義されます。
 488    二階微分は中心差分法を用いて近似されます。k点数が少ない場合はNaNを返します。
 489
 490    :param k_int: 内部単位のk点配列。
 491    :type k_int: np.ndarray
 492    :param E: 各k点に対応するエネルギー配列。
 493    :type E: np.ndarray
 494    :returns: 計算された有効質量配列。計算できない点ではNaNが含まれます。
 495    :rtype: np.ndarray
 496    """
 497    k_int = np.asarray(k_int, dtype=float)
 498    E = np.asarray(E, dtype=float)
 499
 500    if len(k_int) < 3:
 501        return np.full_like(E, np.nan, dtype=float)
 502
 503    dk = k_int[1] - k_int[0]
 504    d2E = np.full_like(E, np.nan, dtype=float)
 505    d2E[1:-1] = (E[2:] - 2.0 * E[1:-1] + E[:-2]) / (dk * dk)
 506
 507    with np.errstate(divide="ignore", invalid="ignore"):
 508        mstar_kint = 1.0 / d2E
 509
 510    mstar_ka = mstar_kint * (NORMALIZATION_UNIT) ** 2 # 1/(d^2E/dk^2) * (dk/d(ka))^2 = 1/(d^2E/dk^2) * (1/ (2π/a)^2)^(-1) = 1/(d^2E/dk^2) * (2π/a)^2
 511    return mstar_ka
 512
 513
 514# =============================================================================
 515# Band + DOS plotter (Straight path only)
 516# =============================================================================
 517
 518def plot_band(args, dim: int, EF: float, E_samples: np.ndarray, E_range_plot, E_range_ef, Egrid_dos=None, dos=None):
 519    """1Dバンドプロット、またはkx方向に沿った直線カット用 (DOS/m*対応)
 520
 521    詳細説明:
 522    1次元のバンド構造、または高次元モデルのkx方向に沿った直線カットのバンド構造をプロットします。
 523    フェルミエネルギー (EF) を破線で表示し、必要に応じてkF点を縦点線で示します。
 524    `args.dos` がTrueの場合、DOSプロットをバンドプロットの隣に表示します。
 525    `args.mstar` がTrueの場合、有効質量 `m*(k)` をバンドプロットの右軸に重ねて表示します。
 526
 527    :param args: コマンドライン引数オブジェクト。
 528    :type args: argparse.Namespace
 529    :param dim: 空間次元 (1, 2, or 3)。
 530    :type dim: int
 531    :param EF: フェルミエネルギー。
 532    :type EF: float
 533    :param E_samples: DOS計算に使用されたエネルギーサンプルの配列。DOSプロットがない場合はNoneでも可。
 534    :type E_samples: np.ndarray or None
 535    :param E_range_plot: プロットのY軸(エネルギー)範囲 (Emin, Emax)。
 536    :type E_range_plot: tuple[float, float]
 537    :param E_range_ef: DOS/EF計算に使用されたエネルギー範囲 (Emin, Emax)。DOSプロットがない場合はNoneでも可。
 538    :type E_range_ef: tuple[float, float] or None
 539    :param Egrid_dos: DOSのエネルギーグリッドの配列。`args.dos` がFalseの場合はNone。
 540    :type Egrid_dos: np.ndarray or None
 541    :param dos: 対応するDOS値の配列。`args.dos` がFalseの場合はNone。
 542    :type dos: np.ndarray or None
 543    :returns: なし。
 544    :rtype: None
 545    """
 546    k_min, k_max = args.k_path_range
 547    k_resolution = args.res * 3
 548    k_range_int = np.linspace(k_min, k_max, k_resolution)
 549
 550    # 1D path energies
 551    all_energies = []
 552    for kx_int in k_range_int:
 553        k_vec = np.zeros(dim, dtype=float)
 554        k_vec[0] = kx_int
 555        all_energies.append(get_band_energies_at_k_dim(k_vec, args.mode, dim, args))
 556    all_energies = np.array(all_energies, dtype=float)  # (Nk, Nband)
 557
 558    # kF roots on this cut
 559    kf_list = find_kf_crossings_1d(k_range_int, all_energies, EF, dim, args)
 560
 561    Emin_plot, Emax_plot = E_range_plot
 562
 563    # DOS plot setup
 564    is_dos_plot = args.dos and (Egrid_dos is not None)
 565
 566    if is_dos_plot:
 567        Nband = nband_of_model(dim, args.mode)
 568        scale_states = float(args.spin) * Nband
 569        fig, (ax_band, ax_dos) = plt.subplots(1, 2, figsize=(11, 5), gridspec_kw={"wspace": 0.28})
 570    else:
 571        fig, ax_band = plt.subplots(figsize=(6.5, 5))
 572        ax_dos = None
 573
 574    # --- band ---
 575    is_reduced_zone = (k_min >= -0.5 and k_max <= 0.5)
 576
 577    for ib in range(all_energies.shape[1]):
 578        ax_band.plot(k_range_int, all_energies[:, ib], linewidth=1.5, alpha=0.9)
 579
 580    if is_reduced_zone:
 581        ax_band.set_xlim(-0.5, 0.5)
 582
 583    ax_band.set_ylim(Emin_plot, Emax_plot)
 584    ax_band.set_title(rf"{args.type} ({args.mode})   $E_F={EF:.6g}$")
 585    ax_band.axhline(y=EF, linestyle="--", linewidth=2, color="gray", label=r"$E_F$")
 586
 587    # kF vertical lines (reduced) + learned label
 588    if kf_list:
 589        for ib, kx_red in kf_list:
 590            if k_min <= kx_red <= k_max:
 591                ax_band.axvline(x=kx_red, linestyle=":", linewidth=1.2)
 592        ax_band.plot([], [], linestyle=":", color="black", label=r"$k_F$")
 593
 594    # effective mass overlay
 595    if args.mstar:
 596        ax_m = ax_band.twinx()
 597        for ib in range(all_energies.shape[1]):
 598            mstar = effective_mass_1d(k_range_int, all_energies[:, ib])
 599            mstar = np.clip(mstar, -5.0, 5.0)  # required range
 600            ax_m.plot(k_range_int, mstar, linestyle="--", linewidth=1.2, alpha=0.7, color="C1")
 601
 602        ax_m.set_ylabel(r"Effective mass $m^*(k)$")
 603        ax_m.set_ylim(-5.0, 5.0)
 604        ax_m.axhline(0.0, color="gray", linewidth=0.8, alpha=0.6)
 605        ax_band.plot([], [], "C1--", label=r"$m^*(k)$")
 606
 607    ax_band.set_xlabel(r"$k_x / (2\pi/a)$")
 608    ax_band.set_ylabel(r"Energy $E$")
 609    ax_band.grid(True, linestyle="--", alpha=0.5)
 610    ax_band.legend(loc="upper right")
 611
 612    # --- DOS ---
 613    if ax_dos is not None:
 614        ax_dos.plot(dos, Egrid_dos, linewidth=1.6)
 615        ax_dos.axhline(EF, linestyle="--", linewidth=2, color="gray", label=r"$E_F$")
 616
 617        # DOSの縦軸はバンド表示レンジに合わせる(見た目の統一)
 618        ax_dos.set_ylim(Emin_plot, Emax_plot)
 619
 620        ax_dos.set_xlabel(r"DOS  (states / cell / energy)")
 621        ax_dos.set_ylabel(r"Energy $E$")
 622        ax_dos.set_title(rf"DOS ($\int$DOS dE = {scale_states:.3g})")
 623        ax_dos.grid(True, linestyle="--", alpha=0.5)
 624        ax_dos.legend(loc="upper right")
 625
 626    plt.tight_layout()
 627    plt.show()
 628
 629
 630# =============================================================================
 631# High-symmetry K-Path Builder (for 2D/3D band plot)
 632# =============================================================================
 633
 634def build_k_path(points, labels, res_per_segment=40):
 635    """高対称点(points)をつなぐパスを生成する
 636
 637    詳細説明:
 638    複数の高対称点 (`points`) を連続して結び、バンド図プロット用のKパスを構築します。
 639    各パスセグメントは `res_per_segment` で指定された数のK点に分割されます。
 640    K点ベクトル、Kパスに沿った累積距離、高対称点の位置とラベルを返します。
 641
 642    :param points: 高対称点の座標のリスト。例: `[[0.0, 0.0], [0.5, 0.0]]`。
 643    :type points: list[list[float]]
 644    :param labels: 各高対称点に対応するラベルのリスト。例: `["Gamma", "X"]`。
 645    :type labels: list[str]
 646    :param res_per_segment: 各パスセグメントあたりのK点解像度。
 647    :type res_per_segment: int, optional
 648    :returns:
 649        - `k_vecs` (np.ndarray): パス上の全K点ベクトルの配列。
 650        - `x_dist` (np.ndarray): パス上のK点に対応する累積距離の配列。
 651        - `x_nodes` (list[float]): 高対称点に対応する累積距離のリスト。
 652        - `labels` (list[str]): 高対称点ラベルのリスト。
 653    :rtype: tuple[np.ndarray, np.ndarray, list[float], list[str]]
 654    """
 655    k_vecs_list = []
 656    x_dist_list = []
 657    x_nodes = []
 658    
 659    current_dist = 0.0
 660    x_nodes.append(current_dist)
 661    
 662    # 修正済み: 始点を配列化してリストに追加
 663    k_vecs_list.append(np.array(points[0]))
 664    x_dist_list.append(np.array([current_dist]))
 665
 666    for i in range(len(points) - 1):
 667        start = np.array(points[i])
 668        end = np.array(points[i+1])
 669        
 670        dist = np.linalg.norm(end - start)
 671        
 672        # 点を生成(始点は重複するので含めず、終点を含める)
 673        segment_k = np.linspace(start, end, res_per_segment + 1)[1:]
 674        segment_dist = np.linspace(current_dist, current_dist + dist, res_per_segment + 1)[1:]
 675        
 676        k_vecs_list.append(segment_k)
 677        x_dist_list.append(segment_dist)
 678        
 679        current_dist += dist
 680        x_nodes.append(current_dist)
 681
 682    k_vecs = np.vstack(k_vecs_list)
 683    x_dist = np.concatenate(x_dist_list)
 684    
 685    return k_vecs, x_dist, x_nodes, labels
 686
 687
 688def plot_band_with_path(args, dim: int, EF: float, E_range_plot):
 689    """高対称点パスに沿ってバンド図を描画する (2D/3D兼用)
 690
 691    詳細説明:
 692    2次元または3次元のバンド構造を高対称点パスに沿ってプロットします。
 693    指定された次元に応じて、対応する標準的な高対称点(例: 2D正方格子のΓ-X-M-Γパス、
 694    3D単純立方格子のΓ-X-M-Γ-R-Xパス)が定義され、そのパスに沿ってバンドエネルギーが計算されます。
 695    フェルミエネルギー (EF) は破線で表示され、高対称点の位置は縦線とラベルで示されます。
 696
 697    :param args: コマンドライン引数オブジェクト。
 698    :type args: argparse.Namespace
 699    :param dim: 空間次元 (2 or 3)。
 700    :type dim: int
 701    :param EF: フェルミエネルギー。
 702    :type EF: float
 703    :param E_range_plot: プロットのY軸(エネルギー)範囲 (Emin, Emax)。
 704    :type E_range_plot: tuple[float, float]
 705    :returns: なし。
 706    :rtype: None
 707    """
 708    
 709    # --- 1. 高対称点とパスの定義 ---
 710    if dim == 2:
 711        # 2D Square Lattice: Gamma -> X -> M -> Gamma
 712        G_pt = [0.0, 0.0]
 713        X_pt = [0.5, 0.0]
 714        M_pt = [0.5, 0.5]
 715        path_points = [G_pt, X_pt, M_pt, G_pt]
 716        path_labels = [r"$\Gamma$", r"$X$", r"$M$", r"$\Gamma$"]
 717        
 718    elif dim == 3:
 719        # 3D Simple Cubic: Gamma -> X -> M -> Gamma -> R -> X
 720        G_pt = [0.0, 0.0, 0.0]
 721        X_pt = [0.5, 0.0, 0.0]
 722        M_pt = [0.5, 0.5, 0.0]
 723        R_pt = [0.5, 0.5, 0.5]
 724        path_points = [G_pt, X_pt, M_pt, G_pt, R_pt, X_pt]
 725        path_labels = [r"$\Gamma$", r"$X$", r"$M$", r"$\Gamma$", r"$R$", r"$X$"]
 726    
 727    else:
 728        # 1D band plotはplot_bandを使うため、通常ここは呼ばれない
 729        print(f"[ERROR] Path plotting for dim={dim} is not supported.")
 730        return
 731
 732    # --- 2. パス生成 ---
 733    k_vecs, x_coords, node_pos, node_labs = build_k_path(path_points, path_labels, res_per_segment=args.res)
 734
 735    # --- 3. エネルギー計算 ---
 736    all_energies = []
 737    for kv in k_vecs:
 738        es = get_band_energies_at_k_dim(kv, args.mode, dim, args)
 739        all_energies.append(es)
 740        
 741    all_energies = np.array(all_energies, dtype=float) # (Nk, Nband)
 742
 743    # --- 4. プロット ---
 744    fig, ax = plt.subplots(figsize=(9, 6))
 745    
 746    # バンド線
 747    for ib in range(all_energies.shape[1]):
 748        ax.plot(x_coords, all_energies[:, ib], linewidth=1.5, alpha=0.9, color="C0")
 749
 750    # フェルミ準位
 751    ax.axhline(EF, linestyle="--", color="gray", linewidth=1.5, label=r"$E_F$")
 752
 753    # 高対称点の縦線とラベル
 754    for x_pos in node_pos:
 755        ax.axvline(x_pos, color="black", linestyle="-", linewidth=0.5)
 756    
 757    ax.set_xticks(node_pos)
 758    ax.set_xticklabels(node_labs, fontsize=12)
 759    
 760    # グラフ設定
 761    Emin, Emax = E_range_plot
 762    ax.set_ylim(Emin, Emax)
 763    ax.set_xlim(x_coords[0], x_coords[-1])
 764    ax.set_ylabel("Energy")
 765    ax.set_title(rf"{dim}D Band Structure ({args.mode})   $E_F={EF:.4g}$")
 766    ax.grid(True, axis="y", linestyle="--", alpha=0.5)
 767
 768    plt.tight_layout()
 769    plt.show()
 770
 771
 772# =============================================================================
 773# Fermi surface: build energy grid for FS plotting
 774# =============================================================================
 775
 776def build_energy_grid_for_fs(args, dim: int):
 777    """フェルミ面プロットのためにエネルギーグリッドを構築する
 778
 779    詳細説明:
 780    指定された次元 `dim` において、ブリルアンゾーン全体を格子状にサンプリングし、
 781    特定のバンドインデックス `args.fs_band` のエネルギー値を計算してグリッドを構築します。
 782    モデル ('tb', 'free', 'nfe') に応じてエネルギー計算が実行されます。
 783    このグリッドはフェルミ面の等値面または等高線プロットの基盤となります。
 784
 785    :param args: コマンドライン引数オブジェクト (res, fs_band, modeなどに必要)。
 786    :type args: argparse.Namespace
 787    :param dim: 空間次元 (1, 2, or 3)。
 788    :type dim: int
 789    :returns:
 790        - `k_range_int` (np.ndarray): 各軸のk点範囲 (内部単位)。
 791        - `E` (np.ndarray): 計算されたエネルギーグリッド。次元に応じた形状。
 792    :rtype: tuple[np.ndarray, np.ndarray]
 793    :raises ValueError: `dim` が1, 2, 3以外の場合。
 794    """
 795    N = args.res
 796    k_range_int = np.linspace(-0.5, 0.5, N)
 797    band_index = int(args.fs_band)
 798
 799    if dim == 1:
 800        KX = k_range_int
 801        E = np.zeros_like(KX, dtype=float)
 802        for i, kx in enumerate(KX):
 803            kv = np.array([kx], dtype=float)
 804            evals = get_band_energies_at_k_dim(kv, args.mode, 1, args)
 805            E[i] = evals[band_index]
 806        return k_range_int, E
 807
 808    if dim == 2:
 809        KX, KY = np.meshgrid(k_range_int, k_range_int, indexing="ij")
 810        E = np.zeros_like(KX, dtype=float)
 811
 812        if args.mode == "tb":
 813            E = -(np.cos(KX * NORMALIZATION_UNIT) + np.cos(KY * NORMALIZATION_UNIT))
 814            return k_range_int, E
 815
 816        if args.mode == "free":
 817            G = build_g_vecs(2)
 818            KX_phys = KX * NORMALIZATION_UNIT
 819            KY_phys = KY * NORMALIZATION_UNIT
 820            dists = []
 821            for g in G:
 822                d2 = (KX_phys - g[0]) ** 2 + (KY_phys - g[1]) ** 2
 823                dists.append(d2)
 824            all_bands = np.stack(dists, axis=0)
 825            all_bands.sort(axis=0)
 826            if band_index < len(G): E = all_bands[band_index]
 827            else: E = all_bands[-1]
 828            return k_range_int, E
 829
 830        if args.mode == "nfe":
 831            it = np.nditer([KX, KY], flags=["multi_index"])
 832            for kx, ky in it:
 833                kv = np.array([float(kx), float(ky)], dtype=float)
 834                evals = get_band_energies_at_k_dim(kv, "nfe", 2, args)
 835                E[it.multi_index] = evals[band_index]
 836            return k_range_int, E
 837
 838    if dim == 3:
 839        KX, KY, KZ = np.meshgrid(k_range_int, k_range_int, k_range_int, indexing="ij")
 840        E = np.zeros_like(KX, dtype=float)
 841
 842        if args.mode == "tb":
 843            E = -(np.cos(KX * NORMALIZATION_UNIT) + np.cos(KY * NORMALIZATION_UNIT) + np.cos(KZ * NORMALIZATION_UNIT))
 844            return k_range_int, E
 845
 846        if args.mode == "free":
 847            G = build_g_vecs(3)
 848            KX_phys = KX * NORMALIZATION_UNIT
 849            KY_phys = KY * NORMALIZATION_UNIT
 850            KZ_phys = KZ * NORMALIZATION_UNIT
 851            dists = []
 852            for g in G:
 853                d2 = (KX_phys - g[0]) ** 2 + (KY_phys - g[1]) ** 2 + (KZ_phys - g[2]) ** 2
 854                dists.append(d2)
 855            all_bands = np.stack(dists, axis=0)
 856            all_bands.sort(axis=0)
 857            if band_index < len(G): E = all_bands[band_index]
 858            else: E = all_bands[-1]
 859            return k_range_int, E
 860
 861        if args.mode == "nfe":
 862            it = np.nditer([KX, KY, KZ], flags=["multi_index"])
 863            for kx, ky, kz in it:
 864                kv = np.array([float(kx), float(ky), float(kz)], dtype=float)
 865                evals = get_band_energies_at_k_dim(kv, "nfe", 3, args)
 866                E[it.multi_index] = evals[band_index]
 867            return k_range_int, E
 868
 869    raise ValueError("dim must be 1,2,3")
 870
 871
 872# =============================================================================
 873# Fermi surface plotters (1dfs/2dfs/3dfs)
 874# =============================================================================
 875
 876def plot_1d_fermi_points(k_range_int: np.ndarray, E_line: np.ndarray, EF: float, args):
 877    """1次元のフェルミ点(kF)をプロットする
 878
 879    詳細説明:
 880    1次元のバンドエネルギー `E_line` とフェルミエネルギー `EF` を用いて、
 881    `E(k) = EF` となるkF点をプロットします。
 882    kF点はバンドとフェルミ準位の交点として見つけられ、第1ブリルアンゾーンに還元されて表示されます。
 883
 884    :param k_range_int: k点範囲の配列 (内部単位)。
 885    :type k_range_int: np.ndarray
 886    :param E_line: 各k点に対応するバンドエネルギーの配列。
 887    :type E_line: np.ndarray
 888    :param EF: フェルミエネルギー。
 889    :type EF: float
 890    :param args: コマンドライン引数オブジェクト (modeなどに必要)。
 891    :type args: argparse.Namespace
 892    :returns: なし。
 893    :rtype: None
 894    """
 895    fig, ax = plt.subplots(figsize=(7, 4))
 896    ax.plot(k_range_int, E_line, linewidth=2.0)
 897    ax.axhline(EF, linestyle="--", linewidth=2, label=r"$E_F$")
 898    roots = []
 899    Ek = E_line - EF
 900    for i in range(len(k_range_int) - 1):
 901        a, b = k_range_int[i], k_range_int[i + 1]
 902        fa, fb = Ek[i], Ek[i + 1]
 903        if fa == 0: roots.append(a)
 904        if fa * fb > 0: continue
 905        if fb != fa:
 906            r = a + (b - a) * (-fa) / (fb - fa)
 907            roots.append(r)
 908    roots_red = sorted({round(reduce_to_first_bz(r), 12) for r in roots})
 909    for r in roots_red: ax.axvline(r, linestyle=":", linewidth=1.5)
 910    if roots_red: ax.plot([], [], linestyle=":", label=r"$k_F$")
 911    ax.set_xlabel(r"$k / (2\pi/a)$")
 912    ax.set_ylabel(r"Energy $E$")
 913    ax.set_title(rf"1D Fermi points ({args.mode})   $E_F={EF:.6g}$")
 914    ax.grid(True, linestyle="--", alpha=0.5)
 915    ax.legend(loc="best")
 916    plt.tight_layout()
 917    plt.show()
 918
 919
 920def plot_2d_fermi_surface(E_grid_2d: np.ndarray, EF: float, args, k_range_int: np.ndarray):
 921    """2次元フェルミ面をコンタープロットとして表示する
 922
 923    詳細説明:
 924    2次元のエネルギーグリッド `E_grid_2d` を用いて、フェルミエネルギー `EF` の
 925    等高線を2Dのフェルミ面としてプロットします。
 926    `E <= EF` の領域は占有領域として塗りつぶされ、
 927    `args.nx_view` に基づいて複数ブリルアンゾーンにわたって表示されるため、
 928    周期的な性質が視覚化されます。
 929
 930    :param E_grid_2d: 2次元エネルギーグリッドの配列。
 931    :type E_grid_2d: np.ndarray
 932    :param EF: フェルミエネルギー。
 933    :type EF: float
 934    :param args: コマンドライン引数オブジェクト (nx_view, modeなどに必要)。
 935    :type args: argparse.Namespace
 936    :param k_range_int: k点範囲の配列 (内部単位)。グリッドの軸生成に使用。
 937    :type k_range_int: np.ndarray
 938    :returns: なし。
 939    :rtype: None
 940    """
 941    view_limit = args.nx_view
 942    N_TILES = int(np.ceil(2 * view_limit / 0.5))
 943    if N_TILES % 2 == 0: N_TILES += 1
 944    E_tile = np.tile(E_grid_2d, (N_TILES, N_TILES))
 945    k_min_total = -0.5 * N_TILES
 946    k_max_total = 0.5 * N_TILES
 947    k_range_ext = np.linspace(k_min_total, k_max_total, len(k_range_int) * N_TILES)
 948    fig, ax = plt.subplots(figsize=(8, 8))
 949    E_min = float(np.min(E_tile))
 950    ax.contourf(k_range_ext, k_range_ext, E_tile.T, levels=[E_min, EF], colors=["lightblue"], alpha=0.6, extend="neither")
 951    ax.contour(k_range_ext, k_range_ext, E_tile.T, levels=[EF], colors=["red"], linewidths=2)
 952    limit_int = 0.5
 953    ax.set_xlim(-view_limit, view_limit)
 954    ax.set_ylim(-view_limit, view_limit)
 955    ax.plot([limit_int, limit_int], [-view_limit, view_limit], "k--", lw=1)
 956    ax.plot([-limit_int, -limit_int], [-view_limit, view_limit], "k--", lw=1)
 957    ax.plot([-view_limit, view_limit], [limit_int, limit_int], "k--", lw=1)
 958    ax.plot([-view_limit, view_limit], [-limit_int, -limit_int], "k--", lw=1)
 959    ax.set_aspect("equal")
 960    ax.set_xlabel(r"$k_x / (2\pi/a)$")
 961    ax.set_ylabel(r"$k_y / (2\pi/a)$")
 962    ax.set_title(rf"2D Fermi Surface (Periodic, {args.mode})   $E_F={EF:.6g}$")
 963    ax.grid(True, linestyle="--", alpha=0.5)
 964    ax.fill_between([], [], color="lightblue", alpha=0.6, label=r"Occupied ($E \leq E_F$)")
 965    ax.legend(loc="lower left")
 966    plt.tight_layout()
 967    plt.show()
 968
 969
 970def plot_3d_fermi_surface(E_grid_3d: np.ndarray, EF: float, args, k_range_int: np.ndarray):
 971    """3次元フェルミ面を3Dサーフェスとしてプロットする
 972
 973    詳細説明:
 974    3次元のエネルギーグリッド `E_grid_3d` を用いて、フェルミエネルギー `EF` の
 975    等値面(フェルミ面)をMarching Cubesアルゴリズムで抽出し、3Dグラフィックとして表示します。
 976    scikit-imageライブラリが必要です。面には光源を考慮したシェーディングが適用され、
 977    `args.fs_alpha` で透明度を調整できます。
 978
 979    :param E_grid_3d: 3次元エネルギーグリッドの配列。
 980    :type E_grid_3d: np.ndarray
 981    :param EF: フェルミエネルギー。
 982    :type EF: float
 983    :param args: コマンドライン引数オブジェクト (mode, fs_alphaなどに必要)。
 984    :type args: argparse.Namespace
 985    :param k_range_int: k点範囲の配列 (内部単位)。グリッドの座標変換に使用。
 986    :type k_range_int: np.ndarray
 987    :returns: なし。
 988    :rtype: None
 989    """
 990    print("[INFO] Marching Cubes...")
 991    try:
 992        from skimage import measure
 993    except ImportError:
 994        print("\n[ERROR] 'scikit-image' is required for 3D Fermi surface plotting.")
 995        print("Please install it via: pip install scikit-image\n")
 996        return
 997
 998    try:
 999        verts, faces, normals, values = measure.marching_cubes(E_grid_3d, EF, spacing=(1, 1, 1))
1000        step_size = 1.0 / (len(k_range_int) - 1)
1001        verts = verts * step_size - 0.5  # [0..N] -> [-0.5..0.5]
1002    except ValueError:
1003        print("[ERROR] Isosurface could not be generated at this EF.")
1004        return
1005
1006    fig = plt.figure(figsize=(10, 8))
1007    ax = fig.add_subplot(111, projection="3d")
1008    ax.set_box_aspect([1, 1, 1])
1009
1010    tris = verts[faces]
1011    v1 = tris[:, 1] - tris[:, 0]
1012    v2 = tris[:, 2] - tris[:, 0]
1013    face_normals = np.cross(v1, v2)
1014    norm_mag = np.linalg.norm(face_normals, axis=1)
1015    norm_mag[norm_mag == 0] = 1.0
1016    face_normals = face_normals / norm_mag[:, np.newaxis]
1017
1018    light_A = np.array([1.0, 1.0, 1.0])
1019    light_A /= np.linalg.norm(light_A)
1020    base_color_A = "cyan" if args.mode != "tb" else "orange"
1021    rgb_A = np.array(mcolors.to_rgb(base_color_A))
1022    base_color_B = "navy" if args.mode != "tb" else "darkred"
1023    rgb_B = np.array(mcolors.to_rgb(base_color_B))
1024
1025    dots = np.dot(face_normals, light_A)
1026    weights = (dots + 1.0) / 2.0
1027    weights = np.clip(weights, 0.0, 1.0)[:, np.newaxis]
1028
1029    face_colors_rgb = weights * rgb_A + (1.0 - weights) * rgb_B
1030    alpha_val = args.fs_alpha
1031    face_colors_rgba = np.hstack((face_colors_rgb, np.full((len(faces), 1), alpha_val)))
1032
1033    mesh = Poly3DCollection(verts[faces], facecolors=face_colors_rgba)
1034    mesh.set_edgecolor("black")
1035    mesh.set_linewidth(0.05)
1036    ax.add_collection3d(mesh)
1037
1038    limit = 0.5
1039    ax.set_xlim(-limit, limit)
1040    ax.set_ylim(-limit, limit)
1041    ax.set_zlim(-limit, limit)
1042
1043    title_str = rf"3D Fermi Surface ({args.mode})   $E_F={EF:.6g}$" + "\n" + r"(Dual Light Shading)"
1044    ax.set_title(title_str)
1045    ax.set_xlabel(r"$k_x / (2\pi/a)$")
1046    ax.set_ylabel(r"$k_y / (2\pi/a)$")
1047    ax.set_zlabel(r"$k_z / (2\pi/a)$")
1048    plt.show()
1049
1050
1051# =============================================================================
1052# Dispatcher (main function)
1053# =============================================================================
1054
1055def dim_from_type(t: str) -> int:
1056    """プロットタイプ文字列から空間次元を決定する
1057
1058    詳細説明:
1059    コマンドライン引数で与えられたプロットタイプ文字列に基づいて、
1060    対応する空間次元(1, 2, または 3)を返します。
1061    例: "1dband" -> 1, "2dfs" -> 2。
1062
1063    :param t: プロットタイプを示す文字列 ('1dband', '2dband', '3dband', '1dfs', '2dfs', '3dfs')。
1064    :type t: str
1065    :returns: 対応する空間次元。
1066    :rtype: int
1067    :raises ValueError: 未知のプロットタイプが指定された場合。
1068    """
1069    if t in ("1dband", "1dfs"): return 1
1070    if t in ("2dband", "2dfs"): return 2
1071    if t in ("3dband", "3dfs"): return 3
1072    raise ValueError("bad --type")
1073
1074
1075def main():
1076    """スクリプトのメインエントリポイント
1077
1078    詳細説明:
1079    コマンドライン引数をパースし、指定された `type` (バンド図、DOS、フェルミ面) に基づいて
1080    適切な計算とプロットを実行します。
1081    フェルミエネルギー (EF) は、電子数 `nelec` と計算されたDOSから決定されるか、
1082    `--ef_fixed` オプションで固定値を指定することもできます。
1083    各プロットタイプ (`1dband`, `2dband`, `3dband`, `1dfs`, `2dfs`, `3dfs`) に応じて、
1084    対応するプロット関数を呼び出します。
1085    """
1086    parser = argparse.ArgumentParser(
1087        description="Toy band / DOS / EF(nelec from DOS integral) / kF / m*(k) and FS (1dfs/2dfs/3dfs) simulator."
1088    )
1089
1090    parser.add_argument("--type", type=str, default="3dband",
1091                        choices=["1dband", "2dband", "3dband", "1dfs", "2dfs", "3dfs"],
1092                        help="Output type: band plots or Fermi surface plots.")
1093
1094    parser.add_argument("--mode", type=str, default="tb",
1095                        choices=["free", "tb", "nfe"],
1096                        help="Model: free / tb / nfe (toy NFE Hamiltonian).")
1097
1098    # --- 新規追加オプション ---
1099    parser.add_argument("--ef_fixed", type=float, default=None,
1100                        help="Fixed Fermi energy (EF). If set, skips DOS calculation (nelec is ignored).")
1101    # --------------------------
1102
1103    # electron count
1104    parser.add_argument("--nelec", type=float, default=1.0,
1105                        help="Electrons per unit cell (including spin). EF is obtained from DOS integral (ignored if --ef_fixed is set).")
1106    parser.add_argument("--spin", type=float, default=2.0,
1107                        help="Spin degeneracy factor (default 2).")
1108
1109    # grid / resolution
1110    parser.add_argument("--res", type=int, default=40,
1111                        help="Resolution for FS grids / band path segments.")
1112    parser.add_argument("--ef_res", type=int, default=24,
1113                        help="BZ sampling resolution per axis for DOS/EF (cost grows as ef_res^dim; ignored if --ef_fixed is set).")
1114
1115    # NFE coupling
1116    parser.add_argument("--v", type=float, default=2.0,
1117                        help="Off-diagonal coupling strength in toy NFE Hamiltonian.")
1118
1119    # band path
1120    parser.add_argument("--k_path_range", type=float, nargs=2, default=[-0.5, 0.5],
1121                        metavar=("KMIN", "KMAX"),
1122                        help="kx range for 1D band/1D cut plots (internal unit).")
1123
1124    # plot Y-range (visual only)
1125    parser.add_argument("--E_range", type=float, nargs=2, default=None, metavar=("EMIN", "EMAX"),
1126                        help="Energy axis range for plotting (visual only).")
1127
1128    # EF/DOS calculation range (important!)
1129    parser.add_argument("--E_ef_range", type=float, nargs=2, default=None, metavar=("EMIN", "EMAX"),
1130                        help="Energy range used for DOS/EF calculation. Default: auto from sampled energies. (ignored if --ef_fixed is set).")
1131
1132    # DOS options
1133    parser.add_argument("--dos", action="store_true",
1134                        help="Also plot DOS (next to band). Requires DOS calculation (--ef_fixed must be None).")
1135    parser.add_argument("--dos_nE", type=int, default=800,
1136                        help="Energy grid points for DOS.")
1137    parser.add_argument("--dos_sigma", type=float, default=0.15,
1138                        help="Gaussian broadening sigma for DOS (<=0 => histogram).")
1139
1140    # kF roots controls
1141    parser.add_argument("--kf_tol", type=float, default=1e-10,
1142                        help="Tolerance for kF root finding.")
1143    parser.add_argument("--kf_merge_tol", type=float, default=1e-4,
1144                        help="Merge tolerance for reduced kF lines (internal unit).")
1145
1146    # effective mass overlay
1147    parser.add_argument("--mstar", action="store_true",
1148                        help="Overlay effective mass m*(k) on band plot (right axis), clipped to [-5,5].")
1149
1150    # FS options
1151    parser.add_argument("--nx_view", type=float, default=1.5,
1152                        help="2D FS periodic view range (internal unit).")
1153    parser.add_argument("--fs_band", type=int, default=0,
1154                        help="Band index used for FS isosurface/contour (default 0).")
1155    parser.add_argument("--fs_alpha", type=float, default=0.6,
1156                        help="Alpha for 3D FS surface.")
1157
1158    args = parser.parse_args()
1159    dim = dim_from_type(args.type)
1160
1161    # basic checks
1162    if args.spin <= 0: raise ValueError("--spin must be > 0")
1163    if args.nelec < 0: raise ValueError("--nelec must be >= 0")
1164
1165    # --- EF/DOS計算ロジック分岐 ---
1166    E_samples = None
1167    Egrid_dos = None
1168    dos = None
1169
1170    if args.ef_fixed is not None:
1171        # EFが固定値の場合
1172        EF = float(args.ef_fixed)
1173        print(f"[INFO] EF (Fixed) = {EF:.6g}")
1174        if args.dos:
1175            print("[WARN] --dos ignored because --ef_fixed is set (skipping time-consuming BZ sampling).")
1176            args.dos = False # DOSプロットを強制的にオフ
1177        
1178        # プロットレンジ決定のために、最小限のエネルギー範囲を計算する(ここでは省略し、プロットレンジはユーザー指定かデフォルトに依存)
1179        if args.E_range is None:
1180            print("[WARN] EF is fixed but --E_range is not set. Using default auto-range logic might be inaccurate.")
1181        
1182        E_range_ef = None # DOS計算はしないので不要
1183    
1184    else:
1185        # nelecからEFを計算する場合
1186        print(f"[INFO] Sampling energies in {dim}D BZ for DOS/EF (ef_res={args.ef_res}) ...")
1187        E_samples = sample_energies_in_bz(args, dim)
1188
1189        Nband = nband_of_model(dim, args.mode)
1190        scale_states = float(args.spin) * Nband
1191        if args.nelec > scale_states + 1e-12:
1192            print(f"[WARN] nelec={args.nelec} exceeds max capacity spin*Nband={scale_states:.6g} "
1193                  f"for this toy basis; EF will saturate near top of sampled states.")
1194
1195        E_range_ef = determine_energy_range(E_samples, args.E_ef_range)
1196
1197        # DOS and EF from DOS integral
1198        Egrid_dos, dos = compute_dos(
1199            E_samples,
1200            nE=args.dos_nE,
1201            sigma=args.dos_sigma,
1202            E_range=E_range_ef,
1203            scale_states=scale_states
1204        )
1205        EF = ef_from_dos(args.nelec, Egrid_dos, dos, warn=True)
1206        print(f"[INFO] EF(from DOS integral) = {EF:.6g}   (nelec={args.nelec}, spin={args.spin}, Nband={Nband})")
1207    
1208    # ------------------------------------
1209
1210    # プロット用レンジ決定(固定値/自動計算に関わらず)
1211    if args.E_range is not None:
1212        E_range_plot = (float(args.E_range[0]), float(args.E_range[1]))
1213    elif E_range_ef is not None:
1214        E_range_plot = E_range_ef
1215    else:
1216        # EF固定時にE_range指定がない場合のフォールバック(DOS計算はしないので、EF周辺を適当に表示)
1217        pad = 2.0
1218        E_range_plot = (EF - pad, EF + pad)
1219        
1220    # 5) dispatch
1221    if args.type in ("2dband", "3dband"):
1222        plot_band_with_path(args, dim, EF, E_range_plot)
1223        return 0
1224
1225    if args.type == "1dband":
1226        # 1Dは直線プロット(DOS情報が必要なら渡す)
1227        plot_band(args, dim, EF, E_samples, E_range_plot, E_range_ef, Egrid_dos, dos)
1228        return 0
1229
1230    # FS energy grids and plots use EF computed above
1231    if args.type == "1dfs":
1232        k_range_int, E_line = build_energy_grid_for_fs(args, 1)
1233        plot_1d_fermi_points(k_range_int, E_line, EF, args)
1234        return 0
1235
1236    if args.type == "2dfs":
1237        k_range_int, E2 = build_energy_grid_for_fs(args, 2)
1238        plot_2d_fermi_surface(E2, EF, args, k_range_int)
1239        return 0
1240
1241    if args.type == "3dfs":
1242        k_range_int, E3 = build_energy_grid_for_fs(args, 3)
1243        plot_3d_fermi_surface(E3, EF, args, k_range_int)
1244        return 0
1245
1246    raise ValueError("unknown --type")
1247
1248
1249if __name__ == "__main__":
1250    raise SystemExit(main())