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

tb_harrison.py をダウンロード

tb_harrison.py
tb_harrison.py
  1"""
  2Slater-Kosterタイトバインディングモデルを実装するモジュール。
  3
  4s, p, d軌道を持つ有限クラスター(分子)用の最小限のSlater-Kosterタイトバインディング(TB)モデルを提供します。
  5直交基底(S = I)と実対称ハミルトニアン(H)を仮定します。ハリスン則による積分パラメータのスケーリングに対応しています。
  6
  7:doc:`tb_harrison_usage`
  8"""
  9import numpy as np
 10from scipy.linalg import eigh
 11
 12
 13class SlaterKosterTB:
 14    """Slater-Kosterタイトバインディング法を実装するクラス。
 15
 16    s, p, d軌道を持つ有限クラスター(分子)用の最小限のSlater-Kosterタイトバインディング(TB)モデルを提供します。
 17    直交基底(S = I)と実対称ハミルトニアン(H)を仮定します。
 18
 19    ハリスン則によるスケーリングをサポートしており、`sk_params` の "harrison" エントリを通じて積分キーごとの指数を指定できます。
 20
 21    `harrison` パラメータの例:
 22    ::
 23
 24        "harrison": {
 25            "d0": 1.0,
 26            "power_default": 2.0,
 27            "power_map": {
 28                "sd_sigma": 3.5,
 29                "pd_sigma": 3.5,
 30                "pd_pi": 3.5,
 31                # "dd_sigma": 5.0, "dd_pi": 5.0, "dd_delta": 5.0,  # if you implement dd later
 32            },
 33            # オプション: キーごとのd0
 34            # "d0_map": {"pd_sigma": 2.0}
 35        }
 36
 37    `harrison` が省略された場合、スケーリングは行われません(スケールは1)。
 38    後方互換性のため、`{"d0":..., "power":...}` のみが与えられた場合、それがデフォルトとして使用されます。
 39    """
 40
 41    def __init__(self):
 42        """SlaterKosterTBクラスの新しいインスタンスを初期化します。
 43
 44        既知の軌道タイプ(s, p, d)とそれに対応する軌道のリストを設定し、
 45        原子リストとハミルトニアン行列を空の状態に初期化します。
 46        """
 47        self.orbitals = {
 48            "s": ["s"],
 49            "p": ["px", "py", "pz"],
 50            "d": ["dxy", "dyz", "dzx", "dx2-y2", "dz2"],
 51        }
 52        self.atoms = []
 53        self.hamiltonian = None
 54
 55    def add_atom(self, symbol, orb_type, x, y, z):
 56        """モデルに原子を追加します。
 57
 58        指定された記号、軌道タイプ、および空間座標を持つ原子を内部リストに追加します。
 59        軌道タイプが`orbitals`辞書に存在しない場合はエラーを発生させます。
 60
 61        :param symbol: str: 原子の元素記号。
 62        :param orb_type: str: 原子の軌道タイプ(例: "s", "p", "d")。
 63        :param x: float: 原子のX座標。
 64        :param y: float: 原子のY座標。
 65        :param z: float: 原子のZ座標。
 66        :returns: None
 67        :raises ValueError: 未知の軌道タイプが指定された場合。
 68        """
 69        if orb_type not in self.orbitals:
 70            raise ValueError(f"Unknown orb_type='{orb_type}'. Choose from {list(self.orbitals.keys())}")
 71        self.atoms.append(
 72            {
 73                "symbol": symbol,
 74                "orb_type": orb_type,
 75                "pos": np.array([x, y, z], dtype=float),
 76                "orbs": self.orbitals[orb_type],
 77            }
 78        )
 79
 80    @staticmethod
 81    def _need_param(p, key):
 82        """指定されたSKパラメータが存在するか確認し、その値を返します。
 83
 84        `sk_params` 辞書から `key` に対応する値を取得します。
 85        `key` が存在しない場合は `KeyError` を発生させます。
 86
 87        :param p: dict: Slater-Kosterパラメータを格納する辞書。
 88        :param key: str: 取得するパラメータのキー。
 89        :returns: float or int: 指定されたキーに対応するパラメータ値。
 90        :raises KeyError: 指定されたキーが `sk_params` に存在しない場合。
 91        """
 92        if key not in p:
 93            raise KeyError(f"Missing SK parameter '{key}' in sk_params.")
 94        return p[key]
 95
 96    @staticmethod
 97    def _harrison_scale(d, sk_params, key=None):
 98        """ハリソン則に基づいて、距離によるスケーリング因子を計算します。
 99
100        サイト間距離 `d` と `sk_params` の "harrison" セクションに定義されたルールに従って、
101        スケーリング因子を計算します。
102        `d0` (基準距離) と `power` (指数) は、グローバルまたはキーごとに指定できます。
103        `harrison` パラメータが指定されていない場合、または `d` が0以下の場合、スケーリングは行われず1.0を返します。
104        指数の選択順序は、1) `power_map[key]` -> 2) `power_default` -> 3) 従来の `power` です。
105
106        :param d: float: サイト間の距離。
107        :param sk_params: dict: Slater-Kosterパラメータを含む辞書。
108        :param key: str, optional: スケーリング対象の積分キー(例: "ss_sigma")。キー固有のスケーリング設定に使用されます。デフォルトはNone。
109        :returns: float: 計算されたハリスン則によるスケーリング因子。
110        """
111        hs = sk_params.get("harrison", None)
112        if hs is None:
113            return 1.0
114        if d <= 0:
115            return 1.0
116
117        # d0 (キーごとに上書き可能)
118        d0 = float(hs.get("d0", 1.0))
119        d0_map = hs.get("d0_map", None)
120        if key is not None and isinstance(d0_map, dict) and key in d0_map:
121            d0 = float(d0_map[key])
122
123        # 指数 (power) の選択順序:
124        # 1) power_map[key]
125        # 2) power_default
126        # 3) 従来の power
127        power_map = hs.get("power_map", None)
128        if key is not None and isinstance(power_map, dict) and key in power_map:
129            power = float(power_map[key])
130        elif "power_default" in hs:
131            power = float(hs["power_default"])
132        else:
133            power = float(hs.get("power", 2.0))  # 従来の形式
134
135        return (d0 / d) ** power
136
137    def _scaled_param(self, d, sk_params, key):
138        """ハリスン則でスケーリングされたSlater-Kosterパラメータを返します。
139
140        まず `_need_param` を使用して元のパラメータ値を取得し、
141        次に `_harrison_scale` を使用してスケーリング因子を計算します。
142        最後に、元のパラメータ値にスケーリング因子を乗じて結果を返します。
143
144        :param d: float: サイト間の距離。
145        :param sk_params: dict: Slater-Kosterパラメータを含む辞書。
146        :param key: str: スケーリング対象のSlater-Koster積分キー。
147        :returns: float: ハリスン則でスケーリングされたパラメータ値。
148        """
149        v = self._need_param(sk_params, key)
150        s = self._harrison_scale(d, sk_params, key=key)
151        return v * s
152
153    def _get_sk_elements(self, vec, orb1, orb2, p):
154        """2つの軌道間のSlater-Koster行列要素を計算します。
155
156        2つの原子間の相対位置ベクトル `vec` とそれぞれの軌道タイプ `orb1`, `orb2` を基に、
157        ハミルトニアンのオフサイト要素を計算します。
158        距離 `d`、方向余弦 `l, m, n` を用いて、s-s, s-p, s-d, p-p, p-d の各相互作用を処理します。
159        d-d 相互作用は現在未実装です。
160        軌道間の対称性に基づいて、特定の相互作用では符号が反転することがあります。
161
162        :param vec: numpy.ndarray: 原子間の相対位置ベクトル `(x, y, z)`。
163        :param orb1: str: 1番目の原子の軌道タイプ(例: "s", "px", "dxy")。
164        :param orb2: str: 2番目の原子の軌道タイプ(例: "s", "px", "dxy")。
165        :param p: dict: Slater-Kosterパラメータを含む辞書。
166        :returns: float: 計算されたSlater-Koster行列要素の値。
167        :raises NotImplementedError: d-d ホッピングまたはサポートされていない軌道ペアが指定された場合。
168        """
169        d = np.linalg.norm(vec)
170        if d < 1e-12:
171            return 0.0
172
173        l, m, n = vec / d
174
175        # quick type flags
176        is_s1, is_s2 = (orb1 == "s"), (orb2 == "s")
177        is_p1, is_p2 = orb1.startswith("p"), orb2.startswith("p")
178        is_d1, is_d2 = orb1.startswith("d"), orb2.startswith("d")
179
180        # 未実装チャネルでの高速失敗
181        if is_d1 and is_d2:
182            raise NotImplementedError("d-d hopping (dd_sigma, dd_pi, dd_delta) is not implemented.")
183
184        # --- 1. s-s ---
185        if is_s1 and is_s2:
186            return self._scaled_param(d, p, "ss_sigma")
187
188        # --- 2. s-p ---
189        if is_s1 and is_p2:
190            vsp = self._scaled_param(d, p, "sp_sigma")
191            if orb2 == "px":
192                return l * vsp
193            if orb2 == "py":
194                return m * vsp
195            if orb2 == "pz":
196                return n * vsp
197
198        if is_p1 and is_s2:
199            return -self._get_sk_elements(vec, orb2, orb1, p)
200
201        # --- 3. s-d ---
202        if is_s1 and is_d2:
203            vsd = self._scaled_param(d, p, "sd_sigma")
204            if orb2 == "dz2":
205                return (n**2 - 0.5 * (l**2 + m**2)) * vsd
206            if orb2 == "dx2-y2":
207                return 0.5 * np.sqrt(3.0) * (l**2 - m**2) * vsd
208            if orb2 == "dxy":
209                return np.sqrt(3.0) * l * m * vsd
210            if orb2 == "dyz":
211                return np.sqrt(3.0) * m * n * vsd
212            if orb2 == "dzx":
213                return np.sqrt(3.0) * l * n * vsd
214
215        if is_d1 and is_s2:
216            return self._get_sk_elements(vec, orb2, orb1, p)
217
218        # --- 4. p-p ---
219        if is_p1 and is_p2:
220            vpps = self._scaled_param(d, p, "pp_sigma")
221            vppp = self._scaled_param(d, p, "pp_pi")
222            coord = {"px": l, "py": m, "pz": n}
223            c1, c2 = coord[orb1], coord[orb2]
224            if orb1 == orb2:
225                return c1**2 * vpps + (1.0 - c1**2) * vppp
226            else:
227                return c1 * c2 * (vpps - vppp)
228
229        # --- 5. p-d ---
230        if is_p1 and is_d2:
231            vpds = self._scaled_param(d, p, "pd_sigma")
232            vpdp = self._scaled_param(d, p, "pd_pi")
233
234            if orb1 == "px":
235                if orb2 == "dxy":
236                    return m * (l**2 * vpds + (1 - 2 * l**2) * vpdp)
237                if orb2 == "dyz":
238                    return l * m * n * (vpds - 2 * vpdp)
239                if orb2 == "dzx":
240                    return n * (l**2 * vpds + (1 - 2 * l**2) * vpdp)
241                if orb2 == "dx2-y2":
242                    return 0.5 * l * (l**2 - m**2) * vpds + l * (1 - l**2 + m**2) * vpdp
243                if orb2 == "dz2":
244                    return l * (n**2 - 0.5 * (l**2 + m**2)) * vpds - np.sqrt(3.0) * l * n**2 * vpdp
245
246            if orb1 == "py":
247                if orb2 == "dxy":
248                    return l * (m**2 * vpds + (1 - 2 * m**2) * vpdp)
249                if orb2 == "dyz":
250                    return n * (m**2 * vpds + (1 - 2 * m**2) * vpdp)
251                if orb2 == "dzx":
252                    return l * m * n * (vpds - 2 * vpdp)
253                if orb2 == "dx2-y2":
254                    return 0.5 * m * (l**2 - m**2) * vpds - m * (1 + l**2 - m**2) * vpdp
255                if orb2 == "dz2":
256                    return m * (n**2 - 0.5 * (l**2 + m**2)) * vpds - np.sqrt(3.0) * m * n**2 * vpdp
257
258            if orb1 == "pz":
259                if orb2 == "dxy":
260                    return l * m * n * (vpds - 2 * vpdp)
261                if orb2 == "dyz":
262                    return m * (n**2 * vpds + (1 - 2 * n**2) * vpdp)
263                if orb2 == "dzx":
264                    return l * (n**2 * vpds + (1 - 2 * n**2) * vpdp)
265                if orb2 == "dx2-y2":
266                    return 0.5 * n * (l**2 - m**2) * vpds - n * (l**2 - m**2) * vpdp
267                if orb2 == "dz2":
268                    return n * (n**2 - 0.5 * (l**2 + m**2)) * vpds + np.sqrt(3.0) * n * (l**2 + m**2) * vpdp
269
270        if is_d1 and is_p2:
271            return -self._get_sk_elements(vec, orb2, orb1, p)
272
273        raise NotImplementedError(f"Unsupported orbital pair: ({orb1}, {orb2})")
274
275    def build_hamiltonian(self, sk_params, onsite_energies):
276        """システムのハミルトニアン行列を構築します。
277
278        追加された原子とその軌道情報、Slater-Kosterパラメータ、オンサイトエネルギーに基づいて、
279        全体のハミルトニアン行列を構築します。
280        まず、オンサイト項を対角要素に設定します。次に、異なる原子間のオフサイト項を
281        `_get_sk_elements` を使用して計算し、行列を埋めます。
282        ハミルトニアンは実対称行列として構築されます。
283
284        :param sk_params: dict: Slater-Kosterパラメータを含む辞書。
285        :param onsite_energies: dict: 原子の記号と軌道タイプごとのオンサイトエネルギーを含む辞書。
286        :returns: numpy.ndarray: 構築されたハミルトニアン行列。
287        """
288        total_orbs = sum(len(a["orbs"]) for a in self.atoms)
289        H = np.zeros((total_orbs, total_orbs), dtype=float)
290
291        # Onsite terms
292        idx = 0
293        for a in self.atoms:
294            n_orb = len(a["orbs"])
295            e_on = onsite_energies.get(a["symbol"], {}).get(a["orb_type"], 0.0)
296            for k in range(n_orb):
297                H[idx + k, idx + k] = e_on
298            idx += n_orb
299
300        # Offsite terms
301        idx_i = 0
302        for i, atom_i in enumerate(self.atoms):
303            idx_j = 0
304            for j, atom_j in enumerate(self.atoms):
305                if i < j:
306                    vec = atom_j["pos"] - atom_i["pos"]
307                    for io, orb_i in enumerate(atom_i["orbs"]):
308                        for jo, orb_j in enumerate(atom_j["orbs"]):
309                            val = self._get_sk_elements(vec, orb_i, orb_j, sk_params)
310                            H[idx_i + io, idx_j + jo] = val
311                            H[idx_j + jo, idx_i + io] = val
312                idx_j += len(atom_j["orbs"])
313            idx_i += len(atom_i["orbs"])
314
315        self.hamiltonian = H
316        return H
317
318    def orbital_labels(self):
319        """各原子の軌道に対応するラベルのリストを生成します。
320
321        各原子の位置と軌道タイプを組み合わせて、ハミルトニアン行列の各要素に対応する
322        識別のための文字列ラベルを生成します。
323        例: "Ba(0.000,0.000,0.000)_s", "N(0.000,0.000,0.800)_px"
324
325        :returns: list[str]: 各軌道に対応するラベルのリスト。
326        """
327        labels = []
328        for a in self.atoms:
329            x, y, z = a["pos"]
330            for o in a["orbs"]:
331                labels.append(f"{a['symbol']}({x:.3f},{y:.3f},{z:.3f})_{o}")
332        return labels
333
334
335def main():
336    """`SlaterKosterTB` クラスの使用例を示します。
337
338    `SlaterKosterTB` クラスを初期化し、複数の原子をモデルに追加します。
339    定義済みのSlater-Kosterパラメータとオンサイトエネルギーを使用してハミルトニアンを構築し、
340    `scipy.linalg.eigh` を使用してハミルトニアンを対角化し、固有値(軌道エネルギー)と固有ベクトルを計算します。
341    結果として得られたエネルギーと、それぞれの固有状態における支配的な軌道成分を表示します。
342    """
343    tb = SlaterKosterTB()
344
345    tb.add_atom("Ba", "d", 0, 0, 0)
346    tb.add_atom("N", "p", 0, 0, 0.8)
347    tb.add_atom("N", "p", 1, 0, 0)
348    tb.add_atom("N", "p", -1, 0, 0)
349    tb.add_atom("N", "p", 0, 1, 0)
350    tb.add_atom("N", "p", 0, -1, 0)
351
352    params = {
353        "ss_sigma": -1.40,
354        "sp_sigma": 1.84,
355        "pp_sigma": 3.24,
356        "pp_pi": -0.81,
357        "sd_sigma": -2.0,
358        "pd_sigma": -2.2,
359        "pd_pi": 1.0,
360        # キーに依存するハリスン則スケーリング:
361        "harrison": {
362            "d0": 1.0,
363            "power_default": 2.0,  # ss/sp/pp はデフォルトで 2.0
364            "power_map": {         # 選択された積分でのオーバーライド
365                "sd_sigma": 3.5,
366                "pd_sigma": 3.5,
367                "pd_pi": 3.5,
368            },
369        },
370    }
371
372    onsite = {"Ba": {"d": 0.0}, "N": {"p": -3.0}}
373
374    H = tb.build_hamiltonian(params, onsite)
375    energies, vectors = eigh(H)
376
377    idx_sort = energies.argsort()
378    evals = energies[idx_sort]
379    evecs = vectors[:, idx_sort]
380    labels = tb.orbital_labels()
381
382    print("Eigenvalues (Orbital Energies) [eV]:")
383    print(evals)
384
385    print("\nNo. |   Energy (eV) |  Dominant components")
386    print("-" * 72)
387    for i in range(len(evals)):
388        coeffs = evecs[:, i]
389        w = coeffs**2
390        top = np.argsort(w)[::-1][:5]
391#        desc = ", ".join([f"{labels[k]}:{coeffs[k]:+.2f}" for k in top])
392#        print(f"{i+1:2d} | {evals[i]:8.4f} | {desc}")
393        print(f"{i+1:2d} | {evals[i]:8.4f}")
394        for l, c in zip(labels, coeffs):
395            print(f"  {l}:{c:+.3f}")
396
397
398if __name__ == "__main__":
399    main()