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

tkpointgroup.py をダウンロード

tkpointgroup.py
tkpointgroup.py
   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3"""
   4tkpointgroup.py
   5  - Pure NumPy point-group utilities extracted from point_group_inf.py
   6
   7このモジュールは、NumPyを使用して点群に関連する様々なユーティリティを提供します。
   8主な機能として、Herman–Mauguin記号とSchoenflies記号の相互変換、群記号からの生成元と全要素の取得、
   9生成元からの閉包計算、点の軌道や独立代表点の取得、および回転、鏡映、反転などの基本的な幾何学操作が含まれます。
  10また、点群の指標表、クラスへの分類、振動モードの既約表現分解など、群論解析のための機能も提供します。
  11
  12:doc:`tkpointgroup_usage`
  13
  14提供機能(再利用向けAPI):
  15    * Herman–Mauguin (国際) と Schoenflies の相互変換
  16    * 群記号から generator と全要素(重複なし)を取得
  17    * generator から閉包を取り、全要素のラベルと行列を取得
  18    * 点の軌道・独立代表点(重複削除)を取得
  19    * 各種ユーティリティ(回転・鏡映・反転、スナップ、直交化など)
  20    * 点群の指標表の取得と操作の分類
  21    * 振動モードの既約表現分解
  22
  23依存: numpy
  24"""
  25
  26from __future__ import annotations
  27import math
  28import numpy as np
  29from typing import List, Tuple, Dict
  30
  31# ========= 基本ユーティリティ =========
  32
  33def _norm(v: np.ndarray) -> np.ndarray:
  34    """
  35    ベクトルを正規化します。
  36
  37    概要:
  38        入力ベクトルを正規化し、単位ベクトルを返します。
  39    詳細説明:
  40        ベクトルのL2ノルムが0の場合はValueErrorを発生させます。
  41    :param v: 正規化する入力ベクトル。
  42    :type v: numpy.ndarray
  43    :returns: 正規化された単位ベクトル。
  44    :rtype: numpy.ndarray
  45    :raises ValueError: 入力ベクトルの長さがゼロの場合。
  46    """
  47    v = np.asarray(v, dtype=float)
  48    n = np.linalg.norm(v)
  49    if n == 0:
  50        raise ValueError("Zero-length vector")
  51    return v / n
  52
  53def rot(axis: np.ndarray, angle_deg: float) -> np.ndarray:
  54    """
  55    指定された軸を中心とした回転行列を生成します。
  56
  57    概要:
  58        与えられた軸と角度に基づいて3x3の回転行列を計算します。
  59    詳細説明:
  60        ロドリゲスの回転公式を使用して回転行列を構築します。
  61        角度は度数で指定します。
  62    :param axis: 回転軸を示すベクトル。
  63    :type axis: numpy.ndarray
  64    :param angle_deg: 回転角度(度数)。
  65    :type angle_deg: float
  66    :returns: 3x3の回転行列。
  67    :rtype: numpy.ndarray
  68    """
  69    a = _norm(axis)
  70    th = math.radians(angle_deg)
  71    c, s = math.cos(th), math.sin(th)
  72    x, y, z = a
  73    R = np.array([
  74        [c + x*x*(1-c),     x*y*(1-c) - z*s, x*z*(1-c) + y*s],
  75        [y*x*(1-c) + z*s,   c + y*y*(1-c),   y*z*(1-c) - x*s],
  76        [z*x*(1-c) - y*s,   z*y*(1-c) + x*s, c + z*z*(1-c)]
  77    ], dtype=float)
  78    return R
  79
  80def mirror(normal: np.ndarray) -> np.ndarray:
  81    """
  82    指定された法線ベクトルを持つ平面での鏡映行列を生成します。
  83
  84    概要:
  85        与えられた法線ベクトルに垂直な平面に関する3x3の鏡映行列を計算します。
  86    詳細説明:
  87        鏡映行列は I - 2*(n @ n.T) で計算されます。ここで I は単位行列、n は正規化された法線ベクトルです。
  88    :param normal: 鏡映平面の法線ベクトル。
  89    :type normal: numpy.ndarray
  90    :returns: 3x3の鏡映行列。
  91    :rtype: numpy.ndarray
  92    """
  93    n = _norm(normal).reshape(3,1)
  94    return np.eye(3) - 2.0*(n @ n.T)
  95
  96def inversion() -> np.ndarray:
  97    """
  98    反転操作の行列を生成します。
  99
 100    概要:
 101        原点に対する反転操作を表す3x3の行列を返します。
 102    詳細説明:
 103        これは -I(マイナス単位行列)と等価です。
 104    :returns: 3x3の反転行列。
 105    :rtype: numpy.ndarray
 106    """
 107    return -np.eye(3)
 108
 109_SNAP_VALUES = [
 110    0.0, 0.5, -0.5,
 111    1.0/math.sqrt(2), -1.0/math.sqrt(2),
 112    math.sqrt(3)/2, -math.sqrt(3)/2,
 113    1.0, -1.0
 114]
 115def _closest(x: float) -> float:
 116    """
 117    与えられた浮動小数点数を、定義済みの代表値リストから最も近い値にスナップします。
 118
 119    概要:
 120        浮動小数点数を物理的に意味のある代表値(0, 0.5, 1/√2, √3/2, 1など)に丸めます。
 121    詳細説明:
 122        `_SNAP_VALUES` リスト内の値と入力値との絶対差が最小となる値を返します。
 123        スナップの閾値は5e-8です。
 124    :param x: スナップする浮動小数点数。
 125    :type x: float
 126    :returns: 最も近い代表値。
 127    :rtype: float
 128    """
 129    return min(_SNAP_VALUES, key=lambda v: abs(v - x))
 130
 131def snap_matrix(M: np.ndarray) -> np.ndarray:
 132    """
 133    行列要素を代表値へスナップし、直交性/ det=±1 を保証します。
 134
 135    概要:
 136        入力された3x3行列の各要素を事前に定義された代表的な浮動小数点数にスナップし、
 137        さらにその行列が直交性を満たし、かつ行列式が±1であることを保証します。
 138    詳細説明:
 139        まず、各要素を `_closest` 関数でスナップします。
 140        次に、スナップ後の行列が厳密に直交行列でない場合、SVD (特異値分解) を用いて
 141        最も近い直交行列に変換し、再度要素をスナップします。
 142        最後に、行列式が±1でない場合、SVDを用いて行列式を±1に修正します。
 143        これにより、数値誤差によって生じるわずかなずれを修正し、物理的に正しい対称操作行列を保証します。
 144    :param M: スナップ処理を行う3x3の浮動小数点行列。
 145    :type M: numpy.ndarray
 146    :returns: スナップされ、直交性および行列式の条件を満たす3x3行列。
 147    :rtype: numpy.ndarray
 148    """
 149    X = np.array([[ (_closest(v) if abs(_closest(v)-v) < 5e-8 else v)
 150                    for v in row ] for row in M ], dtype=float)
 151    if not np.allclose(X.T @ X, np.eye(3), atol=1e-6):
 152        U, _, Vt = np.linalg.svd(X)
 153        X = U @ Vt
 154        X = np.array([[ (_closest(v) if abs(_closest(v)-v) < 5e-8 else v)
 155                        for v in row ] for row in X ], dtype=float)
 156    d = np.linalg.det(X)
 157    if not (abs(abs(d)-1.0) < 1e-6):
 158        U, _, Vt = np.linalg.svd(X)
 159        sign = 1.0 if d >= 0 else -1.0
 160        X = U @ np.diag([1,1,sign]) @ Vt
 161    return X
 162
 163def mat_key(M: np.ndarray) -> tuple:
 164    """
 165    行列の一意な識別キーを生成します。
 166
 167    概要:
 168        NumPy配列を行列の同一性チェックに使用できるハッシュ可能なタプルに変換します。
 169    詳細説明:
 170        行列を1次元に平坦化し、各要素を小数点以下10桁で丸めてタプルに変換します。
 171        これにより、浮動小数点誤差に強いキーとして、辞書のキーや集合の要素として利用可能になります。
 172    :param M: キーを生成する入力行列。
 173    :type M: numpy.ndarray
 174    :returns: 行列の要素を丸めたタプルのキー。
 175    :rtype: tuple
 176    """
 177    return tuple(np.round(np.asarray(M, float).flatten(), 10))
 178
 179def unique_closure(generators: List[np.ndarray]) -> List[np.ndarray]:
 180    """
 181    生成元のリストから有限群の閉包を構成します(重複なし)。
 182
 183    概要:
 184        与えられた生成元行列のセットから、それらによって生成される全てのユニークな群要素(行列)を計算します。
 185    詳細説明:
 186        生成元と既存の群要素の積を計算し、新しいユニークな要素が見つかるたびにキューに追加していく
 187        幅優先探索(またはダイクストラ法に似た)アプローチを使用します。
 188        全ての要素は `snap_matrix` で正規化され、`mat_key` で重複がチェックされます。
 189        入力が空の場合、単位行列のみを含むリストを返します。
 190    :param generators: 群の生成元であるNumPy行列のリスト。
 191    :type generators: List[numpy.ndarray]
 192    :returns: 生成元から構成される点群の全てのユニークな要素行列のリスト。
 193    :rtype: List[numpy.ndarray]
 194    """
 195    gens = [snap_matrix(G) for G in generators]
 196    if not gens:
 197        return [np.eye(3)]
 198    els: Dict[tuple, np.ndarray] = {}
 199    queue: List[np.ndarray] = []
 200    def add(E):
 201        K = mat_key(E)
 202        if K not in els:
 203            els[K] = E
 204            queue.append(E)
 205    add(np.eye(3))
 206    for G in gens:
 207        add(G)
 208    while queue:
 209        A = queue.pop()
 210        for B in list(els.values()):
 211            add(snap_matrix(A @ B))
 212            add(snap_matrix(B @ A))
 213    return list(els.values())
 214
 215# 標準軸・面
 216ex, ey, ez = np.array([1,0,0.]), np.array([0,1,0.]), np.array([0,0,1.])
 217def vertical_plane(phi_deg: float) -> np.ndarray:
 218    """
 219    z軸に垂直な平面(xz平面またはyz平面を含む)に垂直な、xy平面内の法線ベクトルを生成します。
 220
 221    概要:
 222        与えられた角度を持つ垂直面の法線ベクトル(xy平面内)を計算します。
 223    詳細説明:
 224        phi_deg が 0 の場合、y軸に沿ったベクトル ([0, -1, 0]) となり、これはxz平面 (y=0) に垂直です。
 225        phi_deg が 90 の場合、x軸に沿ったベクトル ([-1, 0, 0]) となり、これはyz平面 (x=0) に垂直です。
 226    :param phi_deg: xy平面内での角度(度数)。
 227    :type phi_deg: float
 228    :returns: 垂直面の法線ベクトル。
 229    :rtype: numpy.ndarray
 230    """
 231    φ = math.radians(phi_deg)
 232    return np.array([math.sin(φ), -math.cos(φ), 0.0])
 233
 234def diagonal_plane(phi_deg: float) -> np.ndarray:
 235    """
 236    対角面(z軸に対して45度の角度を持つ面)の法線ベクトルを生成します。
 237
 238    概要:
 239        指定された角度に対する対角面の法線ベクトルを計算します。
 240    詳細説明:
 241        `vertical_plane` 関数を呼び出し、角度に90度を加えることで、
 242        xy平面内で45度の傾きを持つ(例: (1,1,0) や (1,-1,0) 方向を法線に持つ)面を表現します。
 243    :param phi_deg: xy平面内での角度(度数)。
 244    :type phi_deg: float
 245    :returns: 対角面の法線ベクトル。
 246    :rtype: numpy.ndarray
 247    """
 248    return vertical_plane(phi_deg + 90.0)
 249
 250# ========= 群ビルダー =========
 251
 252def build_Cn(n: int) -> List[np.ndarray]:
 253    """
 254    n回軸 (Cn) 点群の全ての対称操作行列を構築します。
 255
 256    概要:
 257        n回回転軸のみから構成される点群の全要素行列を返します。
 258    詳細説明:
 259        z軸周りの360/n度の回転を生成元とし、それらの閉包を計算します。
 260    :param n: 回転軸の次数。
 261    :type n: int
 262    :returns: Cn点群の対称操作行列のリスト。
 263    :rtype: List[numpy.ndarray]
 264    """
 265    return unique_closure([rot(ez, 360.0/n)])
 266
 267def build_Cnv(n: int) -> List[np.ndarray]:
 268    """
 269    Cnv点群の全ての対称操作行列を構築します。
 270
 271    概要:
 272        n回回転軸とそれに含まれる複数の鉛直鏡映面から構成される点群の全要素行列を返します。
 273    詳細説明:
 274        z軸周りの360/n度の回転と、z軸を含む垂直面(phi=0)での鏡映を生成元とし、それらの閉包を計算します。
 275    :param n: 回転軸の次数。
 276    :type n: int
 277    :returns: Cnv点群の対称操作行列のリスト。
 278    :rtype: List[numpy.ndarray]
 279    """
 280    return unique_closure([rot(ez,360.0/n), mirror(vertical_plane(0.0))])
 281
 282def build_Cnh(n: int) -> List[np.ndarray]:
 283    """
 284    Cnh点群の全ての対称操作行列を構築します。
 285
 286    概要:
 287        n回回転軸とそれに垂直な水平鏡映面から構成される点群の全要素行列を返します。
 288    詳細説明:
 289        z軸周りの360/n度の回転と、z軸に垂直な水平面(ez)での鏡映を生成元とし、それらの閉包を計算します。
 290    :param n: 回転軸の次数。
 291    :type n: int
 292    :returns: Cnh点群の対称操作行列のリスト。
 293    :rtype: List[numpy.ndarray]
 294    """
 295    return unique_closure([rot(ez,360.0/n), mirror(ez)])
 296
 297def build_Sn(n: int) -> List[np.ndarray]:
 298    """
 299    Sn点群の全ての対称操作行列を構築します。
 300
 301    概要:
 302        n回回映軸 (improper rotation axis) から構成される点群の全要素行列を返します。
 303    詳細説明:
 304        z軸に垂直な鏡映とz軸周りの360/n度の回転の積(回映操作)を生成元とし、その閉包を計算します。
 305    :param n: 回映軸の次数。
 306    :type n: int
 307    :returns: Sn点群の対称操作行列のリスト。
 308    :rtype: List[numpy.ndarray]
 309    """
 310    return unique_closure([ mirror(ez) @ rot(ez,360.0/n) ])
 311
 312def build_Dn(n: int) -> List[np.ndarray]:
 313    """
 314    Dn点群の全ての対称操作行列を構築します。
 315
 316    概要:
 317        n回回転軸とそれに垂直な2回回転軸(n個)から構成される点群の全要素行列を返します。
 318    詳細説明:
 319        z軸周りの360/n度の回転と、x軸周りの180度回転を生成元とし、それらの閉包を計算します。
 320    :param n: 主回転軸の次数。
 321    :type n: int
 322    :returns: Dn点群の対称操作行列のリスト。
 323    :rtype: List[numpy.ndarray]
 324    """
 325    return unique_closure([rot(ez,360.0/n), rot(ex,180.0)])
 326
 327def build_Dnh(n: int) -> List[np.ndarray]:
 328    """
 329    Dnh点群の全ての対称操作行列を構築します。
 330
 331    概要:
 332        Dn点群に水平鏡映面が加わった点群の全要素行列を返します。
 333    詳細説明:
 334        z軸周りの360/n度の回転、x軸周りの180度回転、およびz軸に垂直な水平鏡映を生成元とし、それらの閉包を計算します。
 335    :param n: 主回転軸の次数。
 336    :type n: int
 337    :returns: Dnh点群の対称操作行列のリスト。
 338    :rtype: List[numpy.ndarray]
 339    """
 340    return unique_closure([rot(ez,360.0/n), rot(ex,180.0), mirror(ez)])
 341
 342def build_Dnd(n: int) -> List[np.ndarray]:
 343    """
 344    Dnd点群の全ての対称操作行列を構築します。
 345
 346    概要:
 347        Dn点群に、2回回転軸の間の対角鏡映面が加わった点群の全要素行列を返します。
 348    詳細説明:
 349        z軸周りの360/n度の回転、x軸周りの180度回転、および2回回転軸の間の対角鏡映面(phi=0)を生成元とし、それらの閉包を計算します。
 350    :param n: 主回転軸の次数。
 351    :type n: int
 352    :returns: Dnd点群の対称操作行列のリスト。
 353    :rtype: List[numpy.ndarray]
 354    """
 355    return unique_closure([rot(ez,360.0/n), rot(ex,180.0), mirror(diagonal_plane(0.0))])
 356
 357def build_T() -> List[np.ndarray]:
 358    """
 359    T (四面体) 点群の全ての対称操作行列を構築します。
 360
 361    概要:
 362        正四面体の対称性を持つ点群の全要素行列を返します。
 363    詳細説明:
 364        [1,1,1]軸周りの120度回転と、x軸周りの180度回転を生成元とし、それらの閉包を計算します。
 365    :returns: T点群の対称操作行列のリスト。
 366    :rtype: List[numpy.ndarray]
 367    """
 368    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0) ])
 369
 370def build_Th() -> List[np.ndarray]:
 371    """
 372    Th (全四面体) 点群の全ての対称操作行列を構築します。
 373
 374    概要:
 375        T点群に反転中心が加わった点群の全要素行列を返します。
 376    詳細説明:
 377        T点群の生成元に反転操作を追加し、それらの閉包を計算します。
 378    :returns: Th点群の対称操作行列のリスト。
 379    :rtype: List[numpy.ndarray]
 380    """
 381    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0), inversion() ])
 382
 383def build_Td() -> List[np.ndarray]:
 384    """
 385    Td (正四面体) 点群の全ての対称操作行列を構築します。
 386
 387    概要:
 388        正四面体の対称性を持つ点群の全要素行列を返します。
 389    詳細説明:
 390        [1,1,1]軸周りの120度回転、x軸周りの180度回転、およびz軸周りのS4回映操作を生成元とし、それらの閉包を計算します。
 391    :returns: Td点群の対称操作行列のリスト。
 392    :rtype: List[numpy.ndarray]
 393    """
 394    S4z = mirror(ez) @ rot(ez,90.0)
 395    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0), S4z ])
 396
 397def build_O() -> List[np.ndarray]:
 398    """
 399    O (八面体) 点群の全ての対称操作行列を構築します。
 400
 401    概要:
 402        正八面体または立方体の対称性を持つ点群の全要素行列を返します。
 403    詳細説明:
 404        3次元空間における軸の順列と符号の組み合わせにより、立方体の回転対称操作を直接列挙します。
 405        行列式が1の操作のみを含めます。
 406    :returns: O点群の対称操作行列のリスト。
 407    :rtype: List[numpy.ndarray]
 408    """
 409    mats = []
 410    from itertools import permutations, product
 411    for perm in permutations(range(3)):
 412        P = np.eye(3)[list(perm)]
 413        for signs in product([-1,1], repeat=3):
 414            S = np.diag(signs)
 415            M = S @ P
 416            if round(np.linalg.det(M)) == 1:
 417                mats.append(snap_matrix(M))
 418    return list({mat_key(M): M for M in mats}.values())
 419
 420def build_Oh() -> List[np.ndarray]:
 421    """
 422    Oh (全八面体) 点群の全ての対称操作行列を構築します。
 423
 424    概要:
 425        O点群に反転中心が加わった点群の全要素行列を返します。
 426    詳細説明:
 427        O点群の全ての操作に反転操作を適用したものを加え、ユニークな要素を抽出します。
 428    :returns: Oh点群の対称操作行列のリスト。
 429    :rtype: List[numpy.ndarray]
 430    """
 431    base = build_O()
 432    inv = inversion()
 433    d = {}
 434    for M in base + [inv @ M for M in base]:
 435        d[mat_key(M)] = snap_matrix(M)
 436    return list(d.values())
 437
 438# ========= 記号の正規化と相互変換 =========
 439
 440def normalize_symbol(s: str) -> str:
 441    """
 442    点群シンボル文字列を正規化します。
 443
 444    概要:
 445        入力された点群シンボル文字列から空白、ハイフン、アンダースコアを除去し、標準形式に変換します。
 446    詳細説明:
 447        異なる形式で入力されうるハイフン文字(U+2212, U+2013, U+2014)も標準のASCIIハイフンに統一します。
 448    :param s: 正規化する点群シンボル文字列。
 449    :type s: str
 450    :returns: 正規化された点群シンボル文字列。
 451    :rtype: str
 452    """
 453    s = s.strip().replace(" ", "")
 454    s = s.replace("−","-").replace("–","-").replace("—","-").replace("_","")
 455    return s
 456
 457# 代表的な相互変換(曖昧さがあるものは代表形に丸める)
 458#   ※ 本ライブラリは簡易マップです。必要に応じて拡張してください。
 459_S_to_H = {
 460    "C1": "1",
 461    "Ci": "-1",
 462    "Cs": "m",
 463    "C2": "2",
 464    "C2h": "2/m",       # 追加
 465    "C2v": "mm2",
 466    "C3": "3",
 467    "C3h": "3/m",       # 代表的対応
 468    "C3v": "3m",
 469    "C4": "4",
 470    "C4h": "4/m",
 471    "C4v": "4mm",
 472    "C6": "6",
 473    "C6h": "6/m",
 474    "C6v": "6mm",
 475    "D2": "222",
 476    "D2h": "mmm",
 477    "D2d": "42m",       # 一般に 4̅2m だが、ここでは群操作生成は内部実装に委譲
 478    "D3": "32",
 479    "D3h": "6mm",       # 代表的に同型(操作生成は build_* に委譲)
 480    "D3d": "-3m",
 481    "D4": "422",
 482    "D4h": "4/mmm",
 483    "D4d": "-42m",
 484    "D6": "622",
 485    "D6h": "6/mmm",
 486    "D6d": "-6m2",
 487    "T": "23",
 488    "Th": "m-3",
 489    "Td": "-43m",
 490    "O": "432",
 491    "Oh": "m-3m",
 492}
 493
 494# 逆写像(値の代表形を採用)
 495_H_to_S = {
 496    "1": "C1",
 497    "-1": "Ci",
 498    "m": "Cs",
 499    "2": "C2",
 500    "2/m": "C2h",
 501    "mm2": "C2v",
 502    "3": "C3",
 503    "3/m": "C3h",
 504    "3m": "C3v",
 505    "4": "C4",
 506    "4/m": "C4h",
 507    "4mm": "C4v",
 508    "4/mmm": "D4h",
 509    "6": "C6",
 510    "6/m": "C6h",
 511    "6mm": "C6v",
 512    "6/mmm": "D6h",
 513    "23": "T",
 514    "m-3": "Th",
 515    "432": "O",
 516    "-43m": "Td",
 517    "m-3m": "Oh",
 518    "-3": "C3i",     # 実務上の近似対応(元コードの方針を踏襲)
 519#    "-3": "C3h",     # 実務上の近似対応(元コードの方針を踏襲)
 520# 以下は build_group 側の代表入力に合わせて一部近傍対応
 521    "mmm": "D2h",
 522
 523    "222": "D2",
 524    "mmm": "D2h",
 525    "-4":  "S4",
 526    "-6":  "S6",
 527    "422": "D4",
 528    "32":  "D3",
 529    "622": "D6",
 530    "-42m": "D2d",
 531    "-6m2": "D3h",
 532    "-3m": "D3d",
 533}
 534
 535def schoenflies_to_hm(s: str) -> str:
 536    """
 537    Schoenflies点群記号をHerman–Mauguin (国際) 記号に変換します。
 538
 539    概要:
 540        与えられたSchoenflies記号を対応するHerman–Mauguin記号に変換します。
 541    詳細説明:
 542        変換マップ `_S_to_H` を使用します。マップにない記号はそのまま返されます。
 543        変換前に `normalize_symbol` でシンボルを正規化します。
 544    :param s: Schoenflies点群記号。
 545    :type s: str
 546    :returns: 対応するHerman–Mauguin点群記号。
 547    :rtype: str
 548    """
 549    s = normalize_symbol(s)
 550    return _S_to_H.get(s, s)
 551
 552def hm_to_schoenflies(h: str) -> str:
 553    """
 554    Herman–Mauguin (国際) 点群記号をSchoenflies記号に変換します。
 555
 556    概要:
 557        与えられたHerman–Mauguin記号を対応するSchoenflies記号に変換します。
 558    詳細説明:
 559        変換マップ `_H_to_S` を使用します。マップにない記号はそのまま返されます。
 560        変換前に `normalize_symbol` でシンボルを正規化します。
 561    :param h: Herman–Mauguin点群記号。
 562    :type h: str
 563    :returns: 対応するSchoenflies点群記号。
 564    :rtype: str
 565    """
 566    h = normalize_symbol(h)
 567    return _H_to_S.get(h, h)
 568
 569# ========= サポート一覧 / 構築エントリ =========
 570
 571SUPPORTED = [
 572    # Schoenflies families
 573    "C1","Ci","Cs",
 574    "C2","C3","C4","C6",
 575    "C2v","C3v","C4v","C6v",
 576    "C2h","C3h","C4h","C6h",
 577    "S2","S3","S4","S6",
 578    "D2","D3","D4","D6",
 579    "D2h","D3h","D4h","D6h",
 580    "D2d","D3d","D4d","D6d",
 581    "T","Th","Td",
 582    "O","Oh",
 583    # Representative international notations (代表形を主に採用)
 584    "1","-1","m","2","2/m","mm2","mmm",
 585    "4","4/m","4mm","4/mmm",
 586    "3","-3","3m","3/m","-3m",
 587    "6","6/m","6mm","6/mmm",
 588    "23","m-3","432","-43m","m-3m",
 589]
 590
 591def supported_symbols() -> List[str]:
 592    """
 593    このモジュールで構築可能な点群シンボルの一覧を返します。
 594
 595    概要:
 596        現在サポートされている点群シンボルのリストを返します。
 597    詳細説明:
 598        Schoenflies記号と、それに対応する代表的なHerman–Mauguin記号が含まれます。
 599    :returns: サポートされている点群シンボルのリスト。
 600    :rtype: List[str]
 601    """
 602    return list(SUPPORTED)
 603
 604def build_group(symbol: str) -> List[np.ndarray]:
 605    """
 606    指定された点群シンボルに対応する全ての対称操作行列を構築します。
 607
 608    概要:
 609        点群シンボル(SchoenfliesまたはHerman–Mauguin)に基づいて、その点群の全ての対称操作行列をリストで返します。
 610    詳細説明:
 611        入力シンボルを正規化し、対応する `build_C*` や `build_D*` などの関数を呼び出します。
 612        サポートされていないシンボルの場合は `ValueError` を発生させます。
 613    :param symbol: 構築する点群のシンボル(例: "C2v", "mmm")。
 614    :type symbol: str
 615    :returns: 点群の全ての対称操作行列のリスト。
 616    :rtype: List[numpy.ndarray]
 617    :raises ValueError: サポートされていない点群シンボルが指定された場合。
 618    """
 619    s = normalize_symbol(symbol)
 620    # Schoenflies
 621    if s in ("C1","1"):
 622        return [np.eye(3)]
 623    if s in ("Ci","-1","1bar"):
 624        return [np.eye(3), inversion()]
 625    if s in ("Cs","m"):
 626        return unique_closure([mirror(ez)])
 627    if s.startswith("C") and s.endswith("v") and s[1:-1].isdigit():
 628        return build_Cnv(int(s[1:-1]))
 629    if s.startswith("C") and s.endswith("h") and s[1:-1].isdigit():
 630        return build_Cnh(int(s[1:-1]))
 631    if s.startswith("C") and s[1:].isdigit():
 632        return build_Cn(int(s[1:]))
 633    if s.startswith("S") and s[1:].isdigit():
 634        return build_Sn(int(s[1:]))
 635
 636    if s.startswith("D") and s.endswith("h") and s[1:-1].isdigit():
 637        return build_Dnh(int(s[1:-1]))
 638    if s.startswith("D") and s.endswith("d") and s[1:-1].isdigit():
 639        return build_Dnd(int(s[1:-1]))
 640    if s.startswith("D") and s[1:].isdigit():
 641        return build_Dn(int(s[1:]))
 642
 643    if s == "T":  return build_T()
 644    if s == "Th": return build_Th()
 645    if s == "Td": return build_Td()
 646    if s == "O":  return build_O()
 647    if s == "Oh": return build_Oh()
 648
 649    # International reps(代表入力を build_* に対応づけ)
 650    if s == "2":      return build_Cn(2)
 651    if s == "2/m":    return build_Cnh(2)
 652    if s == "mm2":    return build_Cnv(2)
 653    if s == "mmm":    return build_Dnh(2)
 654
 655    if s == "4":      return build_Cn(4)
 656    if s == "4/m":    return build_Cnh(4)
 657    if s == "4mm":    return build_Cnv(4)
 658    if s == "4/mmm":  return build_Dnh(4)
 659
 660    if s == "3":      return build_Cn(3)
 661    if s in ("-3","3bar"):  # 実用マップ(元コード踏襲)
 662        return build_Cnh(3)
 663    if s == "3/m":    return build_Cnh(3)      # 追加
 664    if s == "3m":     return build_Cnv(3)
 665    if s in ("-3m","D3d"):
 666        return build_Dnh(3)
 667
 668    if s == "6":      return build_Cn(6)
 669    if s == "6/m":    return build_Cnh(6)
 670    if s == "6mm":    return build_Cnv(6)
 671    if s == "6/mmm":  return build_Dnh(6)
 672
 673    if s == "23":     return build_T()
 674    if s == "m-3":    return build_Th()
 675    if s == "432":    return build_O()
 676    if s == "-43m":   return build_Td()
 677    if s == "m-3m":   return build_Oh()
 678
 679    raise ValueError(f"Unsupported / unknown point group symbol: {symbol}")
 680
 681# ========= ラベリング(簡易) =========
 682
 683def classify_label(M: np.ndarray) -> str:
 684    """
 685    対称操作行列を簡易的なラベル(例: "E", "i", "σ_h", "C(z,90)")に分類します。
 686
 687    概要:
 688        与えられた3x3の対称操作行列を行列式とトレースに基づいて分類し、対応する文字列ラベルを返します。
 689    詳細説明:
 690        単位行列は "E"、反転行列は "i" となります。
 691        行列式が-1の操作で自乗すると単位行列になるものは鏡映(σ)と判断し、
 692        法線ベクトルの方向によって σ_h, σ_v などを割り当てます。
 693        行列式が1の操作は回転 (C)、行列式が-1の操作で鏡映でないものは回映 (S) と判断し、
 694        回転軸と角度に基づいてラベルを生成します。
 695    :param M: 分類する3x3の対称操作行列。
 696    :type M: numpy.ndarray
 697    :returns: 分類された操作を示す文字列ラベル。
 698    :rtype: str
 699    """
 700    if np.allclose(M, np.eye(3), atol=1e-8):
 701        return "E"
 702    if np.allclose(M, -np.eye(3), atol=1e-8):
 703        return "i"
 704    det = np.linalg.det(M)
 705    # mirror?
 706    if abs(det + 1.0) < 1e-6 and np.allclose(M @ M, np.eye(3), atol=1e-6):
 707        # 法線 = 固有値 -1 の固有ベクトル
 708        w, v = np.linalg.eig(M)
 709        idx = np.argmin(np.abs(w + 1))
 710        n = np.real(v[:, idx]); n = _norm(n)
 711        if abs(np.dot(n, ez)) > 0.9: return "σ_h"
 712        if abs(np.dot(n, ex)) > 0.9 or abs(np.dot(n, ey)) > 0.9: return "σ_v"
 713        return "σ"
 714    # rotation / improper
 715    tr = np.trace(M)
 716    ang = math.degrees(math.acos(max(-1.0, min(1.0, (tr-1)/2))))
 717    if abs(det - 1.0) < 1e-6:
 718        # proper rotation
 719        ax = np.array([M[2,1]-M[1,2], M[0,2]-M[2,0], M[1,0]-M[0,1]])
 720        if np.linalg.norm(ax) > 1e-8:
 721            a = _norm(ax)
 722            if abs(np.dot(a, ez)) > 0.9: axis = "z"
 723            elif abs(np.dot(a, ex)) > 0.9: axis = "x"
 724            elif abs(np.dot(a, ey)) > 0.9: axis = "y"
 725            elif abs(np.dot(a, _norm([1,1,1]))) > 0.9: axis = "111"
 726            else: axis = "u"
 727        else:
 728            axis = "u"
 729        return f"C({axis},{int(round(ang))})"
 730    else:
 731        ax = np.array([M[2,1]-M[1,2], M[0,2]-M[2,0], M[1,0]-M[0,1]])
 732        axis = "z" if np.linalg.norm(ax) > 1e-8 and abs(_norm(ax)@ez) > 0.9 else "u"
 733        return f"S({axis},{int(round(ang))})"
 734
 735def label_elements(mats: List[np.ndarray]) -> List[Tuple[str, np.ndarray]]:
 736    """
 737    対称操作行列のリストをラベル付きのリストに変換します。
 738
 739    概要:
 740        各対称操作行列に対して `classify_label` 関数を適用し、(ラベル, 行列) のペアのリストを生成します。
 741    :param mats: 対称操作行列のリスト。
 742    :type mats: List[numpy.ndarray]
 743    :returns: ラベルとそれに対応する行列のペアのリスト。
 744    :rtype: List[Tuple[str, numpy.ndarray]]
 745    """
 746    return [(classify_label(M), M) for M in mats]
 747
 748def generators_for(symbol: str) -> List[Tuple[str, np.ndarray]]:
 749    """
 750    指定された点群シンボルに対応する規約な生成元を返します(ラベル付き)。
 751
 752    概要:
 753        特定の点群シンボルに対して、その群を生成するための最小限の操作行列のリストをラベル付きで提供します。
 754    詳細説明:
 755        ほとんどのSchoenflies記号に対して定義済みの生成元を返します。
 756        国際記号など、生成元が明確に定義されていない場合は空のリストを返します。
 757        返される全ての生成元は `snap_matrix` で正規化されています。
 758    :param symbol: 点群シンボル。
 759    :type symbol: str
 760    :returns: ラベルと生成元行列のペアのリスト。生成元が定義されていない場合は空リスト。
 761    :rtype: List[Tuple[str, numpy.ndarray]]
 762    """
 763    s = normalize_symbol(symbol)
 764    gens: List[Tuple[str, np.ndarray]] = []
 765    if s in ("C1","1"):
 766        gens = []
 767    elif s in ("Ci","-1","1bar"):
 768        gens = [("i", inversion())]
 769    elif s in ("Cs","m"):
 770        gens = [("σ_h", mirror(ez))]
 771    elif s.startswith("C") and s.endswith("v") and s[1:-1].isdigit():
 772        n = int(s[1:-1]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("σ_v(φ=0)", mirror(vertical_plane(0.0)))]
 773    elif s.startswith("C") and s.endswith("h") and s[1:-1].isdigit():
 774        n = int(s[1:-1]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("σ_h", mirror(ez))]
 775    elif s.startswith("C") and s[1:].isdigit():
 776        n = int(s[1:]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n))]
 777    elif s.startswith("S") and s[1:].isdigit():
 778        n = int(s[1:]); gens = [(f"S(z,{360//n})", mirror(ez) @ rot(ez,360.0/n))]
 779    elif s.startswith("D") and s.endswith("h") and s[1:-1].isdigit():
 780        n = int(s[1:-1]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("C2(x)", rot(ex,180.0)), ("σ_h", mirror(ez))]
 781    elif s.startswith("D") and s.endswith("d") and s[1:-1].isdigit():
 782        n = int(s[1:-1]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("C2(x)", rot(ex,180.0)), ("σ_d", mirror(diagonal_plane(0.0)))]
 783    elif s.startswith("D") and s[1:].isdigit():
 784        n = int(s[1:]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("C2(x)", rot(ex,180.0))]
 785    elif s == "T":
 786        gens = [("C(111,120)", rot([1,1,1],120.0)), ("C2(x)", rot(ex,180.0))]
 787    elif s == "Th":
 788        gens = [("C(111,120)", rot([1,1,1],120.0)), ("C2(x)", rot(ex,180.0)), ("i", inversion())]
 789    elif s == "Td":
 790        gens = [("C(111,120)", rot([1,1,1],120.0)), ("C2(x)", rot(ex,180.0)), ("S4(z)", mirror(ez) @ rot(ez,90.0))]
 791    elif s == "O":
 792        gens = [("C4(z)", rot(ez,90.0)), ("C3(111)", rot([1,1,1],120.0))]
 793    elif s == "Oh":
 794        gens = [("C4(z)", rot(ez,90.0)), ("C3(111)", rot([1,1,1],120.0)), ("i", inversion())]
 795    else:
 796        # 国際記号などで generator が空の場合は空返し(必要なら full ops を呼び側で)
 797        return []
 798    return [(lab, snap_matrix(M)) for lab,M in gens]
 799
 800# ========= 軌道・独立代表点(重複削除) =========
 801
 802def point_key(p: np.ndarray, tol: float = 1e-8) -> tuple:
 803    """
 804    点の同一性判定用キーを生成します(トレランスつき)。
 805
 806    概要:
 807        与えられた3D点のNumPy配列を、浮動小数点誤差を考慮したハッシュ可能なタプルキーに変換します。
 808    詳細説明:
 809        点を指定された許容誤差 (`tol`) でスケールし、最も近い整数に丸めます。
 810        これにより、非常に近い点が同じキーを持つようになり、集合や辞書での重複排除に利用できます。
 811    :param p: 3次元の点座標。
 812    :type p: numpy.ndarray
 813    :param tol: 点の比較に用いる許容誤差。
 814    :type tol: float
 815    :returns: 点のハッシュ可能なタプルキー。
 816    :rtype: tuple
 817    """
 818    p = np.asarray(p, float)
 819    scale = max(tol, 1e-15)
 820    return tuple(np.round(p / scale).astype(int))
 821
 822def orbit_for_point(ops: List[np.ndarray], p: np.ndarray, tol: float = 1e-8) -> List[Tuple[np.ndarray, List[int]]]:
 823    """
 824    一点から生成される軌道上の全ての等価点と、それらを生成する操作のインデックスを返します。
 825
 826    概要:
 827        与えられた点pに、点群の全ての操作を適用して生成される軌道上のユニークな点をリストアップします。
 828    詳細説明:
 829        各操作 M に対して M @ p を計算し、`point_key` を使用してユニークな点を識別します。
 830        結果は、(等価点, [その点を生成する操作のインデックスのリスト]) のタプルのリストとして返されます。
 831    :param ops: 点群の対称操作行列のリスト。
 832    :type ops: List[numpy.ndarray]
 833    :param p: 軌道を計算する開始点。
 834    :type p: numpy.ndarray
 835    :param tol: 点の比較に用いる許容誤差。
 836    :type tol: float
 837    :returns: 軌道上の各ユニークな点と、それを生成する操作インデックスのリストのタプル。
 838    :rtype: List[Tuple[numpy.ndarray, List[int]]]
 839    """
 840    pts: Dict[tuple, Tuple[np.ndarray, List[int]]] = {}
 841    for i, M in enumerate(ops):
 842        q = M @ p
 843        k = point_key(q, tol)
 844        if k not in pts:
 845            pts[k] = (q, [i])
 846        else:
 847            pts[k][1].append(i)
 848    return list(pts.values())  # [(q, [op_idx, ...]), ...]
 849
 850def dedup_points(orbits: List[Tuple[np.ndarray, List[int]]], tol: float = 1e-8) -> List[Tuple[np.ndarray, List[int]]]:
 851    """
 852    (点, 操作IDリスト) のリストからユニークな点を抽出します。
 853
 854    概要:
 855        `orbit_for_point` の結果から、座標が同じ点をまとめ、それらに対応する操作IDを結合します。
 856    詳細説明:
 857        `point_key` を使用して点の重複を検出し、重複する点が見つかった場合は、
 858        対応する操作IDのセットを結合します。最終的に、ユニークな点とその点を生成する
 859        全ての操作ID(ソート済みリスト)のペアを返します。
 860    :param orbits: `orbit_for_point` から返される (点, 操作IDリスト) のリスト。
 861    :type orbits: List[Tuple[numpy.ndarray, List[int]]]
 862    :param tol: 点の比較に用いる許容誤差。
 863    :type tol: float
 864    :returns: 重複排除された (ユニークな点, その点を生成する操作IDのリスト) のペアのリスト。
 865    :rtype: List[Tuple[numpy.ndarray, List[int]]]
 866    """
 867    seen: Dict[tuple, int] = {}
 868    reps: List[List[object]] = []
 869    for q, op_ids in orbits:
 870        k = point_key(q, tol)
 871        if k not in seen:
 872            seen[k] = len(reps)
 873            reps.append([q, set(op_ids)])
 874        else:
 875            reps[seen[k]][1].update(op_ids)
 876    return [(np.asarray(q, float), sorted(list(ids))) for q, ids in reps]
 877
 878def unique_orbit_points(ops: List[np.ndarray], p: np.ndarray, tol: float = 1e-8) -> List[Tuple[np.ndarray, List[int]]]:
 879    """
 880    対称操作行列と開始点を受け取り、重複排除された軌道上の独立点とその生成操作のインデックスを返します。
 881
 882    概要:
 883        与えられた点pに点群の全ての操作を適用し、結果として得られる軌道上の重複しない点を抽出します。
 884    詳細説明:
 885        `orbit_for_point` と `dedup_points` を内部的に使用して、
 886        最終的な独立代表点と、それらを作成する全ての対称操作のインデックスをまとめます。
 887    :param ops: 点群の対称操作行列のリスト。
 888    :type ops: List[numpy.ndarray]
 889    :param p: 軌道を計算する開始点。
 890    :type p: numpy.ndarray
 891    :param tol: 点の比較に用いる許容誤差。
 892    :type tol: float
 893    :returns: 軌道上のユニークな各点と、それを生成する操作インデックスのリストのタプル。
 894    :rtype: List[Tuple[numpy.ndarray, List[int]]]
 895    """
 896    return dedup_points(orbit_for_point(ops, p, tol=tol), tol=tol)
 897
 898# ========= 高レベルAPI =========
 899
 900def get_all_operations(symbol: str) -> List[Tuple[str, np.ndarray]]:
 901    """
 902    点群シンボルから全要素(ラベル付き、重複なし)を返します。
 903
 904    概要:
 905        指定された点群シンボルに対応する全ての対称操作行列と、それぞれの簡易ラベルのペアのリストを返します。
 906    詳細説明:
 907        `build_group` を使用して行列を構築し、`label_elements` でラベル付けを行います。
 908    :param symbol: 点群シンボル。
 909    :type symbol: str
 910    :returns: ラベルと操作行列のペアのリスト。
 911    :rtype: List[Tuple[str, numpy.ndarray]]
 912    :raises ValueError: サポートされていない点群シンボルが指定された場合。
 913    """
 914    mats = build_group(symbol)
 915    return label_elements(mats)
 916
 917def get_generators(symbol: str) -> List[Tuple[str, np.ndarray]]:
 918    """
 919    点群シンボルから生成元(ラベル付き)を返します。
 920
 921    概要:
 922        指定された点群シンボルに対応する生成元の操作行列と、それぞれの簡易ラベルのペアのリストを返します。
 923    詳細説明:
 924        `generators_for` を使用して定義済みの生成元を取得します。
 925        もし `generators_for` が生成元を返さない場合(国際記号など)、`get_all_operations` にフォールバックし、
 926        全ての群要素を生成元と見なして返します。
 927    :param symbol: 点群シンボル。
 928    :type symbol: str
 929    :returns: ラベルと生成元行列のペアのリスト。
 930    :rtype: List[Tuple[str, numpy.ndarray]]
 931    :raises ValueError: サポートされていない点群シンボルが指定された場合(`build_group`経由)。
 932    """
 933    gens = generators_for(symbol)
 934    if gens:
 935        return gens
 936    # generator未定義(国際記号など)の場合は全要素へフォールバック
 937    return get_all_operations(symbol)
 938
 939def elements_from_generators(generators: List[np.ndarray]) -> List[Tuple[str, np.ndarray]]:
 940    """
 941    生成元行列のリストから閉包をとり、ラベル付きの全要素を返します。
 942
 943    概要:
 944        与えられた生成元行列のリストから、それらによって生成される全てのユニークな群要素を計算し、
 945        それぞれの操作に簡易ラベルを付けて返します。
 946    詳細説明:
 947        `unique_closure` を使用して生成元から全ての群要素行列を計算し、
 948        `label_elements` で各行列にラベルを付けます。
 949    :param generators: 群の生成元であるNumPy行列のリスト。
 950    :type generators: List[numpy.ndarray]
 951    :returns: ラベルと群要素行列のペアのリスト。
 952    :rtype: List[Tuple[str, numpy.ndarray]]
 953    """
 954    mats = unique_closure(generators)
 955    return label_elements(mats)
 956
 957# Point-group character tables, abstract class sizes, and helpers for Γ_3N/Γ_T/Γ_R.
 958
 959import numpy as _np
 960from collections import Counter as _Counter, defaultdict as _defaultdict
 961from typing import Dict as _Dict, List as _List, Tuple as _Tuple, Optional as _Optional
 962
 963# ---------- Schoenflies <-> HM (補完) ----------
 964# 既存のマップを尊重しつつ、追加グループを補う(C4h, C3h, I, Ih など)
 965_S_to_H.update({
 966    "C4h":"4/m",
 967    "Ch":"m",
 968    "C3h":"C3h",   # 非結晶・抽象モード
 969    "I":"I",       # 正二十面体群(抽象モード)
 970    "Ih":"Ih",     # 完全二十面体群(抽象モード)
 971})
 972_H_to_S.update({
 973    "4/m":"C4h",
 974    "m":"Cs",      # alias
 975    # 非結晶は自分自身を返す
 976    "C3h":"C3h",
 977})
 978
 979# ---------- Character tables ----------
 980# 既知の主要点群 + 非結晶 (I, Ih, C3h) + C4h(可換)
 981# (実数表示/必要箇所は複素数を許容)
 982_phi  = (1 + 5**0.5)/2
 983_phip = (1 - 5**0.5)/2
 984
 985def _gen_C4h_table() -> _Dict[str, _Dict[str, complex]]:
 986    """
 987    C4h点群の指標表を生成します。
 988
 989    概要:
 990        C4h点群のクラスと既約表現の指標を含む辞書を動的に構築して返します。
 991    詳細説明:
 992        C4hは可換群であり、その既約表現は1次元です。
 993        各既約表現は、a (0から3) と偶奇性 (g/u) の組み合わせでタグ付けされます。
 994    :returns: C4h点群の指標表。
 995    :rtype: Dict[str, Dict[str, complex]]
 996    """
 997    classes = ["E","C4","C4^3","C2","i","S4","S4^3","σh"]
 998    irreps = {}
 999    for a in (0,1,2,3):
1000        for par, tag in ((+1,"g"),(-1,"u")):
1001            lab = f{a}{tag}"
1002            def phase(k): return _np.exp(1j*_np.pi/2 * a * k)
1003            irreps[lab] = {
1004                "E":   1.0,
1005                "C4":  phase(1),
1006                "C4^3":phase(3),
1007                "C2":  phase(2),
1008                "i":   par * 1.0,
1009                "S4":  par * phase(1),
1010                "S4^3":par * phase(3),
1011                "σh":  par * phase(2),
1012            }
1013    return {"classes": classes, "irreps": irreps}
1014
1015# 主要表(必要最小限。C系/Cnv/Cnh/D系/T/Th/Td/O/Oh/C3h/C4h/I/Ih を含む)
1016# フル表は長いのでここでは代表的なものを実装(vib_irreps で使う範囲をカバー)
1017PG_CHAR_TABLES: _Dict[str, _Dict[str, _Dict[str, complex]]] = {
1018    # --- C, Cnv, Cnh(代表) ---
1019    "C1":{"classes":["E"], "irreps":{"A":{"E":1}}},
1020    "Ci":{"classes":["E","i"], "irreps":{"Ag":{"E":1,"i":1}, "Au":{"E":1,"i":-1}}},
1021    "Cs":{"classes":["E","σ"], "irreps":{"A'":{"E":1,"σ":1}, "A''":{"E":1,"σ":-1}}},
1022    "Ch":{"classes":["E","σ"], "irreps":{"A'":{"E":1,"σ":1}, "A''":{"E":1,"σ":-1}}},
1023    "C2":{"classes":["E","C2"], "irreps":{"A":{"E":1,"C2":1}, "B":{"E":1,"C2":-1}}},
1024    "C3":{"classes":["E","C3","C3^2"], "irreps":{"A":{"E":1,"C3":1,"C3^2":1}, "E":{"E":2,"C3":-1,"C3^2":-1}}},
1025    "C4":{"classes":["E","C4","C2","C4^3"], "irreps":{
1026        "A":{"E":1,"C4":1,"C2":1,"C4^3":1},
1027        "B":{"E":1,"C4":-1,"C2":1,"C4^3":-1},
1028        "E":{"E":2,"C4":0,"C2":-2,"C4^3":0}
1029    }},
1030    "C6":{"classes":["E","C6","C3","C2","C3^2","C6^5"], "irreps":{
1031        "A":{"E":1,"C6":1,"C3":1,"C2":1,"C3^2":1,"C6^5":1},
1032        "B":{"E":1,"C6":-1,"C3":1,"C2":-1,"C3^2":1,"C6^5":-1},
1033        "E1":{"E":2,"C6":1,"C3":-1,"C2":-2,"C3^2":-1,"C6^5":1},
1034        "E2":{"E":2,"C6":-1,"C3":-1,"C2":2,"C3^2":-1,"C6^5":-1}
1035    }},
1036    "C2v":{"classes":["E","C2","σv","σv'"], "irreps":{
1037        "A1":{"E":1,"C2":1,"σv":1,"σv'":1},
1038        "A2":{"E":1,"C2":1,"σv":-1,"σv'":-1},
1039        "B1":{"E":1,"C2":-1,"σv":1,"σv'":-1},
1040        "B2":{"E":1,"C2":-1,"σv":-1,"σv'":1}
1041    }},
1042    "C3v":{"classes":["E","C3","C3^2","σv"], "irreps":{
1043        "A1":{"E":1,"C3":1,"C3^2":1,"σv":1},
1044        "A2":{"E":1,"C3":1,"C3^2":1,"σv":-1},
1045        "E":{"E":2,"C3":-1,"C3^2":-1,"σv":0}
1046    }},
1047    "C4v":{"classes":["E","C4","C4^3","C2","σv","σd"], "irreps":{
1048        "A1":{"E":1,"C4":1,"C4^3":1,"C2":1,"σv":1,"σd":1},
1049        "A2":{"E":1,"C4":1,"C4^3":1,"C2":1,"σv":-1,"σd":-1},
1050        "B1":{"E":1,"C4":-1,"C4^3":-1,"C2":1,"σv":1,"σd":-1},
1051        "B2":{"E":1,"C4":-1,"C4^3":-1,"C2":1,"σv":-1,"σd":1},
1052        "E":{"E":2,"C4":0,"C4^3":0,"C2":-2,"σv":0,"σd":0}
1053    }},
1054    "C6v":{"classes":["E","C6","C6^5","C3","C3^2","C2","σv","σd"], "irreps":{
1055        "A1":{"E":1,"C6":1,"C6^5":1,"C3":1,"C3^2":1,"C2":1,"σv":1,"σd":1},
1056        "A2":{"E":1,"C6":1,"C6^5":1,"C3":1,"C3^2":1,"C2":1,"σv":-1,"σd":-1},
1057        "B1":{"E":1,"C6":-1,"C6^5":-1,"C3":1,"C3^2":1,"C2":-1,"σv":1,"σd":-1},
1058        "B2":{"E":1,"C6":-1,"C6^5":-1,"C3":1,"C3^2":1,"C2":-1,"σv":-1,"σd":1},
1059        "E1":{"E":2,"C6":1,"C6^5":1,"C3":-1,"C3^2":-1,"C2":-2,"σv":0,"σd":0},
1060        "E2":{"E":2,"C6":-1,"C6^5":-1,"C3":-1,"C3^2":-1,"C2":2,"σv":0,"σd":0}
1061    }},
1062    "C2h":{"classes":["E","C2(z)","i","σh"], "irreps":{
1063        "Ag":{"E":1,"C2(z)":1,"i":1,"σh":1},
1064        "Bg":{"E":1,"C2(z)":-1,"i":1,"σh":-1},
1065        "Au":{"E":1,"C2(z)":1,"i":-1,"σh":-1},
1066        "Bu":{"E":1,"C2(z)":-1,"i":-1,"σh":1},
1067    }},
1068    # --- D 系(代表) ---
1069    "D2":{"classes":["E","C2(x)","C2(y)","C2(z)"], "irreps":{
1070        "A":{"E":1,"C2(x)":1,"C2(y)":1,"C2(z)":1},
1071        "B1":{"E":1,"C2(x)":1,"C2(y)":-1,"C2(z)":-1},
1072        "B2":{"E":1,"C2(x)":-1,"C2(y)":1,"C2(z)":-1},
1073        "B3":{"E":1,"C2(x)":-1,"C2(y)":-1,"C2(z)":1},
1074    }},
1075    "D2h":{"classes":["E","C2(x)","C2(y)","C2(z)","i","σ(xy)","σ(xz)","σ(yz)"], "irreps":{
1076        "Ag":{"E":1,"C2(x)":1,"C2(y)":1,"C2(z)":1,"i":1,"σ(xy)":1,"σ(xz)":1,"σ(yz)":1},
1077        "B1g":{"E":1,"C2(x)":1,"C2(y)":-1,"C2(z)":-1,"i":1,"σ(xy)":1,"σ(xz)":-1,"σ(yz)":-1},
1078        "B2g":{"E":1,"C2(x)":-1,"C2(y)":1,"C2(z)":-1,"i":1,"σ(xy)":-1,"σ(xz)":1,"σ(yz)":-1},
1079        "B3g":{"E":1,"C2(x)":-1,"C2(y)":-1,"C2(z)":1,"i":1,"σ(xy)":-1,"σ(xz)":-1,"σ(yz)":1},
1080        "Au":{"E":1,"C2(x)":1,"C2(y)":1,"C2(z)":1,"i":-1,"σ(xy)":-1,"σ(xz)":-1,"σ(yz)":-1},
1081        "B1u":{"E":1,"C2(x)":1,"C2(y)":-1,"C2(z)":-1,"i":-1,"σ(xy)":-1,"σ(xz)":1,"σ(yz)":1},
1082        "B2u":{"E":1,"C2(x)":-1,"C2(y)":1,"C2(z)":-1,"i":-1,"σ(xy)":1,"σ(xz)":-1,"σ(yz)":1},
1083        "B3u":{"E":1,"C2(x)":-1,"C2(y)":-1,"C2(z)":1,"i":-1,"σ(xy)":1,"σ(xz)":1,"σ(yz)":-1},
1084    }},
1085    "D3":{"classes":["E","C3","C2'"], "irreps":{
1086        "A1":{"E":1,"C3":1,"C2'":1},
1087        "A2":{"E":1,"C3":1,"C2'":-1},
1088        "E":{"E":2,"C3":-1,"C2'":0},
1089    }},
1090    "D3d":{"classes":["E","C3","C2'","i","S6","σd"], "irreps":{
1091        "A1g":{"E":1,"C3":1,"C2'":1,"i":1,"S6":1,"σd":1},
1092        "A2g":{"E":1,"C3":1,"C2'":-1,"i":1,"S6":-1,"σd":-1},
1093        "Eg":{"E":2,"C3":-1,"C2'":0,"i":2,"S6":0,"σd":0},
1094        "A1u":{"E":1,"C3":1,"C2'":1,"i":-1,"S6":-1,"σd":-1},
1095        "A2u":{"E":1,"C3":1,"C2'":-1,"i":-1,"S6":1,"σd":1},
1096        "Eu":{"E":2,"C3":-1,"C2'":0,"i":-2,"S6":0,"σd":0},
1097    }},
1098    "D3h":{"classes":["E","C3","C2'","σh","S3","σv"], "irreps":{
1099        "A1'":{"E":1,"C3":1,"C2'":1,"σh":1,"S3":1,"σv":1},
1100        "A2'":{"E":1,"C3":1,"C2'":-1,"σh":1,"S3":1,"σv":-1},
1101        "E'":{"E":2,"C3":-1,"C2'":0,"σh":2,"S3":-1,"σv":0},
1102        "A1''":{"E":1,"C3":1,"C2'":1,"σh":-1,"S3":-1,"σv":-1},
1103        "A2''":{"E":1,"C3":1,"C2'":-1,"σh":-1,"S3":-1,"σv":1},
1104        "E''":{"E":2,"C3":-1,"C2'":0,"σh":-2,"S3":1,"σv":0},
1105    }},
1106    "D4":{"classes":["E","C4","C2(z)","C2'","C2''"], "irreps":{
1107        "A1":{"E":1,"C4":1,"C2(z)":1,"C2'":1,"C2''":1},
1108        "A2":{"E":1,"C4":1,"C2(z)":1,"C2'":-1,"C2''":-1},
1109        "B1":{"E":1,"C4":-1,"C2(z)":1,"C2'":1,"C2''":-1},
1110        "B2":{"E":1,"C4":-1,"C2(z)":1,"C2'":-1,"C2''":1},
1111        "E":{"E":2,"C4":0,"C2(z)":-2,"C2'":0,"C2''":0},
1112    }},
1113    "D2d":{"classes":["E","S4","C2(z)","C2'","σd"], "irreps":{
1114        "A1":{"E":1,"S4":1,"C2(z)":1,"C2'":1,"σd":1},
1115        "A2":{"E":1,"S4":1,"C2(z)":1,"C2'":-1,"σd":-1},
1116        "B1":{"E":1,"S4":-1,"C2(z)":1,"C2'":1,"σd":-1},
1117        "B2":{"E":1,"S4":-1,"C2(z)":1,"C2'":-1,"σd":1},
1118        "E":{"E":2,"S4":0,"C2(z)":-2,"C2'":0,"σd":0},
1119    }},
1120    "D4h":{"classes":["E","C4","C2(z)","C2'","C2''","i","S4","σh","σv","σd"], "irreps":{
1121        "A1g":{"E":1,"C4":1,"C2(z)":1,"C2'":1,"C2''":1,"i":1,"S4":1,"σh":1,"σv":1,"σd":1},
1122        "A2g":{"E":1,"C4":1,"C2(z)":1,"C2'":-1,"C2''":-1,"i":1,"S4":1,"σh":1,"σv":-1,"σd":-1},
1123        "B1g":{"E":1,"C4":-1,"C2(z)":1,"C2'":1,"C2''":-1,"i":1,"S4":-1,"σh":1,"σv":1,"σd":-1},
1124        "B2g":{"E":1,"C4":-1,"C2(z)":1,"C2'":-1,"C2''":1,"i":1,"S4":-1,"σh":1,"σv":-1,"σd":1},
1125        "Eg":{"E":2,"C4":0,"C2(z)":-2,"C2'":0,"C2''":0,"i":2,"S4":0,"σh":2,"σv":0,"σd":0},
1126        "A1u":{"E":1,"C4":1,"C2(z)":1,"C2'":1,"C2''":1,"i":-1,"S4":-1,"σh":-1,"σv":-1,"σd":-1},
1127        "A2u":{"E":1,"C4":1,"C2(z)":1,"C2'":-1,"C2''":-1,"i":-1,"S4":-1,"σh":-1,"σv":1,"σd":1},
1128        "B1u":{"E":1,"C4":-1,"C2(z)":1,"C2'":1,"C2''":-1,"i":-1,"S4":1,"σh":-1,"σv":-1,"σd":1},
1129        "B2u":{"E":1,"C4":-1,"C2(z)":1,"C2'":-1,"C2''":1,"i":-1,"S4":1,"σh":-1,"σv":1,"σd":-1},
1130        "Eu":{"E":2,"C4":0,"C2(z)":-2,"C2'":0,"C2''":0,"i":-2,"S4":0,"σh":-2,"σv":0,"σd":0},
1131    }},
1132    "D6":{"classes":["E","C6","C3","C2(z)","C2'","C2''"], "irreps":{
1133        "A1":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":1,"C2''":1},
1134        "A2":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":-1,"C2''":-1},
1135        "B1":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":1,"C2''":-1},
1136        "B2":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":-1,"C2''":1},
1137        "E1":{"E":2,"C6":1,"C3":-1,"C2(z)":-2,"C2'":0,"C2''":0},
1138        "E2":{"E":2,"C6":-1,"C3":-1,"C2(z)":2,"C2'":0,"C2''":0},
1139    }},
1140    "D6h":{"classes":["E","C6","C3","C2(z)","C2'","C2''","i","S3","S6","σh","σd","σv"], "irreps":{
1141        "A1g":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":1,"C2''":1,"i":1,"S3":1,"S6":1,"σh":1,"σd":1,"σv":1},
1142        "A2g":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":-1,"C2''":-1,"i":1,"S3":1,"S6":1,"σh":1,"σd":-1,"σv":-1},
1143        "B1g":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":1,"C2''":-1,"i":1,"S3":-1,"S6":1,"σh":1,"σd":1,"σv":-1},
1144        "B2g":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":-1,"C2''":1,"i":1,"S3":-1,"S6":1,"σh":1,"σd":-1,"σv":1},
1145        "E1g":{"E":2,"C6":1,"C3":-1,"C2(z)":-2,"C2'":0,"C2''":0,"i":2,"S3":1,"S6":-1,"σh":2,"σd":0,"σv":0},
1146        "E2g":{"E":2,"C6":-1,"C3":-1,"C2(z)":2,"C2'":0,"C2''":0,"i":2,"S3":-1,"S6":1,"σh":2,"σd":0,"σv":0},
1147        "A1u":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":1,"C2''":1,"i":-1,"S3":-1,"S6":-1,"σh":-1,"σd":-1,"σv":-1},
1148        "A2u":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":-1,"C2''":-1,"i":-1,"S3":-1,"S6":-1,"σh":-1,"σd":1,"σv":1},
1149        "B1u":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":1,"C2''":-1,"i":-1,"S3":1,"S6":-1,"σh":-1,"σd":-1,"σv":1},
1150        "B2u":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":-1,"C2''":1,"i":-1,"S3":1,"S6":-1,"σh":-1,"σd":1,"σv":-1},
1151        "E1u":{"E":2,"C6":1,"C3":-1,"C2(z)":-2,"C2'":0,"C2''":0,"i":-2,"S3":-1,"S6":1,"σh":-2,"σd":0,"σv":0},
1152        "E2u":{"E":2,"C6":-1,"C3":-1,"C2(z)":2,"C2'":0,"C2''":0,"i":-2,"S3":1,"S6":-1,"σh":-2,"σd":0,"σv":0},
1153    }},
1154    # --- 立方 ---
1155    "T":{"classes":["E","C3","C2"], "irreps":{
1156        "A":{"E":1,"C3":1,"C2":1},
1157        "E":{"E":2,"C3":-1,"C2":2},
1158        "T":{"E":3,"C3":0,"C2":-1},
1159    }},
1160    "Td":{"classes":["E","C3","C2","S4","σd"], "irreps":{
1161        "A1":{"E":1,"C3":1,"C2":1,"S4":1,"σd":1},
1162        "A2":{"E":1,"C3":1,"C2":1,"S4":-1,"σd":-1},
1163        "E":{"E":2,"C3":-1,"C2":2,"S4":0,"σd":0},
1164        "T1":{"E":3,"C3":0,"C2":-1,"S4":1,"σd":-1},
1165        "T2":{"E":3,"C3":0,"C2":-1,"S4":-1,"σd":1},
1166    }},
1167    "Th":{"classes":["E","C3","C2","i","S6"], "irreps":{
1168        "Ag":{"E":1,"C3":1,"C2":1,"i":1,"S6":1},
1169        "Au":{"E":1,"C3":1,"C2":1,"i":-1,"S6":-1},
1170        "Eg":{"E":2,"C3":-1,"C2":2,"i":2,"S6":0},
1171        "Eu":{"E":2,"C3":-1,"C2":2,"i":-2,"S6":0},
1172        "Tg":{"E":3,"C3":0,"C2":-1,"i":3,"S6":-1},
1173        "Tu":{"E":3,"C3":0,"C2":-1,"i":-3,"S6":1},
1174    }},
1175    "O":{"classes":["E","C3","C4","C2(ax)","C2(di)"], "irreps":{
1176        "A1":{"E":1,"C3":1,"C4":1,"C2(ax)":1,"C2(di)":1},
1177        "A2":{"E":1,"C3":1,"C4":-1,"C2(ax)":1,"C2(di)":-1},
1178        "E":{"E":2,"C3":-1,"C4":0,"C2(ax)":2,"C2(di)":0},
1179        "T1":{"E":3,"C3":0,"C4":1,"C2(ax)":-1,"C2(di)":-1},
1180        "T2":{"E":3,"C3":0,"C4":-1,"C2(ax)":-1,"C2(di)":1},
1181    }},
1182    "Oh":{"classes":["E","C3","C4","C2(ax)","C2(di)","i","S6","S4","σh","σd"], "irreps":{
1183        "A1g":{"E":1,"C3":1,"C4":1,"C2(ax)":1,"C2(di)":1,"i":1,"S6":1,"S4":1,"σh":1,"σd":1},
1184        "A2g":{"E":1,"C3":1,"C4":-1,"C2(ax)":1,"C2(di)":-1,"i":1,"S6":-1,"S4":-1,"σh":1,"σd":-1},
1185        "Eg":{"E":2,"C3":-1,"C4":0,"C2(ax)":2,"C2(di)":0,"i":2,"S6":0,"S4":0,"σh":2,"σd":0},
1186        "T1g":{"E":3,"C3":0,"C4":1,"C2(ax)":-1,"C2(di)":-1,"i":3,"S6":-1,"S4":1,"σh":-1,"σd":-1},
1187        "T2g":{"E":3,"C3":0,"C4":-1,"C2(ax)":-1,"C2(di)":1,"i":3,"S6":1,"S4":-1,"σh":-1,"σd":1},
1188        "A1u":{"E":1,"C3":1,"C4":1,"C2(ax)":1,"C2(di)":1,"i":-1,"S6":-1,"S4":-1,"σh":-1,"σd":-1},
1189        "A2u":{"E":1,"C3":1,"C4":-1,"C2(ax)":1,"C2(di)":-1,"i":-1,"S6":1,"S4":1,"σh":-1,"σd":1},
1190        "Eu":{"E":2,"C3":-1,"C4":0,"C2(ax)":2,"C2(di)":0,"i":-2,"S6":0,"S4":0,"σh":-2,"σd":0},
1191        "T1u":{"E":3,"C3":0,"C4":1,"C2(ax)":-1,"C2(di)":-1,"i":-3,"S6":1,"S4":-1,"σh":1,"σd":1},
1192        "T2u":{"E":3,"C3":0,"C4":-1,"C2(ax)":-1,"C2(di)":1,"i":-3,"S6":-1,"S4":1,"σh":1,"σd":-1},
1193    }},
1194    # --- C3h, C4h ---
1195    "C3h":{"classes":["E","C3","σh","S3"], "irreps":{
1196        "A'":{"E":1,"C3":1,"σh":1,"S3":1},
1197        "A''":{"E":1,"C3":1,"σh":-1,"S3":-1},
1198        "E'":{"E":2,"C3":-1,"σh":2,"S3":-1},
1199        "E''":{"E":2,"C3":-1,"σh":-2,"S3":1},
1200    }},
1201}
1202PG_CHAR_TABLES["C4h"] = _gen_C4h_table()
1203
1204# --- Icosahedral (抽象) ---
1205PG_CHAR_TABLES.update({
1206    "I":{"classes":["E","C5","C5^2","C3","C2"], "irreps":{
1207        "A":{"E":1,"C5":1,"C5^2":1,"C3":1,"C2":1},
1208        "T1":{"E":3,"C5":_phi,"C5^2":_phip,"C3":0,"C2":-1},
1209        "T2":{"E":3,"C5":_phip,"C5^2":_phi,"C3":0,"C2":-1},
1210        "G":{"E":4,"C5":-1,"C5^2":-1,"C3":1,"C2":0},
1211        "H":{"E":5,"C5":0,"C5^2":0,"C3":-1,"C2":1},
1212    }},
1213    "Ih":{"classes":["E","C5","C5^2","C3","C2","i","S10","S10^3","S6","S2"], "irreps":{}},
1214})
1215def _fill_Ih_from_I():
1216    """
1217    Icosahedral (Ih) 点群の指標表をI (I) 点群の指標表から構築します。
1218
1219    概要:
1220        Ih点群の指標表は、I点群の指標表に反転操作を追加することで生成されます。
1221    詳細説明:
1222        I点群の各既約表現 (irreps) に対して、偶パリティ ('g') と奇パリティ ('u') の2つの新しい表現を生成します。
1223        これらの表現には、反転 (i) や回映 (S10, S6など) 操作の指標が含まれます。
1224    """
1225    base = PG_CHAR_TABLES["I"]["irreps"]
1226    irr = {}
1227    for name, row in base.items():
1228        gn = name+"g"; un = name+"u"
1229        irr[gn] = dict(row); irr[gn].update({"i":+row["E"], "S10":+row["C5"], "S10^3":+row["C5^2"], "S6":+row["C3"], "S2":+row["C2"]})
1230        irr[un] = dict(row); irr[un].update({"i":-row["E"], "S10":-row["C5"], "S10^3":-row["C5^2"], "S6":-row["C3"], "S2":-row["C2"]})
1231    PG_CHAR_TABLES["Ih"]["irreps"] = irr
1232_fill_Ih_from_I()
1233
1234# 公開API
1235def character_table(pg: str) -> _Dict[str, _Dict[str, complex]]:
1236    """
1237    指定された点群の指標表(classes, irreps)を返します。
1238
1239    概要:
1240        与えられた点群シンボルに対応する指標表を検索し、そのクラスと既約表現の指標を返します。
1241    詳細説明:
1242        `PG_CHAR_TABLES` 辞書から対応する指標表を取得します。
1243        サポートされていない点群の場合は `ValueError` を発生させます。
1244        シンボルは `normalize_symbol` で正規化されます。
1245    :param pg: 点群シンボル。
1246    :type pg: str
1247    :returns: 指標表を含む辞書。キーは 'classes' (クラス名のリスト) と 'irreps' (既約表現ごとの指標の辞書)。
1248    :rtype: Dict[str, Dict[str, complex]]
1249    :raises ValueError: 指定された点群の指標表が利用できない場合。
1250    """
1251    s = normalize_symbol(pg)
1252    if s not in PG_CHAR_TABLES:
1253        raise ValueError(f"Character table for '{pg}' is not available.")
1254    return PG_CHAR_TABLES[s]
1255
1256# 抽象モードのクラスサイズ(pymatgen/builder未対応の群用)
1257ABSTRACT_CLASS_SIZES: _Dict[str, _Dict[str,int]] = {
1258    "C3h":{"E":1, "C3":2, "σh":1, "S3":2},             # |G|=6
1259    "I":  {"E":1, "C5":12, "C5^2":12, "C3":20, "C2":15},# |G|=60
1260    "Ih": {"E":1, "C5":12, "C5^2":12, "C3":20, "C2":15, "i":1, "S10":12, "S10^3":12, "S6":20, "S2":15}, # |G|=120
1261}
1262
1263# ---------- 分類ラベル(クラス名)付け:表に合わせる ----------
1264_X = _np.array([1.0,0.0,0.0]); _Y = _np.array([0.0,1.0,0.0]); _Z = _np.array([0.0,0.0,1.0])
1265_XY_DIAGS = [_np.array([1,1,0.0]), _np.array([1,-1,0.0]), _np.array([-1,1,0.0]), _np.array([-1,-1,0.0])]
1266_C2_DIAGS3D = [
1267    _np.array([1,1,0.0]), _np.array([1,-1,0.0]), _np.array([-1,1,0.0]), _np.array([-1,-1,0.0]),
1268    _np.array([1,0,1.0]), _np.array([1,0,-1.0]), _np.array([-1,0,1.0]), _np.array([-1,0,-1.0]),
1269    _np.array([0,1,1.0]), _np.array([0,1,-1.0]), _np.array([0,-1,1.0]), _np.array([0,-1,-1.0]),
1270]
1271def _unit(v: _np.ndarray) -> _np.ndarray:
1272    """
1273    ベクトルを単位化します。
1274
1275    概要:
1276        入力ベクトルを単位ベクトルに変換します。
1277    詳細説明:
1278        ベクトルのノルムが非常に小さい場合でもゼロ除算を避けるために小さな値を加えます。
1279    :param v: 単位化するベクトル。
1280    :type v: numpy.ndarray
1281    :returns: 単位ベクトル。
1282    :rtype: numpy.ndarray
1283    """
1284    v=_np.asarray(v,float); n=_np.linalg.norm(v); return v/(n+1e-15)
1285
1286def _close_to(v: _np.ndarray, cands: _List[_np.ndarray], tol: float = 0.1) -> bool:
1287    """
1288    ベクトルが候補ベクトルのいずれかに近似しているかを判定します。
1289
1290    概要:
1291        与えられた単位ベクトルが、候補リスト内のいずれかの単位ベクトルに指定されたトレランス内で近似するかどうかをチェックします。
1292    詳細説明:
1293        入力ベクトルと各候補ベクトルの内積の絶対値を計算し、それが `1 - tol` より大きい場合に近似と見なします。
1294    :param v: 比較する単位ベクトル。
1295    :type v: numpy.ndarray
1296    :param cands: 比較対象の候補ベクトルリスト。
1297    :type cands: List[numpy.ndarray]
1298    :param tol: 近似を判定する許容誤差。
1299    :type tol: float
1300    :returns: ベクトルが候補群に近似していればTrue、そうでなければFalse。
1301    :rtype: bool
1302    """
1303    v=_unit(v); best=-1
1304    for c in cands:
1305        d=abs(float(_unit(c)@v))
1306        if d>best: best=d
1307    return best >= (1-tol)
1308
1309def _orth_keep_det(R: _np.ndarray) -> _np.ndarray:
1310    """
1311    行列を直交化し、行列式の符号を維持します。
1312
1313    概要:
1314        入力行列を最も近い直交行列に変換し、元の行列式の符号が正であれば新しい行列式も正に、負であれば負になるように調整します。
1315    詳細説明:
1316        特異値分解 (SVD) を使用して行列を直交化します。
1317        もし直交化後の行列式が元の符号と異なる場合、SVDのU行列の最後の列の符号を反転させて調整します。
1318    :param R: 直交化する行列。
1319    :type R: numpy.ndarray
1320    :returns: 直交化され、行列式の符号が維持された行列。
1321    :rtype: numpy.ndarray
1322    """
1323    U,_,Vt = _np.linalg.svd(R); R_=U@Vt
1324    if _np.linalg.det(R_)<0: U[:,-1]*=-1; R_=U@Vt
1325    return R_
1326
1327def _rot_axis(R: _np.ndarray) -> _Optional[_np.ndarray]:
1328    """
1329    回転行列の回転軸を抽出します。
1330
1331    概要:
1332        与えられた回転行列の固定軸(回転軸)を実固有ベクトルとして特定し、正規化されたベクトルを返します。
1333    詳細説明:
1334        回転行列の固有値を計算し、固有値1に対応する実固有ベクトルを回転軸と見なします。
1335        もし固有値1に対応する固有ベクトルが見つからない場合や、実数部が1に近似しない場合はNoneを返します。
1336    :param R: 回転行列。
1337    :type R: numpy.ndarray
1338    :returns: 回転軸を示す単位ベクトル、またはNone。
1339    :rtype: Optional[numpy.ndarray]
1340    """
1341    vals,vecs=_np.linalg.eig(R); idx=_np.argmax(_np.real(vals))
1342    if _np.isclose(vals[idx].real,1.0,atol=1e-5):
1343        return _unit(_np.real(vecs[:,idx]))
1344    return None
1345
1346def _angle_from_trace(R: _np.ndarray) -> float:
1347    """
1348    回転行列のトレースから回転角度を計算します。
1349
1350    概要:
1351        回転行列のトレース情報を使用して、その回転が表す角度(ラジアン)を計算します。
1352    詳細説明:
1353        回転行列Rのトレース `tr(R)` は `1 + 2*cos(theta)` の関係があります。
1354        この式を逆算して `theta = arccos((tr(R)-1)/2)` を求めます。
1355        数値誤差を考慮して `(tr(R)-1)/2` の値は [-1, 1] にクリップされます。
1356    :param R: 回転行列。
1357    :type R: numpy.ndarray
1358    :returns: 回転角度(ラジアン)。
1359    :rtype: float
1360    """
1361    return _np.arccos(float(_np.clip((_np.trace(R)-1)/2,-1,1)))
1362
1363def classify_op_for_table(pg: str, R: _np.ndarray, tol: float = 1e-6) -> str:
1364    """
1365    回転行列を、指定された点群の指標表中のクラス名に対応づけて分類します。
1366
1367    概要:
1368        与えられた対称操作行列を、点群のコンテキストに基づいて標準的なクラスラベル(例: "C2", "C2(x)", "σh")に分類します。
1369    詳細説明:
1370        まず行列を直交化し、行列式を±1に修正します。
1371        その後、行列式、トレース、固有ベクトル、そして特定の点群(例: D2h, D4h)の軸の方向に基づいて操作を分類します。
1372        例えば、C2操作は点群によって "C2(x)", "C2(y)", "C2(z)"、または単に "C2" とラベル付けされます。
1373    :param pg: 点群シンボル (例: "C2v", "D2h")。
1374    :type pg: str
1375    :param R: 分類する3x3の対称操作行列。
1376    :type R: numpy.ndarray
1377    :param tol: 浮動小数点比較の許容誤差。
1378    :type tol: float
1379    :returns: 指標表のクラス名に対応する文字列ラベル。
1380    :rtype: str
1381    """
1382    R = _orth_keep_det(R); det=float(_np.linalg.det(R))
1383    if _np.allclose(R, _np.eye(3), atol=tol): return "E"
1384    if _np.allclose(R, -_np.eye(3), atol=tol): return "i"
1385    # mirrors
1386    if det<0 and _np.allclose(R@R, _np.eye(3), atol=1e-6):
1387        vals,vecs=_np.linalg.eig(R)
1388        nrm=_unit(_np.real(vecs[:, _np.argmin(_np.real(vals)) ]))
1389        nz=abs(nrm[2])
1390        if nz>1-1e-3: return "σh"
1391        vscore=max(abs(nrm@_unit(v)) for v in [_X,_Y,-_X,-_Y])
1392        dscore=max(abs(nrm@_unit(v)) for v in _XY_DIAGS)
1393        if vscore>=dscore: return "σv"
1394        else: return "σd"
1395    # proper
1396    if det>0:
1397        th=_angle_from_trace(R); 
1398        if th<1e-6: return "E"
1399        n=int(round(2*_np.pi/th))
1400        ax=_rot_axis(R)
1401        if n==2:
1402            if pg in ("D2","D2h"):
1403                if _close_to(ax,[_X,-_X]): return "C2(x)"
1404                if _close_to(ax,[_Y,-_Y]): return "C2(y)"
1405                if _close_to(ax,[_Z,-_Z]): return "C2(z)"
1406            if pg in ("C2h",) and _close_to(ax,[_Z,-_Z]): return "C2(z)"
1407            if pg in ("D4","D4h","D2d","C4h","C4v","Oh","O","D6","D6h"):
1408                if _close_to(ax,[_Z,-_Z]): return "C2(z)"
1409                if _close_to(ax,[_X,-_X,_Y,-_Y]): return "C2'"
1410                if _close_to(ax,_XY_DIAGS): return "C2''"
1411                if pg in ("O","Oh"):
1412                    if _close_to(ax,[_X,-_X,_Y,-_Y,_Z,-_Z]): return "C2(ax)"
1413                    if _close_to(ax,_C2_DIAGS3D): return "C2(di)"
1414            return "C2"
1415        if n==3:
1416            return "C3" if th<=_np.pi/1.5 + 1e-3 else "C3^2" # 120deg vs 240deg
1417        if n==4:
1418            return "C4" if R[0,1]<0 else "C4^3" # R[0,1]<0 for 90 deg rotation around z, if start from [1,0,0] -> [0,1,0]
1419        if n==5:
1420            if _np.isclose(th,2*_np.pi/5,atol=1e-3): return "C5"
1421            if _np.isclose(th,4*_np.pi/5,atol=1e-3): return "C5^2"
1422            return "C5"
1423        if n==6:
1424            if _np.isclose(th,_np.pi/3,atol=1e-3): return "C6"
1425            # C3 means 2pi/3. C3^2 means 4pi/3. If n=6, 2pi/6=pi/3 (C6), 4pi/6=2pi/3 (C3), 6pi/6=pi (C2), 8pi/6=4pi/3 (C3^2), 10pi/6=5pi/3 (C6^5)
1426            # The current logic using angle_from_trace and 2pi/th to determine 'n' will likely map 2pi/3 to n=3, 4pi/3 to n=3.
1427            # This needs careful handling for C6.
1428            # For simplicity, if _angle_from_trace returns an angle associated with C6, we label it as C6.
1429            # If the angle is 2pi/3 (C3 equivalent within C6), it would be caught by n=3.
1430            # For this context, C6 should be specific to 60 deg rotation.
1431            return "C6"
1432        return f"C{n}"
1433    # improper
1434    if det<0:
1435        A=-R; th=_angle_from_trace(A); n=int(round(2*_np.pi/th))
1436        if n==4: return "S4" if A[0,1]<0 else "S4^3"
1437        if n==6: return "S6"
1438        if n==3: return "S3"
1439        if n==10:
1440            if _np.isclose(th,2*_np.pi/5,atol=5e-3): return "S10"
1441            if _np.isclose(th,4*_np.pi/5,atol=5e-3): return "S10^3"
1442            return "S10"
1443        return f"S{n}"
1444    return "?"
1445
1446# ---------- クラス集約・Γ_3N/Γ_T/Γ_R ----------
1447def class_aggregation(pg: str, raw_labels: _List[str]) -> _Tuple[_List[str], _Dict[str,str]]:
1448    """
1449    生の操作ラベルを、指定された点群の指標表のクラス名に集約します。
1450
1451    概要:
1452        `classify_op_for_table` によって生成された詳細な操作ラベルを、
1453        点群の指標表で定義されているクラス名にマッピングし、集約します。
1454    詳細説明:
1455        点群の指標表からクラスのリストを取得し、入力された生のラベルを
1456        これらのクラス名に対応付ける辞書を構築します。
1457        例えば、"σ_v(φ=0)" や "σ_v(φ=90)" のような複数のσv面が単一の "σv" クラスに集約されます。
1458    :param pg: 点群シンボル。
1459    :type pg: str
1460    :param raw_labels: 各対称操作に対する生の文字列ラベルのリスト。
1461    :type raw_labels: List[str]
1462    :returns: (指標表のクラス名のリスト, 生ラベルからクラス名へのマッピング辞書) のタプル。
1463    :rtype: Tuple[List[str], Dict[str, str]]
1464    """
1465    classes = character_table(pg)["classes"]
1466    setc=set(classes); cmap={}
1467    for lab in raw_labels:
1468        if lab in setc: cmap[lab]=lab; continue
1469        if lab=="C2" and "C2(z)" in setc: cmap[lab]="C2(z)"; continue
1470        if lab.startswith("σv") and "σv" in setc: cmap[lab]="σv"; continue
1471        if lab.startswith("σd") and "σd" in setc: cmap[lab]="σd"; continue
1472        if lab in ("C2'", "C2''") and lab in setc: cmap[lab]=lab; continue
1473        if lab=="C2" and "C2'" in setc: cmap[lab]="C2'"; continue
1474        if lab in ("C2(ax)","C2(di)") and lab in setc: cmap[lab]=lab; continue
1475        if lab.startswith("C4") and "C4" in setc and "C4^3" not in setc: cmap[lab]="C4"; continue
1476        if lab.startswith("C3") and "C3" in setc: cmap[lab]="C3"; continue
1477        if lab.startswith("S3") and "S6" in setc and "S3" not in setc: cmap[lab]="S6"; continue
1478        if lab in ("S10","S10^3") and lab in setc: cmap[lab]=lab; continue
1479        if lab.startswith("S4") and "S4" in setc and "S4^3" not in setc: cmap[lab]="S4"; continue
1480        if lab.startswith("σ") and "σ" in setc: cmap[lab]="σ"; continue
1481        cmap[lab]=lab
1482    return classes, cmap
1483
1484def _parse_power(label: str, default_n: _Optional[int] = None) -> _Tuple[str, _Optional[int], int]:
1485    """
1486    操作ラベルから回転軸の次数と指数を解析します。
1487
1488    概要:
1489        "C3^2" のような操作ラベルを解析し、操作の種類 ('C'/'S')、次数 (3)、指数 (2) を抽出します。
1490    詳細説明:
1491        ラベルに "^" が含まれる場合、ベースと指数に分割します。
1492        ベース部分から操作の種類(C, Sなど)と次数を抽出します。
1493        次数が数値でない場合は `default_n` を使用します。
1494    :param label: 解析する操作ラベル。
1495    :type label: str
1496    :param default_n: 次数が解析できない場合のデフォルト値。
1497    :type default_n: Optional[int]
1498    :returns: (操作の種類, 次数, 指数) のタプル。
1499    :rtype: Tuple[str, Optional[int], int]
1500    """
1501    if "^" in label:
1502        base,p = label.split("^",1); p=int(p)
1503    else:
1504        base,p = label,1
1505    kind=base[0]
1506    try: n=int(base[1:])
1507    except ValueError: n=default_n
1508    return kind,n,p
1509
1510def polar_trace_from_label(lab: str) -> float:
1511    """
1512    操作ラベルから、3次元の直交変換行列のトレースを計算します。
1513
1514    概要:
1515        与えられた対称操作のラベルに基づいて、その操作の3x3行列のトレース(対角成分の和)を返します。
1516    詳細説明:
1517        'E' は3、'i' は-3。
1518        鏡映操作 ('σ') は1。
1519        回転操作 ('C') は `1 + 2*cos(theta)` を計算します。
1520        回映操作 ('S') は `2*cos(theta) - 1` を計算します。
1521        角度 `theta` はラベルから解析される次数 `n` と指数 `p` に基づきます (`2*pi*p/n`)。
1522    :param lab: 対称操作のラベル。
1523    :type lab: str
1524    :returns: その操作の3x3行列のトレース。
1525    :rtype: float
1526    """
1527    if lab=="E": return 3.0
1528    if lab=="i": return -3.0
1529    if lab.startswith("σ"): return 1.0
1530    if lab.startswith("C"):
1531        _,n,p=_parse_power(lab); 
1532        if n is None: return 0.0 # Should not happen for valid C_n
1533        ang=2*_np.pi*(p%n)/n
1534        return 1.0 + 2.0*_np.cos(ang)
1535    if lab.startswith("S"):
1536        _,n,p=_parse_power(lab); 
1537        if n is None: return 0.0 # Should not happen for valid S_n
1538        ang=2*_np.pi*(p%n)/n
1539        return 2.0*_np.cos(ang) - 1.0
1540    return 0.0
1541
1542def det_from_label(lab: str) -> int:
1543    """
1544    操作ラベルから、その操作の行列式を返します。
1545
1546    概要:
1547        与えられた対称操作のラベルに基づいて、その操作の行列式 (1または-1) を返します。
1548    詳細説明:
1549        'E' や 'C' (純粋回転) 操作は行列式が1です。
1550        'i' (反転)、'σ' (鏡映)、'S' (回映) 操作は行列式が-1です。
1551    :param lab: 対称操作のラベル。
1552    :type lab: str
1553    :returns: 行列式 (1または-1)。
1554    :rtype: int
1555    """
1556    if lab=="E": return 1
1557    if lab.startswith("C"): return 1
1558    return -1  # i, σ*, S*
1559
1560def gamma_3N_characters(coords: _np.ndarray, op_mats: _List[_np.ndarray],
1561                        labels: _List[str], tol: float = 1e-5) -> _Dict[str, float]:
1562    """
1563    分子の全並進振動モード (Γ_3N) の指標を計算します。
1564
1565    概要:
1566        与えられた原子座標、対称操作行列、およびそれらのラベルを使用して、
1567        分子の全振動モードの可約表現の指標を計算します。
1568    詳細説明:
1569        各対称操作Rに対して、その操作によって移動しない原子を特定します。
1570        移動しない原子については、Rのトレースが指標に貢献します。
1571        全ての操作についてこれを合計し、各ラベルに対応する総指標を算出します。
1572    :param coords: 原子座標のNumPy配列 (N, 3)。
1573    :type coords: numpy.ndarray
1574    :param op_mats: 対称操作行列のリスト。
1575    :type op_mats: List[numpy.ndarray]
1576    :param labels: 各操作行列に対応するラベルのリスト。
1577    :type labels: List[str]
1578    :param tol: 原子位置の比較に用いる許容誤差。
1579    :type tol: float
1580    :returns: 各操作ラベルに対するΓ_3Nの指標値の辞書。
1581    :rtype: Dict[str, float]
1582    """
1583    chars=_defaultdict(float)
1584    for R,lab in zip(op_mats, labels):
1585        chi=0.0
1586        for r in coords:
1587            rp = R @ r
1588            if _np.linalg.norm(rp - r) < tol:
1589                chi += float(_np.trace(R))
1590        chars[lab]+=chi
1591    return dict(chars)
1592
1593def gamma_trans_characters(labels: _List[str], class_map: _Dict[str,str]) -> _Dict[str,float]:
1594    """
1595    分子の並進振動モード (Γ_T) の指標を計算します。
1596
1597    概要:
1598        対称操作のラベルとクラスマップを使用して、分子の並進モードの可約表現の指標を計算します。
1599    詳細説明:
1600        各操作ラベルについて、その操作の3x3行列のトレースを `polar_trace_from_label` を用いて計算します。
1601        これらのトレース値をクラスマップに従って集約し、各クラスのΓ_T指標を求めます。
1602    :param labels: 各対称操作のラベルのリスト。
1603    :type labels: List[str]
1604    :param class_map: 生ラベルから指標表クラス名へのマッピング辞書。
1605    :type class_map: Dict[str, str]
1606    :returns: 各クラスに対するΓ_Tの指標値の辞書。
1607    :rtype: Dict[str, float]
1608    """
1609    d=_defaultdict(float)
1610    for lab in labels:
1611        key=class_map.get(lab,lab)
1612        d[key]+=polar_trace_from_label(lab)
1613    return dict(d)
1614
1615def gamma_rot_characters(labels: _List[str], class_map: _Dict[str,str]) -> _Dict[str,float]:
1616    """
1617    分子の回転振動モード (Γ_R) の指標を計算します。
1618
1619    概要:
1620        対称操作のラベルとクラスマップを使用して、分子の回転モードの可約表現の指標を計算します。
1621    詳細説明:
1622        各操作ラベルについて、その操作の3x3行列のトレースにその行列式 (`det_from_label`) を乗じた値を計算します。
1623        これは、回転操作 (det=1) にはトレースそのままを、回転-反転操作 (det=-1) にはトレースに-1を乗じた値を意味します。
1624        これらの値をクラスマップに従って集約し、各クラスのΓ_R指標を求めます。
1625    :param labels: 各対称操作のラベルのリスト。
1626    :type labels: List[str]
1627    :param class_map: 生ラベルから指標表クラス名へのマッピング辞書。
1628    :type class_map: Dict[str, str]
1629    :returns: 各クラスに対するΓ_Rの指標値の辞書。
1630    :rtype: Dict[str, float]
1631    """
1632    d=_defaultdict(float)
1633    for lab in labels:
1634        key=class_map.get(lab,lab)
1635        d[key]+= det_from_label(lab) * polar_trace_from_label(lab)
1636    return dict(d)
1637
1638def reduce_to_classes(vec_by_label: _Dict[str,float], classes: _List[str], class_map: _Dict[str,str]) -> _Dict[str,float]:
1639    """
1640    ラベルごとの指標ベクトルを指標表のクラスに集約します。
1641
1642    概要:
1643        詳細な操作ラベルに対応する指標値を、点群の指標表で定義されたクラス名に対応する指標値に集約します。
1644    詳細説明:
1645        `class_map` を使用して、各ラベルの指標値を対応するクラスに加算します。
1646        最終結果は、指標表の全てのクラス名を含む辞書として返され、対応する指標値がないクラスには0が割り当てられます。
1647    :param vec_by_label: 各操作ラベルに対する指標値の辞書。
1648    :type vec_by_label: Dict[str, float]
1649    :param classes: 指標表で定義されたクラス名のリスト。
1650    :type classes: List[str]
1651    :param class_map: 生ラベルから指標表クラス名へのマッピング辞書。
1652    :type class_map: Dict[str, str]
1653    :returns: 各クラスに対する集約された指標値の辞書。
1654    :rtype: Dict[str, float]
1655    """
1656    agg=_defaultdict(float)
1657    for lab,val in vec_by_label.items():
1658        key=class_map.get(lab,lab)
1659        agg[key]+=val
1660    return {c:agg.get(c,0.0) for c in classes}
1661
1662def group_order(symbol: str) -> int:
1663    """
1664    点群の位数を返します。
1665
1666    概要:
1667        指定された点群シンボルに対応する群の位数(要素数)を返します。
1668    詳細説明:
1669        `build_group` で構築可能な群については、実際に構築された操作の数を返します。
1670        `ABSTRACT_CLASS_SIZES` に定義されている抽象群については、その合計クラスサイズを返します。
1671        どちらにも対応しない場合は `ValueError` を発生させます。
1672    :param symbol: 点群シンボル。
1673    :type symbol: str
1674    :returns: 点群の位数。
1675    :rtype: int
1676    :raises ValueError: サポートされていない、または抽象定義されていない点群シンボルが指定された場合。
1677    """
1678    s=normalize_symbol(symbol)
1679    try:
1680        return len(build_group(s))
1681    except Exception:
1682        if s in ABSTRACT_CLASS_SIZES:
1683            return sum(ABSTRACT_CLASS_SIZES[s].values())
1684        raise ValueError(f"Group order for '{symbol}' is not available.")
1685
1686def group_ops(symbol: str) -> _List[_np.ndarray]:
1687    """
1688    点群の全ての対称操作(3x3行列)を返します。
1689
1690    概要:
1691        指定された点群シンボルに対応する全ての対称操作行列のリストを返します。
1692    詳細説明:
1693        `build_group` 関数を呼び出して操作行列を構築し、`snap_matrix` で正規化します。
1694        `build_group` で対応していない抽象群の場合は、`ValueError` が発生します。
1695    :param symbol: 点群シンボル。
1696    :type symbol: str
1697    :returns: 点群の全ての対称操作行列のリスト。
1698    :rtype: List[numpy.ndarray]
1699    :raises ValueError: 抽象群など、`build_group` で操作行列を構築できない場合。
1700    """
1701    s=normalize_symbol(symbol)
1702    mats = build_group(s)  # 例外で抽象群へ
1703    return [snap_matrix(M) for M in mats]
1704
1705def decompose_irreps(chars_by_class: _Dict[str,float], pg: str,
1706                     class_sizes_override: _Optional[_Dict[str,int]]=None,
1707                     order_override: _Optional[int]=None) -> _Dict[str,int]:
1708    """
1709    クラスごとの指標ベクトルを既約表現へ直交分解します。
1710
1711    概要:
1712        与えられた可約表現の指標ベクトルを、指定された点群の既約表現に分解し、
1713        各既約表現の多重度を計算します。
1714    詳細説明:
1715        群論の直交性定理 (`m_i = (1/h) * sum(chi_reducible(R) * chi_irreducible(R)* * N_R)`) を使用します。
1716        `h` は群の位数、`N_R` はクラスRの要素数、`chi(R)` はクラスRの指標です。
1717        クラスサイズと群の位数は、`class_sizes_override` や `order_override` で明示的に指定できます。
1718        指定がない場合、`group_order` と `classify_op_for_table` を用いて自動的に計算されます。
1719    :param chars_by_class: 各クラスに対する可約表現の指標値の辞書。
1720    :type chars_by_class: Dict[str, float]
1721    :param pg: 点群シンボル。
1722    :type pg: str
1723    :param class_sizes_override: クラスサイズを明示的に指定する辞書(クラス名: サイズ)。
1724    :type class_sizes_override: Optional[Dict[str, int]]
1725    :param order_override: 群の位数を明示的に指定する整数。
1726    :type order_override: Optional[int]
1727    :returns: 各既約表現名とその多重度(整数)の辞書。
1728    :rtype: Dict[str, int]
1729    :raises ValueError: 指定された点群の指標表が利用できない場合、または群の位数を特定できない場合。
1730    """
1731    tbl = character_table(pg); irreps = tbl["irreps"]
1732    if class_sizes_override is not None:
1733        class_sizes = _Counter(class_sizes_override)
1734        h = order_override if order_override is not None else sum(class_sizes.values())
1735    else:
1736        h = group_order(pg)
1737        ops = group_ops(pg)
1738        raw_labels = [classify_op_for_table(pg, R) for R in ops]
1739        _, cmap = class_aggregation(pg, raw_labels)
1740        class_sizes = _Counter(cmap.get(l,l) for l in raw_labels)
1741    mults={}
1742    for ir,row in irreps.items():
1743        s=0.0+0.0j
1744        for cl,chi_ir in row.items():
1745            s += class_sizes.get(cl,0) * _np.conj(chi_ir) * chars_by_class.get(cl,0.0)
1746        m = s / h
1747        mults[ir] = int(round(float(_np.real(m))))
1748    return mults
1749
1750def pretty_irreps(pg: str, mults: _Dict[str,int]) -> str:
1751    """
1752    既約表現の多重度を見やすい和の形式で整形します。
1753
1754    概要:
1755        既約表現の多重度を示す辞書を受け取り、それを "A1 + 2E + T2" のような
1756        人間が読みやすい文字列形式に変換します。
1757    詳細説明:
1758        一般的な点群の既約表現には標準的な並び順があります。
1759        この関数では、`order_map` を使用して、指定された点群の既約表現を
1760        その慣習的な順序で並べ替えます。
1761        多重度が1より大きい場合は `2E` のように係数を付け、1の場合は `A1` のように表現名を直接使用します。
1762        多重度が0の表現は省略されます。
1763    :param pg: 点群シンボル。
1764    :type pg: str
1765    :param mults: 各既約表現名とその多重度(整数)の辞書。
1766    :type mults: Dict[str, int]
1767    :returns: 整形された既約表現の和の文字列。
1768    :rtype: str
1769    """
1770    # 代表的な並び(見栄え用、未知は辞書順)
1771    order_map = {
1772        "C2v": ["A1","A2","B1","B2"],
1773        "C3v": ["A1","A2","E"],
1774        "C4v": ["A1","A2","B1","B2","E"],
1775        "C6v": ["A1","A2","B1","B2","E1","E2"],
1776        "C1": ["A"], "Ci":["Ag","Au"], "Cs":["A'","A''"], "Ch":["A'","A''"],
1777        "C2":["A","B"], "C3":["A","E"], "C4":["A","B","E"], "C6":["A","B","E1","E2"],
1778        "C2h":["Ag","Bg","Au","Bu"],
1779        "C3h":["A'","A''","E'","E''"],
1780        "C4h":[f{a}g" for a in (0,1,2,3)] + [f{a}u" for a in (0,1,2,3)],
1781        "D2":["A","B1","B2","B3"],
1782        "D2h":["Ag","B1g","B2g","B3g","Au","B1u","B2u","B3u"],
1783        "D3":["A1","A2","E"], "D3d":["A1g","A2g","Eg","A1u","A2u","Eu"], "D3h":["A1'","A2'","E'","A1''","A2''","E''"],
1784        "D4":["A1","A2","B1","B2","E"], "D2d":["A1","A2","B1","B2","E"], "D4h":["A1g","A2g","B1g","B2g","Eg","A1u","A2u","B1u","B2u","Eu"],
1785        "D6":["A1","A2","B1","B2","E1","E2"], "D6h":["A1g","A2g","B1g","B2g","E1g","E2g","A1u","A2u","B1u","B2u","E1u","E2u"],
1786        "T":["A","E","T"], "Td":["A1","A2","E","T1","T2"], "Th":["Ag","Au","Eg","Eu","Tg","Tu"],
1787        "O":["A1","A2","E","T1","T2"], "Oh":["A1g","A2g","Eg","T1g","T2g","A1u","A2u","Eu","T1u","T2u"],
1788        "I":["A","T1","T2","G","H"],
1789        "Ih":["Ag","T1g","T2g","Gg","Hg","Au","T1u","T2u","Gu","Hu"],
1790    }
1791    seq = order_map.get(normalize_symbol(pg), sorted(character_table(pg)["irreps"].keys()))
1792    parts=[]
1793    for ir in seq:
1794        m=mults.get(ir,0)
1795        if m>0: parts.append(f"{m}{ir}" if m>1 else ir)
1796    return " + ".join(parts) if parts else "0"