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

find_point_group.py をダウンロード

find_point_group.py
find_point_group.py
  1# find_point_group.py (refactored to use tkpointgroup.py)
  2#!/usr/bin/env python3
  3# -*- coding: utf-8 -*-
  4
  5"""
  6概要:
  7    XYZ 分子から点群を推定し、対称操作を直交化、整列、ラベル付けして表示します。
  8詳細説明:
  9    このスクリプトは、XYZ分子ファイルから点群を推定し、検出された対称操作を分類・ラベル付けするツールです。
 10    質量中心への平行移動、慣性主軸への整列などの前処理オプションも提供します。
 11    pymatgen を使用してXYZファイルを読み込み、PointGroupAnalyzer で点群記号と対称操作を特定します。
 12    対称操作の行列はSVDにより直交化され、tkpointgroup ライブラリの関数を用いて代表値にスナップされます。
 13    Cnv (n=3,4,6), C2v, Dn (n=3,4,6), T, Td, Th, O, Oh などの点群ファミリーに対応するカスタムラベリング関数により、
 14    各対称操作に詳細なSchoenflies記号のラベルが付与されます。
 15    Cnv群ではσvとσdの区別、C2v群ではσv(xz)とσv'(yz)の区別、Dn群ではCn^k(z)とC2'(x系)の分類が行われます。
 16    必要となるライブラリは pymatgen です。pip install pymatgen でインストールしてください。
 17    tkpointgroup.py はこのスクリプトと同じディレクトリ、またはPYTHONPATHに存在する必要があります。
 18関連リンク:
 19    find_point_group_usage
 20"""
 21
 22# コマンドライン実行例はDocstringではなく、Pythonのコメントとして保持します。
 23# python find_point_group.py --xyz h2o.xyz --center --align --dump-ops --write-aligned h2o_aligned.xyz
 24# python find_point_group.py --xyz nh3.xyz --center --align --symprec 2e-3 --angle-tol 8
 25# python find_point_group.py --xyz ch4.xyz --center --align --dump-ops
 26# python find_point_group.py --xyz h2o.xyz --center --align 
 27# python find_point_group.py --xyz SF6.xyz --center --align 
 28# python find_point_group.py --xyz C6H6.xyz --center --align 
 29# 推奨:オプションはそのまま(eigen-tol=1e-2, matrix-tol=1e-1)
 30
 31import argparse
 32import numpy as np
 33from pathlib import Path
 34from typing import List, Tuple, Optional
 35
 36from pymatgen.core import Molecule
 37from pymatgen.core.periodic_table import Element
 38from pymatgen.symmetry.analyzer import PointGroupAnalyzer
 39
 40import tkpointgroup as pg  # ← 追加:共通ライブラリ
 41
 42# ====== 数値ユーティリティ(pg を用いつつ、このスクリプト特有の処理を保持) ======
 43
 44def _orth_preserve_det(R: np.ndarray) -> np.ndarray:
 45    """
 46    概要:
 47        行列を直交化し、detの符号を保ちつつ代表的な値にスナップします。
 48    詳細説明:
 49        SVDを用いて行列Rを直交行列に変換し、元の行列式detの符号を維持するように調整します。
 50        最終的には tkpointgroup.snap_matrix を使用して要素を代表値にスナップします。
 51    引数:
 52        :param R: 直交化する行列。
 53        :type R: numpy.ndarray
 54    戻り値:
 55        :returns: 直交化され、代表値にスナップされた行列。
 56        :rtype: numpy.ndarray
 57    """
 58    U, _, Vt = np.linalg.svd(R)
 59    R_ = U @ Vt
 60    if np.sign(np.linalg.det(R_)) != np.sign(np.linalg.det(R)):
 61        U[:, -1] *= -1
 62        R_ = U @ Vt
 63    return pg.snap_matrix(R_)  # 最終スナップはライブラリを使用
 64
 65def _vec_to_unit(v: np.ndarray) -> np.ndarray:
 66    """
 67    概要:
 68        ベクトルを単位ベクトルに正規化します。
 69    詳細説明:
 70        ベクトルのノルムが非常に小さい場合は、ゼロベクトルを返します。
 71    引数:
 72        :param v: 正規化するベクトル。
 73        :type v: numpy.ndarray
 74    戻り値:
 75        :returns: 単位ベクトル。
 76        :rtype: numpy.ndarray
 77    """
 78    n = np.linalg.norm(v)
 79    return v / n if n > 1e-15 else v
 80
 81def rot_axis_angle(R: np.ndarray) -> Tuple[Optional[np.ndarray], float]:
 82    """
 83    概要:
 84        回転行列 R の回転軸(Noneのときは不定)と回転角(0..π)を返します。
 85    詳細説明:
 86        回転行列から固有値と固有ベクトルを計算し、固有値1に対応する固有ベクトルを回転軸とします。
 87        回転角はトレースから計算します。回転軸が不定の場合はNoneを返します。
 88    引数:
 89        :param R: 回転行列。
 90        :type R: numpy.ndarray
 91    戻り値:
 92        :returns: 回転軸(Noneの場合あり)と回転角(ラジアン、0からπの範囲)のタプル。
 93        :rtype: tuple
 94    """
 95    R = _orth_preserve_det(R)
 96    if np.allclose(R, np.eye(3), atol=1e-10):
 97        return None, 0.0
 98    vals, vecs = np.linalg.eig(R)
 99    idx = np.where(np.isclose(vals.real, 1.0, atol=1e-6))[0]
100    if idx.size == 0:
101        th = float(np.arccos(np.clip((np.trace(R)-1)/2, -1, 1)))
102        return None, th
103    axis = vecs[:, idx[0]].real
104    axis /= (np.linalg.norm(axis) + 1e-15)
105    th = float(np.arccos(np.clip((np.trace(R)-1)/2, -1, 1)))
106    return axis, th
107
108def mirror_normal(R: np.ndarray) -> Optional[np.ndarray]:
109    """
110    概要:
111        鏡映の法線ベクトルを返します(鏡映でない場合は None)。
112    詳細説明:
113        行列式が負でかつ二乗して単位行列となる行列を鏡映と判定します。
114        鏡映の場合、固有値-1に対応する固有ベクトルを法線ベクトルとします。
115    引数:
116        :param R: 検査する行列。
117        :type R: numpy.ndarray
118    戻り値:
119        :returns: 鏡映の法線ベクトル、またはNone。
120        :rtype: Optional[numpy.ndarray]
121    """
122    R = _orth_preserve_det(R)
123    if not (np.linalg.det(R) < 0 and np.allclose(R @ R, np.eye(3), atol=1e-6)):
124        return None
125    vals, vecs = np.linalg.eig(R)
126    idx = np.where(np.isclose(vals.real, -1.0, atol=1e-6))[0]
127    if idx.size == 0:
128        return None
129    n = vecs[:, idx[0]].real
130    n /= (np.linalg.norm(n) + 1e-15)
131    return n
132
133def pretty_matrix(R: np.ndarray) -> str:
134    """
135    概要:
136        行列を整形して文字列として返します。
137    詳細説明:
138        微小な要素を0として丸め、指定された書式で数値文字列を生成します。
139    引数:
140        :param R: 整形する行列。
141        :type R: numpy.ndarray
142    戻り値:
143        :returns: 整形された行列の文字列表現。
144        :rtype: str
145    """
146    Rt = np.where(np.abs(R) < 1e-10, 0.0, R)
147    Rt = np.round(Rt, 6)
148    return np.array2string(Rt, formatter={'float_kind': lambda x: f"{x:9.6f}"})
149
150# 代表方向(規格化済み)
151U100 = [_vec_to_unit(np.array([sx,0,0])) for sx in (1,-1)] + \
152       [_vec_to_unit(np.array([0,sy,0])) for sy in (1,-1)] + \
153       [_vec_to_unit(np.array([0,0,sz])) for sz in (1,-1)]
154
155U110: List[np.ndarray] = []
156for sx in (1,-1):
157    for sy in (1,-1):
158        U110 += [_vec_to_unit(np.array([sx, sy, 0]))]
159for sy in (1,-1):
160    for sz in (1,-1):
161        U110 += [_vec_to_unit(np.array([0, sy, sz]))]
162for sx in (1,-1):
163    for sz in (1,-1):
164        U110 += [_vec_to_unit(np.array([sx, 0, sz]))]
165
166U111 = [_vec_to_unit(np.array([sx, sy, sz]))
167        for sx in (1,-1) for sy in (1,-1) for sz in (1,-1)]
168
169def align_score(axis: np.ndarray, fam: List[np.ndarray]) -> float:
170    """
171    概要:
172        特定の方向群に対する軸の整列スコアを計算します。
173    詳細説明:
174        与えられた軸が方向群のいずれかのベクトルとどれだけ平行に近いかを示すスコアを計算します。
175        最大の内積の絶対値をスコアとします。
176    引数:
177        :param axis: 評価する軸。
178        :type axis: numpy.ndarray
179        :param fam: 比較する方向ベクトル群。
180        :type fam: List[numpy.ndarray]
181    戻り値:
182        :returns: 整列スコア (0.0から1.0)。
183        :rtype: float
184    """
185    axis = _vec_to_unit(axis)
186    return max(abs(float(np.dot(axis, d))) for d in fam)
187
188# ====== 幾何前処理(共通関数はこのスクリプトで保持) ======
189
190def center_of_mass(mol: Molecule) -> np.ndarray:
191    """
192    概要:
193        分子の質量中心を計算します。
194    引数:
195        :param mol: pymatgen.core.Molecule オブジェクト。
196        :type mol: pymatgen.core.Molecule
197    戻り値:
198        :returns: 分子の質量中心座標。
199        :rtype: numpy.ndarray
200    """
201    masses = np.array([float(Element(s.species_string).atomic_mass) for s in mol.sites])
202    coords = np.array([s.coords for s in mol.sites])
203    return (masses[:, None] * coords).sum(axis=0) / masses.sum()
204
205def inertia_align_matrix(mol: Molecule) -> np.ndarray:
206    """
207    概要:
208        分子の慣性主軸に整列するための回転行列を計算します。
209    詳細説明:
210        質量中心を計算し、それに基づいて慣性テンソルを構築します。
211        慣性テンソルの固有ベクトルが慣性主軸となり、これらを基底とする回転行列を生成します。
212        行列式が負になる場合は、主軸の一つを反転して右手系を保ちます。
213    引数:
214        :param mol: pymatgen.core.Molecule オブジェクト。
215        :type mol: pymatgen.core.Molecule
216    戻り値:
217        :returns: 慣性主軸に整列させるための回転行列。
218        :rtype: numpy.ndarray
219    """
220    masses = np.array([float(Element(s.species_string).atomic_mass) for s in mol.sites])
221    coords = np.array([s.coords for s in mol.sites])
222    com = center_of_mass(mol)
223    r = coords - com
224    I = np.zeros((3, 3))
225    for m, v in zip(masses, r):
226        x, y, z = v
227        I += m * np.array([[y*y+z*z, -x*y,    -x*z],
228                           [-x*y,     x*x+z*z, -y*z ],
229                           [-x*z,     -y*z,    x*x+y*y]])
230    _, vecs = np.linalg.eigh(I)
231    R = vecs.copy()
232    if np.linalg.det(R) < 0:
233        R[:, 0] *= -1
234    return R
235
236def apply_rigid_transform(mol: Molecule, R: np.ndarray, t: np.ndarray) -> Molecule:
237    """
238    概要:
239        分子に剛体変換(回転と平行移動)を適用します。
240    詳細説明:
241        分子の原子座標に平行移動 t を適用し、その後に回転行列 R を適用します。
242    引数:
243        :param mol: 変換を適用する pymatgen.core.Molecule オブジェクト。
244        :type mol: pymatgen.core.Molecule
245        :param R: 回転行列。
246        :type R: numpy.ndarray
247        :param t: 平行移動ベクトル。
248        :type t: numpy.ndarray
249    戻り値:
250        :returns: 変換が適用された新しい Molecule オブジェクト。
251        :rtype: pymatgen.core.Molecule
252    """
253    coords = np.array([s.coords for s in mol.sites])
254    new = (coords - t) @ R.T
255    species = [s.species for s in mol.sites]
256    return Molecule(species, new)
257
258def write_xyz(mol: Molecule, path: Path, comment: str = ""):
259    """
260    概要:
261        Molecule オブジェクトをXYZファイルとして書き込みます。
262    引数:
263        :param mol: 書き込む Molecule オブジェクト。
264        :type mol: pymatgen.core.Molecule
265        :param path: 出力ファイルのパス。
266        :type path: pathlib.Path
267        :param comment: XYZファイルの2行目に書き込むコメント。
268        :type comment: str
269    戻り値:
270        なし
271    """
272    with open(path, "w", encoding="utf-8") as f:
273        f.write(f"{len(mol)}\n{comment}\n")
274        for site in mol.sites:
275            el = site.species_string
276            x, y, z = site.coords
277            f.write(f"{el:2s} {x: .6f} {y: .6f} {z: .6f}\n")
278
279# ====== 点群推定 ======
280
281def guess_point_group(mol: Molecule, tolerance: float = 0.03, eigen_tolerance: float = 1e-2):
282    """
283    概要:
284        Molecule オブジェクトから点群とその対称操作を推定します。
285    詳細説明:
286        pymatgen.symmetry.analyzer.PointGroupAnalyzer を使用して点群記号と対称操作を特定します。
287        toleranceとeigen_toleranceは対称性の検出における許容誤差を制御します。
288    引数:
289        :param mol: 点群を推定する Molecule オブジェクト。
290        :type mol: pymatgen.core.Molecule
291        :param tolerance: 座標の許容差 (Å)。
292        :type tolerance: float
293        :param eigen_tolerance: 固有値縮退の許容差。
294        :type eigen_tolerance: float
295    戻り値:
296        :returns: Schoenflies記号と対称操作のリストのタプル。
297        :rtype: tuple
298    """
299    pga = PointGroupAnalyzer(mol, tolerance=tolerance, eigen_tolerance=eigen_tolerance)
300    sch = pga.sch_symbol  # e.g., Td, Oh, T, Th, C3v ...
301    ops = pga.get_symmetry_operations()
302    return sch, ops
303
304# ====== ラベル付け(各ファミリー) ======
305
306def label_c2v(R: np.ndarray) -> str:
307    """
308    概要:
309        C2v点群の対称操作にラベルを付けます。
310    詳細説明:
311        単位元、鏡映(σv(xz)またはσv'(yz))、またはC2回転(C2(z)またはC2)に分類します。
312    引数:
313        :param R: 対称操作の回転行列。
314        :type R: numpy.ndarray
315    戻り値:
316        :returns: ラベル文字列(例: E, σv(xz), C2)。
317        :rtype: str
318    """
319    R = _orth_preserve_det(R)
320    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
321    nrm = mirror_normal(R)
322    if nrm is not None:
323        # x軸・y軸により近い方で σv/σv' を判定
324        return "σv(xz)" if abs(nrm[1]) >= abs(nrm[0]) else "σv'(yz)"
325    axis, th = rot_axis_angle(R)
326    if axis is not None and abs(axis[2]) > 0.999 and np.isclose(th, np.pi, atol=1e-2):
327        return "C2(z)"
328    if np.isclose(th, np.pi, atol=1e-2): return "C2"
329    return "?"
330
331def label_cnv(n: int, R: np.ndarray) -> str:
332    """
333    概要:
334        Cnv点群(C3v, C4v, C6v)の対称操作にラベルを付けます。
335    詳細説明:
336        単位元、Cn^k(z)回転、または鏡映(σvまたはσd)に分類します。
337        nの値に基づいて回転軸と鏡映面を判定します。
338    引数:
339        :param n: Cn回転軸の次数(3, 4, 6)。
340        :type n: int
341        :param R: 対称操作の回転行列。
342        :type R: numpy.ndarray
343    戻り値:
344        :returns: ラベル文字列(例: E, C3^1(z), σv)。
345        :rtype: str
346    """
347    R = _orth_preserve_det(R)
348    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
349    nrm = mirror_normal(R)
350    if nrm is not None:
351        nxy = _vec_to_unit(np.array([nrm[0], nrm[1], 0.0]))
352        v_dirs = [np.array([ 1, 0, 0.0]), np.array([ 0, 1, 0.0]),
353                  np.array([-1, 0, 0.0]), np.array([ 0,-1, 0.0])]
354        d_dirs = [np.array([ 1, 1, 0.0]), np.array([ 1,-1, 0.0]),
355                  np.array([-1, 1, 0.0]), np.array([-1,-1, 0.0])]
356        vscore = max(abs(np.dot(nxy, _vec_to_unit(v))) for v in v_dirs)
357        dscore = max(abs(np.dot(nxy, _vec_to_unit(d))) for d in d_dirs)
358        return "σv" if vscore >= dscore else "σd"
359    det = np.linalg.det(R)
360    if det > 0:
361        axis, th = rot_axis_angle(R)
362        if axis is not None and abs(axis[2]) > 0.999:
363            k = int(round(th / (2*np.pi/n))) % n
364            if n in (4,6) and k == n//2: return "C2(z)"
365            if k == 0: return "E"
366            return f"C{n}^{k}(z)"
367        if np.isclose(th, np.pi, atol=1e-2): return "C2"
368    return "?"
369
370def label_dn(n: int, R: np.ndarray) -> str:
371    """
372    概要:
373        Dn点群(D3, D4, D6)の対称操作にラベルを付けます。
374    詳細説明:
375        単位元、Cn^k(z)回転、またはC2'(xy)回転に分類します。
376        z軸周りのCn回転とxy面内のC2回転を識別します。
377    引数:
378        :param n: Cn回転軸の次数(3, 4, 6)。
379        :type n: int
380        :param R: 対称操作の回転行列。
381        :type R: numpy.ndarray
382    戻り値:
383        :returns: ラベル文字列(例: E, C3^1(z), C2'(xy))。
384        :rtype: str
385    """
386    R = _orth_preserve_det(R)
387    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
388    det = np.linalg.det(R)
389    axis, th = rot_axis_angle(R) if det > 0 else (None, None)
390    if det > 0 and axis is not None and abs(axis[2]) > 0.999:
391        k = int(round(th / (2*np.pi/n))) % n
392        if n % 2 == 0 and k == n//2: return "C2(z)"
393        if k == 0: return "E"
394        return f"C{n}^{k}(z)"
395    if det > 0 and axis is not None and abs(axis[2]) < 1e-3 and np.isclose(th, np.pi, atol=1e-2):
396        return "C2'(xy)"
397    return "?"
398
399def label_t(R: np.ndarray) -> str:
400    """
401    概要:
402        T点群の対称操作にラベルを付けます。
403    詳細説明:
404        単位元、C3回転(<111>方向)、またはC2回転(<110>または<100>方向)に分類します。
405    引数:
406        :param R: 対称操作の回転行列。
407        :type R: numpy.ndarray
408    戻り値:
409        :returns: ラベル文字列(例: E, C3, C2)。
410        :rtype: str
411    """
412    R = _orth_preserve_det(R)
413    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
414    if np.linalg.det(R) < 0: return "?"
415    axis, th = rot_axis_angle(R)
416    if axis is None: return "?"
417    th_deg = np.degrees(th)
418    if (np.isclose(th_deg, 120.0, atol=1.0) or np.isclose(th_deg, 240.0, atol=1.0)) and align_score(axis, U111) > 0.99:
419        return "C3" if th_deg < 180.0 else "C3^2"
420    if np.isclose(th_deg, 180.0, atol=1.0) and (align_score(axis, U110) > 0.99 or align_score(axis, U100) > 0.99):
421        return "C2"
422    return "?"
423
424def label_th(R: np.ndarray) -> str:
425    """
426    概要:
427        Th点群の対称操作にラベルを付けます。
428    詳細説明:
429        単位元、反転中心 i、T点群の回転操作、S6回転、またはσh鏡映に分類します。
430        行列式が負の操作には逆行列の回転軸と角度を評価します。
431    引数:
432        :param R: 対称操作の回転行列。
433        :type R: numpy.ndarray
434    戻り値:
435        :returns: ラベル文字列(例: E, i, C3, S6, σh)。
436        :rtype: str
437    """
438    R = _orth_preserve_det(R)
439    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
440    if np.allclose(R, -np.eye(3), atol=1e-8): return "i"
441    det = np.linalg.det(R)
442    if det > 0:
443        return label_t(R)
444    Rt = -R
445    axis, th = rot_axis_angle(Rt)
446    if axis is None: return "?"
447    th_deg = np.degrees(th)
448    if align_score(axis, U111) > 0.99:
449        if np.isclose(th_deg, 60.0, atol=1.0):  return "S6"
450        if np.isclose(th_deg, 300.0, atol=1.0): return "S6^5"
451    if np.isclose(th_deg, 180.0, atol=1.0) and (align_score(axis, U110) > 0.99 or align_score(axis, U100) > 0.99):
452        return "σh"
453    return "?"
454
455def label_td(R: np.ndarray) -> str:
456    """
457    概要:
458        Td点群の対称操作にラベルを付けます。
459    詳細説明:
460        単位元、C3回転、C2回転、S4回転、またはσd鏡映に分類します。
461        各操作の回転軸や法線ベクトルが特定の結晶軸方向(<100>, <110>, <111>)にどれだけ一致するかを評価します。
462    引数:
463        :param R: 対称操作の回転行列。
464        :type R: numpy.ndarray
465    戻り値:
466        :returns: ラベル文字列(例: E, C3, S4, σd)。
467        :rtype: str
468    """
469    R = _orth_preserve_det(R)
470    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
471    det = np.linalg.det(R)
472    nrm = mirror_normal(R)
473    if nrm is not None:
474        return "σd" if align_score(nrm, U110) > 0.99 else "mirror"
475    if det > 0:
476        axis, th = rot_axis_angle(R)
477        if axis is None: return "?"
478        th_deg = np.degrees(th)
479        if (np.isclose(th_deg, 120.0, atol=1.0) or np.isclose(th_deg, 240.0, atol=1.0)) and align_score(axis, U111) > 0.99:
480            return "C3" if th_deg < 180.0 else "C3^2"
481        if np.isclose(th_deg, 180.0, atol=1.0) and (align_score(axis, U100) > 0.99 or align_score(axis, U110) > 0.99):
482            return "C2"
483        return "?"
484    Rt = -R
485    axis, th = rot_axis_angle(Rt)
486    if axis is not None and align_score(axis, U100) > 0.99:
487        th_deg = np.degrees(th)
488        if np.isclose(th_deg, 90.0, atol=1.0):  return "S4"
489        if np.isclose(th_deg, 270.0, atol=1.0): return "S4^3"
490    return "?"
491
492def label_o(R: np.ndarray) -> str:
493    """
494    概要:
495        O点群の対称操作にラベルを付けます。
496    詳細説明:
497        単位元、C3回転、C4回転、またはC2回転に分類します。
498        回転軸が<100>または<110>または<111>方向であるかを評価します。
499    引数:
500        :param R: 対称操作の回転行列。
501        :type R: numpy.ndarray
502    戻り値:
503        :returns: ラベル文字列(例: E, C3, C4, C2(⟨100⟩))。
504        :rtype: str
505    """
506    R = _orth_preserve_det(R)
507    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
508    if np.linalg.det(R) < 0: return "?"
509    axis, th = rot_axis_angle(R)
510    if axis is None: return "?"
511    th_deg = np.degrees(th)
512    if (np.isclose(th_deg, 120.0, atol=1.0) or np.isclose(th_deg, 240.0, atol=1.0)) and align_score(axis, U111) > 0.99:
513        return "C3" if th_deg < 180.0 else "C3^2"
514    if align_score(axis, U100) > 0.99:
515        if np.isclose(th_deg, 90.0, atol=1.0):  return "C4"
516        if np.isclose(th_deg, 270.0, atol=1.0): return "C4^3"
517        if np.isclose(th_deg, 180.0, atol=1.0): return "C2(⟨100⟩)"
518    if np.isclose(th_deg, 180.0, atol=1.0) and align_score(axis, U110) > 0.99:
519        return "C2(⟨110⟩)"
520    return "?"
521
522def label_oh(R: np.ndarray) -> str:
523    """
524    概要:
525        Oh点群の対称操作にラベルを付けます。
526    詳細説明:
527        単位元、反転中心 i、O点群の回転操作、S4回転、またはS6回転に分類します。
528        回転軸が<100>または<111>方向であるかを評価します。
529    引数:
530        :param R: 対称操作の回転行列。
531        :type R: numpy.ndarray
532    戻り値:
533        :returns: ラベル文字列(例: E, i, C4, S6)。
534        :rtype: str
535    """
536    R = _orth_preserve_det(R)
537    if np.allclose(R, np.eye(3), atol=1e-8): return "E"
538    if np.allclose(R, -np.eye(3), atol=1e-8): return "i"
539    det = np.linalg.det(R)
540    if det > 0:
541        return label_o(R)
542    Rt = -R
543    axis, th = rot_axis_angle(Rt)
544    if axis is None: return "?"
545    th_deg = np.degrees(th)
546    if align_score(axis, U100) > 0.99:
547        if np.isclose(th_deg, 90.0, atol=1.0):  return "S4"
548        if np.isclose(th_deg, 270.0, atol=1.0): return "S4^3"
549    if align_score(axis, U111) > 0.99:
550        if np.isclose(th_deg, 60.0, atol=1.0):   return "S6"
551        if np.isclose(th_deg, 300.0, atol=1.0):  return "S6^5"
552    return "?"
553
554# ====== メイン ======
555
556def main():
557    """
558    概要:
559        コマンドライン引数に基づいてXYZ分子の点群を推定し、対称操作をラベル付けして表示します。
560    詳細説明:
561        入力XYZファイルから分子を読み込み、オプションに応じて質量中心への平行移動や慣性主軸への整列を行います。
562        pymatgen.symmetry.analyzer.PointGroupAnalyzer を使用して点群を推定し、
563        tkpointgroup ユーティリティとカスタムのラベリング関数を使って対称操作を分類・表示します。
564        点群の推定結果と、選択された点群ファミリーに基づくラベル付け結果が標準出力されます。
565        --xyz で入力ファイル、--tolerance で座標許容差、--eigen-tol で固有値縮退の許容差を指定します。
566        --center で質量中心へ平行移動、--align で慣性主軸整列を行います。
567        --dump-ops を指定すると直交化前の行列も表示され、--write-aligned で整列後のXYZファイルを保存できます。
568        --assume オプションで分類ファミリーを明示的に指定することも可能です。
569    戻り値:
570        なし
571    """
572    ap = argparse.ArgumentParser(
573        description="XYZ 分子から点群を推定し、対称操作を Cnv/Dn/T/Td/Th/O/Oh 記号でラベル付け。"
574    )
575    ap.add_argument("--xyz", required=True, help="入力 XYZ")
576    ap.add_argument("--tolerance", type=float, default=0.03, help="座標許容差 [Å]")
577    ap.add_argument("--eigen-tol", type=float, default=1e-2, help="固有値縮退の許容(無次元)")
578    ap.add_argument("--center", action="store_true", help="質量中心へ平行移動")
579    ap.add_argument("--align", action="store_true", help="慣性主軸整列")
580    ap.add_argument("--dump-ops", action="store_true", help="直交化前の行列も表示")
581    ap.add_argument("--write-aligned", metavar="OUT_XYZ", help="整列後 XYZ を保存")
582    ap.add_argument("--assume",
583        choices=["C2v","C3v","C4v","C6v","D3","D4","D6","T","Th","Td","O","Oh"],
584        help="分類ファミリー(未指定なら推定記号に基づいて自動)")
585    args = ap.parse_args()
586
587    mol = Molecule.from_file(args.xyz)
588
589    if args.center or args.align:
590        com = center_of_mass(mol)
591        R0 = np.eye(3)
592        if args.align:
593            R0 = inertia_align_matrix(mol)
594        mol = apply_rigid_transform(mol, R0, com)
595
596    sch_raw, ops0 = guess_point_group(mol, tolerance=args.tolerance, eigen_tolerance=args.eigen_tolerance)
597    sch = pg.normalize_symbol(sch_raw)  # ← 記号を正規化
598    # Pymatgen の操作行列 → 直交化・代表スナップ
599    ops = [_orth_preserve_det(op.rotation_matrix) for op in ops0]
600
601    # ファミリー自動選択
602    assume = args.assume
603    if assume is None:
604        if sch in {"Td"}:          assume = "Td"
605        elif sch in {"Oh"}:        assume = "Oh"
606        elif sch in {"O"}:         assume = "O"
607        elif sch in {"T"}:         assume = "T"
608        elif sch in {"Th"}:        assume = "Th"
609        elif "v" in sch:           assume = sch if sch in {"C3v","C4v","C6v","C2v"} else "C2v"
610        elif sch.startswith("D"):  assume = sch if sch in {"D3","D4","D6"} else "D3"
611        else:
612            if "3" in sch:   assume = "C3v"
613            elif "4" in sch: assume = "C4v"
614            elif "6" in sch: assume = "C6v"
615            else:            assume = "C2v"
616
617    # ラベリング
618    labeled = []
619    for R in ops:
620        if assume == "Td":        name = label_td(R)
621        elif assume == "O":       name = label_o(R)
622        elif assume == "Oh":      name = label_oh(R)
623        elif assume == "T":       name = label_t(R)
624        elif assume == "Th":      name = label_th(R)
625        elif assume == "C2v":     name = label_c2v(R)
626        elif assume in ("C3v","C4v","C6v"):
627            n = {"C3v":3,"C4v":4,"C6v":6}[assume]
628            name = label_cnv(n, R)
629        elif assume in ("D3","D4","D6"):
630            n = int(assume[1:])
631            name = label_dn(n, R)
632        else:
633            name = "?"
634        # フォールバック(任意):どうしても判別できないときに基本分類を添える
635        if name == "?":
636            name = pg.classify_label(R)
637        labeled.append((name, R))
638
639    # 出力
640    print(f"File            : {args.xyz}")
641    print(f"Atoms           : {len(mol)}")
642    print(f"Guessed PG      : {sch_raw}")
643    print(f"Assume family   : {assume}")
644    print(f"Group order     : {len(ops)} operations")
645
646    if args.dump_ops:
647        print("\n--- Raw rotation matrices (from analyzer) ---")
648        for i, op in enumerate(ops0, 1):
649            print(f"[{i:02d}]")
650            print(np.array2string(op.rotation_matrix, formatter={'float_kind': lambda x: f"{x:8.5f}"}))
651
652    order_key = {
653        "E": 0, "i": 0.5,
654        # O/Oh
655        "C3": 1, "C3^2": 2, "C4": 1.5, "C4^3": 1.6, "C2(⟨100⟩)": 2.5, "C2(⟨110⟩)": 2.6,
656        "S4": 3.0, "S4^3": 3.1, "S6": 3.2, "S6^5": 3.3,
657        # Td/T/Th 汎用
658        "C2": 2.7, "σd": 4.0, "σh": 4.05, "mirror": 4.1,
659        # Cnv/Dn
660        "C2(z)": 1.2, "C3^1(z)": 1.3, "C3^2(z)": 1.4, "C4^1(z)": 1.5, "C4^2(z)": 1.6,
661        "σv": 4.2, "σd(Cnv)": 4.21, "σv(xz)": 4.22, "σv'(yz)": 4.23, "C2'(xy)": 5,
662        "?": 9
663    }
664    def sort_key(item):
665        name, _ = item
666        return (order_key.get(name, 8), name)
667
668    print("\n--- Canonicalized & labeled operations ---")
669    for i, (name, R) in enumerate(sorted(labeled, key=sort_key), 1):
670        print(f"[{i:02d}] {name}\n{pretty_matrix(R)}\n")
671
672    if args.write_aligned:
673        out = Path(args.write_aligned)
674        write_xyz(mol, out, comment=f"aligned; PG≈{sch_raw}; family={assume}")
675        print(f"Aligned XYZ written to: {out}")
676
677if __name__ == "__main__":
678    main()
679    input("\nPress ENTER to terminate>>\n")