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

analyze_symmetry.py をダウンロード

analyze_symmetry.py
analyze_symmetry.py
  1"""
  2配位子構造の点群対称性を解析し、対称適合線形結合 (SALC) を構築するスクリプト。
  3
  4詳細説明:
  5本スクリプトは、JSONファイルから配位子の構造データを読み込み、点群対称性を自動検出します。
  6検出された点群に基づいて、配位子の対称性を既約表現に分解し、
  7中心金属のd軌道とのハイブリダイゼーションの可否を評価します。
  8さらに、各既約表現に対応する対称適合線形結合 (SALC) を抽出し、その結果を出力します。
  9
 10関連リンク:
 11:doc:`salc_calculation_overview`
 12"""
 13import json
 14import argparse
 15import numpy as np
 16
 17import tkpg
 18from tkpg import core
 19
 20
 21def load_structure_json(path: str) -> dict:
 22    """
 23    指定されたJSONファイルから構造設定を読み込む。
 24
 25    詳細説明:
 26    ファイルパスを引数として受け取り、配位子の位置、d軌道タイプ、解析モード、
 27    許容誤差などの設定を辞書形式で抽出して返します。
 28    欠損しているキーにはデフォルト値が適用されます。
 29
 30    :param path: str: 構造設定を含むJSONファイルのパス。
 31    :returns: dict: 読み込まれた設定値を含む辞書。
 32        主なキーとして`ligands_pos` (numpy.ndarray), `d_orbital` (str), `mode` (str),
 33        `tol_match_geom` (float), `tol_match_D` (float), `salc_eig_tol` (float),
 34        `coeff_tol` (float), `print_coeffs` (bool), `print_salc_thr` (float),
 35        `max_salc_per_irrep` (int | None), `autodetect` (dict) などが含まれます。
 36    """
 37    with open(path, "r", encoding="utf-8") as f:
 38        obj = json.load(f)
 39
 40    ligands_pos = np.array(obj["ligands_pos"], float)
 41    d_orbital = obj.get("d_orbital", "d_xy")
 42    mode = obj.get("mode", "full")
 43
 44    tol = obj.get("tolerances", {})
 45    tol_match_geom = float(tol.get("tol_match_geom", 5e-3))
 46    tol_match_D = float(tol.get("tol_match_D", 5e-3))
 47
 48    salc_cfg = obj.get("salc", {})
 49    cfg = {
 50        "ligands_pos": ligands_pos,
 51        "d_orbital": d_orbital,
 52        "mode": mode,
 53        "tol_match_geom": tol_match_geom,
 54        "tol_match_D": tol_match_D,
 55        "salc_eig_tol": float(salc_cfg.get("salc_eig_tol", 1e-6)),
 56        "coeff_tol": float(salc_cfg.get("coeff_tol", 1e-6)),
 57        "print_coeffs": bool(salc_cfg.get("print_coeffs", True)),
 58        "print_salc_thr": float(salc_cfg.get("print_salc_thr", 1e-3)),
 59        "max_salc_per_irrep": salc_cfg.get("max_salc_per_irrep", None),
 60        "autodetect": obj.get("autodetect", {}),
 61    }
 62    return cfg
 63
 64
 65def center_positions(ligands_pos: np.ndarray) -> np.ndarray:
 66    """
 67    配位子の位置を重心が原点に来るように移動する。
 68
 69    詳細説明:
 70    与えられた配位子の三次元座標群の重心を計算し、各座標から重心を引くことで、
 71    重心を原点に移動した新しい座標群を生成します。
 72
 73    :param ligands_pos: numpy.ndarray: shapeが(N, 3)の配位子座標配列。
 74    :returns: numpy.ndarray: 重心が原点に移動された配位子座標配列。
 75    """
 76    return ligands_pos - np.mean(ligands_pos, axis=0, keepdims=True)
 77
 78
 79def choose_point_group(
 80    plugins: list[dict],
 81    ligands_pos: np.ndarray,
 82    tol_match_geom: float,
 83    autodetect_cfg: dict | None
 84) -> tuple[dict, list[dict]]:
 85    """
 86    複数の点群プラグインを評価し、最も適切な点群を自動選択する。
 87
 88    詳細説明:
 89    各プラグインが提供する`align_guess`と`symmetry_hit_rate`の結果を評価し、
 90    以下のルールに基づいて最適な点群を選択します:
 91    1. `min_rate_strict`設定で定義された閾値`strict_ok`を満たすプラグインを優先します。
 92    2. 残りの候補の中から、ヒット数 (`hits`) が最大であるものを選択します。
 93    3. 次に、群の位数 (`Gorder`) が最大であるものを選択します。
 94    4. 最後に、ヒット率 (`rate`) が最大であるものを選択します。
 95
 96    :param plugins: list[dict]: 点群プラグインのリスト。
 97        各プラグインは辞書形式で、`name` (str), `align_guess` (callable), `build_group` (callable),
 98        `d_irreps` (dict), `irreps` (callable), `irrep_dim` (callable), `irrep_char` (callable) などの情報を含む。
 99    :param ligands_pos: numpy.ndarray: shapeが(N, 3)の配位子座標配列。
100    :param tol_match_geom: float: 幾何学的な一致を判定するための許容誤差。
101    :param autodetect_cfg: dict | None: 自動検出設定を含む辞書。
102        `min_rate_strict` (dict) などのキーを含むことができる。
103    :returns: tuple[dict, list[dict]]:
104        - `dict`: 選択された最適な点群プラグインと評価結果を含む辞書。
105          キーとして`plugin` (dict), `ok_align` (bool), `R_align` (numpy.ndarray),
106          `pos_aligned` (numpy.ndarray), `hits` (int), `rate` (float),
107          `Gorder` (int), `strict_thr` (float), `strict_ok` (bool) を含む。
108        - `list[dict]`: 評価された全ての点群プラグインの結果リスト。
109          各要素は上記の`dict`と同じ構造を持つ。
110    """
111    autodetect_cfg = autodetect_cfg or {}
112    min_rate = autodetect_cfg.get("min_rate_strict", {})  # 例: {"Oh":0.70, "C4v":0.80}
113
114    scored = []
115    for p in plugins:
116        ok, R_align, pos_aligned = p["align_guess"](ligands_pos)
117        group = p["build_group"]()
118        Gorder = len(group)
119
120        if ok:
121#            hits, rate = core.symmetry_hits(group, pos_aligned, tol_match=tol_match_geom)
122            hits, rate = core.symmetry_hit_rate(group, pos_aligned, tol_match=tol_match_geom)
123        else:
124            hits, rate = 0, 0.0
125
126        # strict acceptance flag (rate based: backward compatible)
127        thr = float(min_rate.get(p["name"], 0.0))
128        strict_ok = (rate >= thr)
129
130        scored.append({
131            "plugin": p,
132            "ok_align": ok,
133            "R_align": R_align,
134            "pos_aligned": pos_aligned,
135            "hits": hits,
136            "rate": rate,
137            "Gorder": Gorder,
138            "strict_thr": thr,
139            "strict_ok": strict_ok,
140        })
141
142    # ---- selection rule ----
143    # 1) Prefer strict_ok True (if any)
144    strict = [s for s in scored if s["strict_ok"]]
145    pool = strict if strict else scored
146
147    # 2) Sort by hits desc, then |G| desc, then rate desc
148    pool.sort(key=lambda s: (s["hits"], s["Gorder"], s["rate"]), reverse=True)
149
150    best = pool[0]
151    return best, scored
152
153
154def main() -> None:
155    """
156    コマンドライン引数からJSONファイルを読み込み、配位子構造の点群対称性解析を実行する。
157
158    詳細説明:
159    1. コマンドライン引数(`--infile`)から構造設定JSONファイルのパスを読み込みます。
160       デフォルトは`structure.json`です。
161    2. JSONファイルから設定を読み込み、配位子の位置を重心が原点となるように正規化します。
162    3. `tkpg`プラグインを使用して最適な点群を自動検出します。
163    4. 検出された点群と設定されたd軌道に基づいて、配位子の既約表現への分解を行います。
164    5. 中心金属d軌道とのハイブリダイゼーションの対称性による可否を判定します。
165    6. 対称適合線形結合 (SALC) を抽出し、その係数を出力します。
166
167    :returns: None
168    """
169    ap = argparse.ArgumentParser()
170    ap.add_argument("--infile", default="structure.json",
171                    help="structure JSON file (default: structure.json)")
172    args = ap.parse_args()
173
174    cfg = load_structure_json(args.infile)
175
176    ligands_pos = center_positions(cfg["ligands_pos"])
177    d_orbital = cfg["d_orbital"]
178    mode = cfg["mode"]
179
180    tol_match_geom = cfg["tol_match_geom"]
181    tol_match_D = cfg["tol_match_D"]
182
183    plugins = tkpg.load_plugins()
184    if not plugins:
185        raise RuntimeError("No tkpg plugins found. Ensure tkpg/Oh.py, tkpg/C4v.py exist.")
186
187    best, scored = choose_point_group(
188        plugins, ligands_pos, tol_match_geom, cfg["autodetect"]
189    )
190
191    plugin = best["plugin"]
192    rate = best["rate"]
193    hits = best["hits"]
194    Gorder = best["Gorder"]
195    R_align = best["R_align"]
196    pos_aligned = best["pos_aligned"]
197
198    print(f"\n[Auto-detected point group]  {plugin['name']}   (hits={hits}/{Gorder}, hit_rate={rate:.3f})")
199
200    print("[Candidates]")
201    # show all candidates, sorted by the SAME selection key (but keep strict_ok shown)
202    scored_sorted = sorted(scored, key=lambda s: (s["strict_ok"], s["hits"], s["Gorder"], s["rate"]), reverse=True)
203    for s in scored_sorted:
204        p = s["plugin"]
205        flag = "STRICT" if s["strict_ok"] else "     "
206        thr = s["strict_thr"]
207        print(f"  {p['name']:>4s} : hits={s['hits']:>2d}/{s['Gorder']:<2d}  rate={s['rate']:.3f}  "
208              f"ok_align={s['ok_align']}  {flag} (thr={thr:.2f})")
209
210    # d_zx alias
211    d_orbital_use = "d_xz" if d_orbital == "d_zx" else d_orbital
212    dmap = plugin["d_irreps"]
213    if d_orbital_use not in dmap:
214        raise ValueError(f"d orbital '{d_orbital}' not defined for point group {plugin['name']}")
215
216    d_irrep = dmap[d_orbital_use]
217    print("\n[Central metal d orbital]")
218    print(f"  Orbital : {d_orbital}")
219    print(f"  Irrep   : {d_irrep}")
220
221    # Build basis
222    frames = core.compute_local_frames(pos_aligned)
223    basis  = core.build_ligand_basis(len(pos_aligned), frames, mode=mode)
224
225    group = plugin["build_group"]()
226    irreps = plugin["irreps"]()
227    irrep_dim = plugin["irrep_dim"]
228    irrep_char = plugin["irrep_char"]
229
230    # Decompose
231    Gamma = core.build_reducible_characters(group, pos_aligned, frames, basis, mode=mode, tol_match=tol_match_D)
232    coeffs = core.decompose_generic(Gamma, group, irreps, irrep_char)
233
234    if cfg["print_coeffs"]:
235        print(f"\n[Ligand reducible rep decomposition ({plugin['name']})]")
236        for ir in irreps:
237            if coeffs.get(ir, 0.0) > cfg["coeff_tol"]:
238                print(f"  {ir}: {coeffs[ir]:.3f}")
239
240    # Hybridization check
241    print("\n[Hybridization check]")
242    if coeffs.get(d_irrep, 0.0) > cfg["coeff_tol"]:
243        print(f"  ✔ Hybridization allowed by symmetry: ligand contains {d_irrep}")
244    else:
245        print(f"  ✘ Symmetry forbidden: ligand does NOT contain {d_irrep}")
246
247    # SALCs: show ALL irreps
248    print(f"\n[All ligand SALCs ({plugin['name']})]  mode={mode}")
249    any_printed = False
250    for ir in irreps:
251        mult = coeffs.get(ir, 0.0)
252        if mult <= cfg["coeff_tol"]:
253            continue
254
255        salcs = core.extract_salc(
256            ir, group,
257            irrep_dim, irrep_char,
258            pos_aligned, frames, basis, mode=mode,
259            tol_match=tol_match_D, tol=cfg["salc_eig_tol"]
260        )
261
262        if len(salcs) == 0:
263            print(f"\n[SALCs for {ir}]  (expected mult≈{mult:.3f}, but none extracted; try lowering salc_eig_tol)")
264            continue
265
266        any_printed = True
267        core.print_salc(
268            ir, salcs, basis,
269            thr=cfg["print_salc_thr"],
270            max_salc=cfg["max_salc_per_irrep"]
271        )
272
273    if not any_printed:
274        print("  (No SALCs extracted. Try increasing tol_match_D or lowering salc_eig_tol.)")
275
276
277if __name__ == "__main__":
278    main()