#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
tkpointgroup.py
  - Pure NumPy point-group utilities extracted from point_group_inf.py

このモジュールは、NumPyを使用して点群に関連する様々なユーティリティを提供します。
主な機能として、Herman–Mauguin記号とSchoenflies記号の相互変換、群記号からの生成元と全要素の取得、
生成元からの閉包計算、点の軌道や独立代表点の取得、および回転、鏡映、反転などの基本的な幾何学操作が含まれます。
また、点群の指標表、クラスへの分類、振動モードの既約表現分解など、群論解析のための機能も提供します。

:doc:`tkpointgroup_usage`

提供機能（再利用向けAPI）:
    * Herman–Mauguin (国際) と Schoenflies の相互変換
    * 群記号から generator と全要素（重複なし）を取得
    * generator から閉包を取り、全要素のラベルと行列を取得
    * 点の軌道・独立代表点（重複削除）を取得
    * 各種ユーティリティ（回転・鏡映・反転、スナップ、直交化など）
    * 点群の指標表の取得と操作の分類
    * 振動モードの既約表現分解

依存: numpy
"""

from __future__ import annotations
import math
import numpy as np
from typing import List, Tuple, Dict

# ========= 基本ユーティリティ =========

def _norm(v: np.ndarray) -> np.ndarray:
    """
    ベクトルを正規化します。

    概要:
        入力ベクトルを正規化し、単位ベクトルを返します。
    詳細説明:
        ベクトルのL2ノルムが0の場合はValueErrorを発生させます。
    :param v: 正規化する入力ベクトル。
    :type v: numpy.ndarray
    :returns: 正規化された単位ベクトル。
    :rtype: numpy.ndarray
    :raises ValueError: 入力ベクトルの長さがゼロの場合。
    """
    v = np.asarray(v, dtype=float)
    n = np.linalg.norm(v)
    if n == 0:
        raise ValueError("Zero-length vector")
    return v / n

def rot(axis: np.ndarray, angle_deg: float) -> np.ndarray:
    """
    指定された軸を中心とした回転行列を生成します。

    概要:
        与えられた軸と角度に基づいて3x3の回転行列を計算します。
    詳細説明:
        ロドリゲスの回転公式を使用して回転行列を構築します。
        角度は度数で指定します。
    :param axis: 回転軸を示すベクトル。
    :type axis: numpy.ndarray
    :param angle_deg: 回転角度（度数）。
    :type angle_deg: float
    :returns: 3x3の回転行列。
    :rtype: numpy.ndarray
    """
    a = _norm(axis)
    th = math.radians(angle_deg)
    c, s = math.cos(th), math.sin(th)
    x, y, z = a
    R = np.array([
        [c + x*x*(1-c),     x*y*(1-c) - z*s, x*z*(1-c) + y*s],
        [y*x*(1-c) + z*s,   c + y*y*(1-c),   y*z*(1-c) - x*s],
        [z*x*(1-c) - y*s,   z*y*(1-c) + x*s, c + z*z*(1-c)]
    ], dtype=float)
    return R

def mirror(normal: np.ndarray) -> np.ndarray:
    """
    指定された法線ベクトルを持つ平面での鏡映行列を生成します。

    概要:
        与えられた法線ベクトルに垂直な平面に関する3x3の鏡映行列を計算します。
    詳細説明:
        鏡映行列は I - 2*(n @ n.T) で計算されます。ここで I は単位行列、n は正規化された法線ベクトルです。
    :param normal: 鏡映平面の法線ベクトル。
    :type normal: numpy.ndarray
    :returns: 3x3の鏡映行列。
    :rtype: numpy.ndarray
    """
    n = _norm(normal).reshape(3,1)
    return np.eye(3) - 2.0*(n @ n.T)

def inversion() -> np.ndarray:
    """
    反転操作の行列を生成します。

    概要:
        原点に対する反転操作を表す3x3の行列を返します。
    詳細説明:
        これは -I（マイナス単位行列）と等価です。
    :returns: 3x3の反転行列。
    :rtype: numpy.ndarray
    """
    return -np.eye(3)

_SNAP_VALUES = [
    0.0, 0.5, -0.5,
    1.0/math.sqrt(2), -1.0/math.sqrt(2),
    math.sqrt(3)/2, -math.sqrt(3)/2,
    1.0, -1.0
]
def _closest(x: float) -> float:
    """
    与えられた浮動小数点数を、定義済みの代表値リストから最も近い値にスナップします。

    概要:
        浮動小数点数を物理的に意味のある代表値（0, 0.5, 1/√2, √3/2, 1など）に丸めます。
    詳細説明:
        `_SNAP_VALUES` リスト内の値と入力値との絶対差が最小となる値を返します。
        スナップの閾値は5e-8です。
    :param x: スナップする浮動小数点数。
    :type x: float
    :returns: 最も近い代表値。
    :rtype: float
    """
    return min(_SNAP_VALUES, key=lambda v: abs(v - x))

def snap_matrix(M: np.ndarray) -> np.ndarray:
    """
    行列要素を代表値へスナップし、直交性/ det=±1 を保証します。

    概要:
        入力された3x3行列の各要素を事前に定義された代表的な浮動小数点数にスナップし、
        さらにその行列が直交性を満たし、かつ行列式が±1であることを保証します。
    詳細説明:
        まず、各要素を `_closest` 関数でスナップします。
        次に、スナップ後の行列が厳密に直交行列でない場合、SVD (特異値分解) を用いて
        最も近い直交行列に変換し、再度要素をスナップします。
        最後に、行列式が±1でない場合、SVDを用いて行列式を±1に修正します。
        これにより、数値誤差によって生じるわずかなずれを修正し、物理的に正しい対称操作行列を保証します。
    :param M: スナップ処理を行う3x3の浮動小数点行列。
    :type M: numpy.ndarray
    :returns: スナップされ、直交性および行列式の条件を満たす3x3行列。
    :rtype: numpy.ndarray
    """
    X = np.array([[ (_closest(v) if abs(_closest(v)-v) < 5e-8 else v)
                    for v in row ] for row in M ], dtype=float)
    if not np.allclose(X.T @ X, np.eye(3), atol=1e-6):
        U, _, Vt = np.linalg.svd(X)
        X = U @ Vt
        X = np.array([[ (_closest(v) if abs(_closest(v)-v) < 5e-8 else v)
                        for v in row ] for row in X ], dtype=float)
    d = np.linalg.det(X)
    if not (abs(abs(d)-1.0) < 1e-6):
        U, _, Vt = np.linalg.svd(X)
        sign = 1.0 if d >= 0 else -1.0
        X = U @ np.diag([1,1,sign]) @ Vt
    return X

def mat_key(M: np.ndarray) -> tuple:
    """
    行列の一意な識別キーを生成します。

    概要:
        NumPy配列を行列の同一性チェックに使用できるハッシュ可能なタプルに変換します。
    詳細説明:
        行列を1次元に平坦化し、各要素を小数点以下10桁で丸めてタプルに変換します。
        これにより、浮動小数点誤差に強いキーとして、辞書のキーや集合の要素として利用可能になります。
    :param M: キーを生成する入力行列。
    :type M: numpy.ndarray
    :returns: 行列の要素を丸めたタプルのキー。
    :rtype: tuple
    """
    return tuple(np.round(np.asarray(M, float).flatten(), 10))

def unique_closure(generators: List[np.ndarray]) -> List[np.ndarray]:
    """
    生成元のリストから有限群の閉包を構成します（重複なし）。

    概要:
        与えられた生成元行列のセットから、それらによって生成される全てのユニークな群要素（行列）を計算します。
    詳細説明:
        生成元と既存の群要素の積を計算し、新しいユニークな要素が見つかるたびにキューに追加していく
        幅優先探索（またはダイクストラ法に似た）アプローチを使用します。
        全ての要素は `snap_matrix` で正規化され、`mat_key` で重複がチェックされます。
        入力が空の場合、単位行列のみを含むリストを返します。
    :param generators: 群の生成元であるNumPy行列のリスト。
    :type generators: List[numpy.ndarray]
    :returns: 生成元から構成される点群の全てのユニークな要素行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    gens = [snap_matrix(G) for G in generators]
    if not gens:
        return [np.eye(3)]
    els: Dict[tuple, np.ndarray] = {}
    queue: List[np.ndarray] = []
    def add(E):
        K = mat_key(E)
        if K not in els:
            els[K] = E
            queue.append(E)
    add(np.eye(3))
    for G in gens:
        add(G)
    while queue:
        A = queue.pop()
        for B in list(els.values()):
            add(snap_matrix(A @ B))
            add(snap_matrix(B @ A))
    return list(els.values())

# 標準軸・面
ex, ey, ez = np.array([1,0,0.]), np.array([0,1,0.]), np.array([0,0,1.])
def vertical_plane(phi_deg: float) -> np.ndarray:
    """
    z軸に垂直な平面（xz平面またはyz平面を含む）に垂直な、xy平面内の法線ベクトルを生成します。

    概要:
        与えられた角度を持つ垂直面の法線ベクトル（xy平面内）を計算します。
    詳細説明:
        phi_deg が 0 の場合、y軸に沿ったベクトル ([0, -1, 0]) となり、これはxz平面 (y=0) に垂直です。
        phi_deg が 90 の場合、x軸に沿ったベクトル ([-1, 0, 0]) となり、これはyz平面 (x=0) に垂直です。
    :param phi_deg: xy平面内での角度（度数）。
    :type phi_deg: float
    :returns: 垂直面の法線ベクトル。
    :rtype: numpy.ndarray
    """
    φ = math.radians(phi_deg)
    return np.array([math.sin(φ), -math.cos(φ), 0.0])

def diagonal_plane(phi_deg: float) -> np.ndarray:
    """
    対角面（z軸に対して45度の角度を持つ面）の法線ベクトルを生成します。

    概要:
        指定された角度に対する対角面の法線ベクトルを計算します。
    詳細説明:
        `vertical_plane` 関数を呼び出し、角度に90度を加えることで、
        xy平面内で45度の傾きを持つ（例: (1,1,0) や (1,-1,0) 方向を法線に持つ）面を表現します。
    :param phi_deg: xy平面内での角度（度数）。
    :type phi_deg: float
    :returns: 対角面の法線ベクトル。
    :rtype: numpy.ndarray
    """
    return vertical_plane(phi_deg + 90.0)

# ========= 群ビルダー =========

def build_Cn(n: int) -> List[np.ndarray]:
    """
    n回軸 (Cn) 点群の全ての対称操作行列を構築します。

    概要:
        n回回転軸のみから構成される点群の全要素行列を返します。
    詳細説明:
        z軸周りの360/n度の回転を生成元とし、それらの閉包を計算します。
    :param n: 回転軸の次数。
    :type n: int
    :returns: Cn点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([rot(ez, 360.0/n)])

def build_Cnv(n: int) -> List[np.ndarray]:
    """
    Cnv点群の全ての対称操作行列を構築します。

    概要:
        n回回転軸とそれに含まれる複数の鉛直鏡映面から構成される点群の全要素行列を返します。
    詳細説明:
        z軸周りの360/n度の回転と、z軸を含む垂直面（phi=0）での鏡映を生成元とし、それらの閉包を計算します。
    :param n: 回転軸の次数。
    :type n: int
    :returns: Cnv点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([rot(ez,360.0/n), mirror(vertical_plane(0.0))])

def build_Cnh(n: int) -> List[np.ndarray]:
    """
    Cnh点群の全ての対称操作行列を構築します。

    概要:
        n回回転軸とそれに垂直な水平鏡映面から構成される点群の全要素行列を返します。
    詳細説明:
        z軸周りの360/n度の回転と、z軸に垂直な水平面（ez）での鏡映を生成元とし、それらの閉包を計算します。
    :param n: 回転軸の次数。
    :type n: int
    :returns: Cnh点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([rot(ez,360.0/n), mirror(ez)])

def build_Sn(n: int) -> List[np.ndarray]:
    """
    Sn点群の全ての対称操作行列を構築します。

    概要:
        n回回映軸 (improper rotation axis) から構成される点群の全要素行列を返します。
    詳細説明:
        z軸に垂直な鏡映とz軸周りの360/n度の回転の積（回映操作）を生成元とし、その閉包を計算します。
    :param n: 回映軸の次数。
    :type n: int
    :returns: Sn点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([ mirror(ez) @ rot(ez,360.0/n) ])

def build_Dn(n: int) -> List[np.ndarray]:
    """
    Dn点群の全ての対称操作行列を構築します。

    概要:
        n回回転軸とそれに垂直な2回回転軸（n個）から構成される点群の全要素行列を返します。
    詳細説明:
        z軸周りの360/n度の回転と、x軸周りの180度回転を生成元とし、それらの閉包を計算します。
    :param n: 主回転軸の次数。
    :type n: int
    :returns: Dn点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([rot(ez,360.0/n), rot(ex,180.0)])

def build_Dnh(n: int) -> List[np.ndarray]:
    """
    Dnh点群の全ての対称操作行列を構築します。

    概要:
        Dn点群に水平鏡映面が加わった点群の全要素行列を返します。
    詳細説明:
        z軸周りの360/n度の回転、x軸周りの180度回転、およびz軸に垂直な水平鏡映を生成元とし、それらの閉包を計算します。
    :param n: 主回転軸の次数。
    :type n: int
    :returns: Dnh点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([rot(ez,360.0/n), rot(ex,180.0), mirror(ez)])

def build_Dnd(n: int) -> List[np.ndarray]:
    """
    Dnd点群の全ての対称操作行列を構築します。

    概要:
        Dn点群に、2回回転軸の間の対角鏡映面が加わった点群の全要素行列を返します。
    詳細説明:
        z軸周りの360/n度の回転、x軸周りの180度回転、および2回回転軸の間の対角鏡映面（phi=0）を生成元とし、それらの閉包を計算します。
    :param n: 主回転軸の次数。
    :type n: int
    :returns: Dnd点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([rot(ez,360.0/n), rot(ex,180.0), mirror(diagonal_plane(0.0))])

def build_T() -> List[np.ndarray]:
    """
    T (四面体) 点群の全ての対称操作行列を構築します。

    概要:
        正四面体の対称性を持つ点群の全要素行列を返します。
    詳細説明:
        [1,1,1]軸周りの120度回転と、x軸周りの180度回転を生成元とし、それらの閉包を計算します。
    :returns: T点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0) ])

def build_Th() -> List[np.ndarray]:
    """
    Th (全四面体) 点群の全ての対称操作行列を構築します。

    概要:
        T点群に反転中心が加わった点群の全要素行列を返します。
    詳細説明:
        T点群の生成元に反転操作を追加し、それらの閉包を計算します。
    :returns: Th点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0), inversion() ])

def build_Td() -> List[np.ndarray]:
    """
    Td (正四面体) 点群の全ての対称操作行列を構築します。

    概要:
        正四面体の対称性を持つ点群の全要素行列を返します。
    詳細説明:
        [1,1,1]軸周りの120度回転、x軸周りの180度回転、およびz軸周りのS4回映操作を生成元とし、それらの閉包を計算します。
    :returns: Td点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    S4z = mirror(ez) @ rot(ez,90.0)
    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0), S4z ])

def build_O() -> List[np.ndarray]:
    """
    O (八面体) 点群の全ての対称操作行列を構築します。

    概要:
        正八面体または立方体の対称性を持つ点群の全要素行列を返します。
    詳細説明:
        3次元空間における軸の順列と符号の組み合わせにより、立方体の回転対称操作を直接列挙します。
        行列式が1の操作のみを含めます。
    :returns: O点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    mats = []
    from itertools import permutations, product
    for perm in permutations(range(3)):
        P = np.eye(3)[list(perm)]
        for signs in product([-1,1], repeat=3):
            S = np.diag(signs)
            M = S @ P
            if round(np.linalg.det(M)) == 1:
                mats.append(snap_matrix(M))
    return list({mat_key(M): M for M in mats}.values())

def build_Oh() -> List[np.ndarray]:
    """
    Oh (全八面体) 点群の全ての対称操作行列を構築します。

    概要:
        O点群に反転中心が加わった点群の全要素行列を返します。
    詳細説明:
        O点群の全ての操作に反転操作を適用したものを加え、ユニークな要素を抽出します。
    :returns: Oh点群の対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    """
    base = build_O()
    inv = inversion()
    d = {}
    for M in base + [inv @ M for M in base]:
        d[mat_key(M)] = snap_matrix(M)
    return list(d.values())

# ========= 記号の正規化と相互変換 =========

def normalize_symbol(s: str) -> str:
    """
    点群シンボル文字列を正規化します。

    概要:
        入力された点群シンボル文字列から空白、ハイフン、アンダースコアを除去し、標準形式に変換します。
    詳細説明:
        異なる形式で入力されうるハイフン文字（U+2212, U+2013, U+2014）も標準のASCIIハイフンに統一します。
    :param s: 正規化する点群シンボル文字列。
    :type s: str
    :returns: 正規化された点群シンボル文字列。
    :rtype: str
    """
    s = s.strip().replace(" ", "")
    s = s.replace("−","-").replace("–","-").replace("—","-").replace("_","")
    return s

# 代表的な相互変換（曖昧さがあるものは代表形に丸める）
#   ※ 本ライブラリは簡易マップです。必要に応じて拡張してください。
_S_to_H = {
    "C1": "1",
    "Ci": "-1",
    "Cs": "m",
    "C2": "2",
    "C2h": "2/m",       # 追加
    "C2v": "mm2",
    "C3": "3",
    "C3h": "3/m",       # 代表的対応
    "C3v": "3m",
    "C4": "4",
    "C4h": "4/m",
    "C4v": "4mm",
    "C6": "6",
    "C6h": "6/m",
    "C6v": "6mm",
    "D2": "222",
    "D2h": "mmm",
    "D2d": "42m",       # 一般に 4̅2m だが、ここでは群操作生成は内部実装に委譲
    "D3": "32",
    "D3h": "6mm",       # 代表的に同型（操作生成は build_* に委譲）
    "D3d": "-3m",
    "D4": "422",
    "D4h": "4/mmm",
    "D4d": "-42m",
    "D6": "622",
    "D6h": "6/mmm",
    "D6d": "-6m2",
    "T": "23",
    "Th": "m-3",
    "Td": "-43m",
    "O": "432",
    "Oh": "m-3m",
}

# 逆写像（値の代表形を採用）
_H_to_S = {
    "1": "C1",
    "-1": "Ci",
    "m": "Cs",
    "2": "C2",
    "2/m": "C2h",
    "mm2": "C2v",
    "3": "C3",
    "3/m": "C3h",
    "3m": "C3v",
    "4": "C4",
    "4/m": "C4h",
    "4mm": "C4v",
    "4/mmm": "D4h",
    "6": "C6",
    "6/m": "C6h",
    "6mm": "C6v",
    "6/mmm": "D6h",
    "23": "T",
    "m-3": "Th",
    "432": "O",
    "-43m": "Td",
    "m-3m": "Oh",
    "-3": "C3i",     # 実務上の近似対応（元コードの方針を踏襲）
#    "-3": "C3h",     # 実務上の近似対応（元コードの方針を踏襲）
# 以下は build_group 側の代表入力に合わせて一部近傍対応
    "mmm": "D2h",

    "222": "D2",
    "mmm": "D2h",
    "-4":  "S4",
    "-6":  "S6",
    "422": "D4",
    "32":  "D3",
    "622": "D6",
    "-42m": "D2d",
    "-6m2": "D3h",
    "-3m": "D3d",
}

def schoenflies_to_hm(s: str) -> str:
    """
    Schoenflies点群記号をHerman–Mauguin (国際) 記号に変換します。

    概要:
        与えられたSchoenflies記号を対応するHerman–Mauguin記号に変換します。
    詳細説明:
        変換マップ `_S_to_H` を使用します。マップにない記号はそのまま返されます。
        変換前に `normalize_symbol` でシンボルを正規化します。
    :param s: Schoenflies点群記号。
    :type s: str
    :returns: 対応するHerman–Mauguin点群記号。
    :rtype: str
    """
    s = normalize_symbol(s)
    return _S_to_H.get(s, s)

def hm_to_schoenflies(h: str) -> str:
    """
    Herman–Mauguin (国際) 点群記号をSchoenflies記号に変換します。

    概要:
        与えられたHerman–Mauguin記号を対応するSchoenflies記号に変換します。
    詳細説明:
        変換マップ `_H_to_S` を使用します。マップにない記号はそのまま返されます。
        変換前に `normalize_symbol` でシンボルを正規化します。
    :param h: Herman–Mauguin点群記号。
    :type h: str
    :returns: 対応するSchoenflies点群記号。
    :rtype: str
    """
    h = normalize_symbol(h)
    return _H_to_S.get(h, h)

# ========= サポート一覧 / 構築エントリ =========

SUPPORTED = [
    # Schoenflies families
    "C1","Ci","Cs",
    "C2","C3","C4","C6",
    "C2v","C3v","C4v","C6v",
    "C2h","C3h","C4h","C6h",
    "S2","S3","S4","S6",
    "D2","D3","D4","D6",
    "D2h","D3h","D4h","D6h",
    "D2d","D3d","D4d","D6d",
    "T","Th","Td",
    "O","Oh",
    # Representative international notations (代表形を主に採用)
    "1","-1","m","2","2/m","mm2","mmm",
    "4","4/m","4mm","4/mmm",
    "3","-3","3m","3/m","-3m",
    "6","6/m","6mm","6/mmm",
    "23","m-3","432","-43m","m-3m",
]

def supported_symbols() -> List[str]:
    """
    このモジュールで構築可能な点群シンボルの一覧を返します。

    概要:
        現在サポートされている点群シンボルのリストを返します。
    詳細説明:
        Schoenflies記号と、それに対応する代表的なHerman–Mauguin記号が含まれます。
    :returns: サポートされている点群シンボルのリスト。
    :rtype: List[str]
    """
    return list(SUPPORTED)

def build_group(symbol: str) -> List[np.ndarray]:
    """
    指定された点群シンボルに対応する全ての対称操作行列を構築します。

    概要:
        点群シンボル（SchoenfliesまたはHerman–Mauguin）に基づいて、その点群の全ての対称操作行列をリストで返します。
    詳細説明:
        入力シンボルを正規化し、対応する `build_C*` や `build_D*` などの関数を呼び出します。
        サポートされていないシンボルの場合は `ValueError` を発生させます。
    :param symbol: 構築する点群のシンボル（例: "C2v", "mmm"）。
    :type symbol: str
    :returns: 点群の全ての対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    :raises ValueError: サポートされていない点群シンボルが指定された場合。
    """
    s = normalize_symbol(symbol)
    # Schoenflies
    if s in ("C1","1"):
        return [np.eye(3)]
    if s in ("Ci","-1","1bar"):
        return [np.eye(3), inversion()]
    if s in ("Cs","m"):
        return unique_closure([mirror(ez)])
    if s.startswith("C") and s.endswith("v") and s[1:-1].isdigit():
        return build_Cnv(int(s[1:-1]))
    if s.startswith("C") and s.endswith("h") and s[1:-1].isdigit():
        return build_Cnh(int(s[1:-1]))
    if s.startswith("C") and s[1:].isdigit():
        return build_Cn(int(s[1:]))
    if s.startswith("S") and s[1:].isdigit():
        return build_Sn(int(s[1:]))

    if s.startswith("D") and s.endswith("h") and s[1:-1].isdigit():
        return build_Dnh(int(s[1:-1]))
    if s.startswith("D") and s.endswith("d") and s[1:-1].isdigit():
        return build_Dnd(int(s[1:-1]))
    if s.startswith("D") and s[1:].isdigit():
        return build_Dn(int(s[1:]))

    if s == "T":  return build_T()
    if s == "Th": return build_Th()
    if s == "Td": return build_Td()
    if s == "O":  return build_O()
    if s == "Oh": return build_Oh()

    # International reps（代表入力を build_* に対応づけ）
    if s == "2":      return build_Cn(2)
    if s == "2/m":    return build_Cnh(2)
    if s == "mm2":    return build_Cnv(2)
    if s == "mmm":    return build_Dnh(2)

    if s == "4":      return build_Cn(4)
    if s == "4/m":    return build_Cnh(4)
    if s == "4mm":    return build_Cnv(4)
    if s == "4/mmm":  return build_Dnh(4)

    if s == "3":      return build_Cn(3)
    if s in ("-3","3bar"):  # 実用マップ（元コード踏襲）
        return build_Cnh(3)
    if s == "3/m":    return build_Cnh(3)      # 追加
    if s == "3m":     return build_Cnv(3)
    if s in ("-3m","D3d"):
        return build_Dnh(3)

    if s == "6":      return build_Cn(6)
    if s == "6/m":    return build_Cnh(6)
    if s == "6mm":    return build_Cnv(6)
    if s == "6/mmm":  return build_Dnh(6)

    if s == "23":     return build_T()
    if s == "m-3":    return build_Th()
    if s == "432":    return build_O()
    if s == "-43m":   return build_Td()
    if s == "m-3m":   return build_Oh()

    raise ValueError(f"Unsupported / unknown point group symbol: {symbol}")

# ========= ラベリング（簡易） =========

def classify_label(M: np.ndarray) -> str:
    """
    対称操作行列を簡易的なラベル（例: "E", "i", "σ_h", "C(z,90)"）に分類します。

    概要:
        与えられた3x3の対称操作行列を行列式とトレースに基づいて分類し、対応する文字列ラベルを返します。
    詳細説明:
        単位行列は "E"、反転行列は "i" となります。
        行列式が-1の操作で自乗すると単位行列になるものは鏡映（σ）と判断し、
        法線ベクトルの方向によって σ_h, σ_v などを割り当てます。
        行列式が1の操作は回転 (C)、行列式が-1の操作で鏡映でないものは回映 (S) と判断し、
        回転軸と角度に基づいてラベルを生成します。
    :param M: 分類する3x3の対称操作行列。
    :type M: numpy.ndarray
    :returns: 分類された操作を示す文字列ラベル。
    :rtype: str
    """
    if np.allclose(M, np.eye(3), atol=1e-8):
        return "E"
    if np.allclose(M, -np.eye(3), atol=1e-8):
        return "i"
    det = np.linalg.det(M)
    # mirror?
    if abs(det + 1.0) < 1e-6 and np.allclose(M @ M, np.eye(3), atol=1e-6):
        # 法線 = 固有値 -1 の固有ベクトル
        w, v = np.linalg.eig(M)
        idx = np.argmin(np.abs(w + 1))
        n = np.real(v[:, idx]); n = _norm(n)
        if abs(np.dot(n, ez)) > 0.9: return "σ_h"
        if abs(np.dot(n, ex)) > 0.9 or abs(np.dot(n, ey)) > 0.9: return "σ_v"
        return "σ"
    # rotation / improper
    tr = np.trace(M)
    ang = math.degrees(math.acos(max(-1.0, min(1.0, (tr-1)/2))))
    if abs(det - 1.0) < 1e-6:
        # proper rotation
        ax = np.array([M[2,1]-M[1,2], M[0,2]-M[2,0], M[1,0]-M[0,1]])
        if np.linalg.norm(ax) > 1e-8:
            a = _norm(ax)
            if abs(np.dot(a, ez)) > 0.9: axis = "z"
            elif abs(np.dot(a, ex)) > 0.9: axis = "x"
            elif abs(np.dot(a, ey)) > 0.9: axis = "y"
            elif abs(np.dot(a, _norm([1,1,1]))) > 0.9: axis = "111"
            else: axis = "u"
        else:
            axis = "u"
        return f"C({axis},{int(round(ang))})"
    else:
        ax = np.array([M[2,1]-M[1,2], M[0,2]-M[2,0], M[1,0]-M[0,1]])
        axis = "z" if np.linalg.norm(ax) > 1e-8 and abs(_norm(ax)@ez) > 0.9 else "u"
        return f"S({axis},{int(round(ang))})"

def label_elements(mats: List[np.ndarray]) -> List[Tuple[str, np.ndarray]]:
    """
    対称操作行列のリストをラベル付きのリストに変換します。

    概要:
        各対称操作行列に対して `classify_label` 関数を適用し、(ラベル, 行列) のペアのリストを生成します。
    :param mats: 対称操作行列のリスト。
    :type mats: List[numpy.ndarray]
    :returns: ラベルとそれに対応する行列のペアのリスト。
    :rtype: List[Tuple[str, numpy.ndarray]]
    """
    return [(classify_label(M), M) for M in mats]

def generators_for(symbol: str) -> List[Tuple[str, np.ndarray]]:
    """
    指定された点群シンボルに対応する規約な生成元を返します（ラベル付き）。

    概要:
        特定の点群シンボルに対して、その群を生成するための最小限の操作行列のリストをラベル付きで提供します。
    詳細説明:
        ほとんどのSchoenflies記号に対して定義済みの生成元を返します。
        国際記号など、生成元が明確に定義されていない場合は空のリストを返します。
        返される全ての生成元は `snap_matrix` で正規化されています。
    :param symbol: 点群シンボル。
    :type symbol: str
    :returns: ラベルと生成元行列のペアのリスト。生成元が定義されていない場合は空リスト。
    :rtype: List[Tuple[str, numpy.ndarray]]
    """
    s = normalize_symbol(symbol)
    gens: List[Tuple[str, np.ndarray]] = []
    if s in ("C1","1"):
        gens = []
    elif s in ("Ci","-1","1bar"):
        gens = [("i", inversion())]
    elif s in ("Cs","m"):
        gens = [("σ_h", mirror(ez))]
    elif s.startswith("C") and s.endswith("v") and s[1:-1].isdigit():
        n = int(s[1:-1]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("σ_v(φ=0)", mirror(vertical_plane(0.0)))]
    elif s.startswith("C") and s.endswith("h") and s[1:-1].isdigit():
        n = int(s[1:-1]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("σ_h", mirror(ez))]
    elif s.startswith("C") and s[1:].isdigit():
        n = int(s[1:]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n))]
    elif s.startswith("S") and s[1:].isdigit():
        n = int(s[1:]); gens = [(f"S(z,{360//n})", mirror(ez) @ rot(ez,360.0/n))]
    elif s.startswith("D") and s.endswith("h") and s[1:-1].isdigit():
        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))]
    elif s.startswith("D") and s.endswith("d") and s[1:-1].isdigit():
        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)))]
    elif s.startswith("D") and s[1:].isdigit():
        n = int(s[1:]); gens = [(f"C(z,{360//n})", rot(ez,360.0/n)), ("C2(x)", rot(ex,180.0))]
    elif s == "T":
        gens = [("C(111,120)", rot([1,1,1],120.0)), ("C2(x)", rot(ex,180.0))]
    elif s == "Th":
        gens = [("C(111,120)", rot([1,1,1],120.0)), ("C2(x)", rot(ex,180.0)), ("i", inversion())]
    elif s == "Td":
        gens = [("C(111,120)", rot([1,1,1],120.0)), ("C2(x)", rot(ex,180.0)), ("S4(z)", mirror(ez) @ rot(ez,90.0))]
    elif s == "O":
        gens = [("C4(z)", rot(ez,90.0)), ("C3(111)", rot([1,1,1],120.0))]
    elif s == "Oh":
        gens = [("C4(z)", rot(ez,90.0)), ("C3(111)", rot([1,1,1],120.0)), ("i", inversion())]
    else:
        # 国際記号などで generator が空の場合は空返し（必要なら full ops を呼び側で）
        return []
    return [(lab, snap_matrix(M)) for lab,M in gens]

# ========= 軌道・独立代表点（重複削除） =========

def point_key(p: np.ndarray, tol: float = 1e-8) -> tuple:
    """
    点の同一性判定用キーを生成します（トレランスつき）。

    概要:
        与えられた3D点のNumPy配列を、浮動小数点誤差を考慮したハッシュ可能なタプルキーに変換します。
    詳細説明:
        点を指定された許容誤差 (`tol`) でスケールし、最も近い整数に丸めます。
        これにより、非常に近い点が同じキーを持つようになり、集合や辞書での重複排除に利用できます。
    :param p: 3次元の点座標。
    :type p: numpy.ndarray
    :param tol: 点の比較に用いる許容誤差。
    :type tol: float
    :returns: 点のハッシュ可能なタプルキー。
    :rtype: tuple
    """
    p = np.asarray(p, float)
    scale = max(tol, 1e-15)
    return tuple(np.round(p / scale).astype(int))

def orbit_for_point(ops: List[np.ndarray], p: np.ndarray, tol: float = 1e-8) -> List[Tuple[np.ndarray, List[int]]]:
    """
    一点から生成される軌道上の全ての等価点と、それらを生成する操作のインデックスを返します。

    概要:
        与えられた点pに、点群の全ての操作を適用して生成される軌道上のユニークな点をリストアップします。
    詳細説明:
        各操作 M に対して M @ p を計算し、`point_key` を使用してユニークな点を識別します。
        結果は、(等価点, [その点を生成する操作のインデックスのリスト]) のタプルのリストとして返されます。
    :param ops: 点群の対称操作行列のリスト。
    :type ops: List[numpy.ndarray]
    :param p: 軌道を計算する開始点。
    :type p: numpy.ndarray
    :param tol: 点の比較に用いる許容誤差。
    :type tol: float
    :returns: 軌道上の各ユニークな点と、それを生成する操作インデックスのリストのタプル。
    :rtype: List[Tuple[numpy.ndarray, List[int]]]
    """
    pts: Dict[tuple, Tuple[np.ndarray, List[int]]] = {}
    for i, M in enumerate(ops):
        q = M @ p
        k = point_key(q, tol)
        if k not in pts:
            pts[k] = (q, [i])
        else:
            pts[k][1].append(i)
    return list(pts.values())  # [(q, [op_idx, ...]), ...]

def dedup_points(orbits: List[Tuple[np.ndarray, List[int]]], tol: float = 1e-8) -> List[Tuple[np.ndarray, List[int]]]:
    """
    (点, 操作IDリスト) のリストからユニークな点を抽出します。

    概要:
        `orbit_for_point` の結果から、座標が同じ点をまとめ、それらに対応する操作IDを結合します。
    詳細説明:
        `point_key` を使用して点の重複を検出し、重複する点が見つかった場合は、
        対応する操作IDのセットを結合します。最終的に、ユニークな点とその点を生成する
        全ての操作ID（ソート済みリスト）のペアを返します。
    :param orbits: `orbit_for_point` から返される (点, 操作IDリスト) のリスト。
    :type orbits: List[Tuple[numpy.ndarray, List[int]]]
    :param tol: 点の比較に用いる許容誤差。
    :type tol: float
    :returns: 重複排除された (ユニークな点, その点を生成する操作IDのリスト) のペアのリスト。
    :rtype: List[Tuple[numpy.ndarray, List[int]]]
    """
    seen: Dict[tuple, int] = {}
    reps: List[List[object]] = []
    for q, op_ids in orbits:
        k = point_key(q, tol)
        if k not in seen:
            seen[k] = len(reps)
            reps.append([q, set(op_ids)])
        else:
            reps[seen[k]][1].update(op_ids)
    return [(np.asarray(q, float), sorted(list(ids))) for q, ids in reps]

def unique_orbit_points(ops: List[np.ndarray], p: np.ndarray, tol: float = 1e-8) -> List[Tuple[np.ndarray, List[int]]]:
    """
    対称操作行列と開始点を受け取り、重複排除された軌道上の独立点とその生成操作のインデックスを返します。

    概要:
        与えられた点pに点群の全ての操作を適用し、結果として得られる軌道上の重複しない点を抽出します。
    詳細説明:
        `orbit_for_point` と `dedup_points` を内部的に使用して、
        最終的な独立代表点と、それらを作成する全ての対称操作のインデックスをまとめます。
    :param ops: 点群の対称操作行列のリスト。
    :type ops: List[numpy.ndarray]
    :param p: 軌道を計算する開始点。
    :type p: numpy.ndarray
    :param tol: 点の比較に用いる許容誤差。
    :type tol: float
    :returns: 軌道上のユニークな各点と、それを生成する操作インデックスのリストのタプル。
    :rtype: List[Tuple[numpy.ndarray, List[int]]]
    """
    return dedup_points(orbit_for_point(ops, p, tol=tol), tol=tol)

# ========= 高レベルAPI =========

def get_all_operations(symbol: str) -> List[Tuple[str, np.ndarray]]:
    """
    点群シンボルから全要素（ラベル付き、重複なし）を返します。

    概要:
        指定された点群シンボルに対応する全ての対称操作行列と、それぞれの簡易ラベルのペアのリストを返します。
    詳細説明:
        `build_group` を使用して行列を構築し、`label_elements` でラベル付けを行います。
    :param symbol: 点群シンボル。
    :type symbol: str
    :returns: ラベルと操作行列のペアのリスト。
    :rtype: List[Tuple[str, numpy.ndarray]]
    :raises ValueError: サポートされていない点群シンボルが指定された場合。
    """
    mats = build_group(symbol)
    return label_elements(mats)

def get_generators(symbol: str) -> List[Tuple[str, np.ndarray]]:
    """
    点群シンボルから生成元（ラベル付き）を返します。

    概要:
        指定された点群シンボルに対応する生成元の操作行列と、それぞれの簡易ラベルのペアのリストを返します。
    詳細説明:
        `generators_for` を使用して定義済みの生成元を取得します。
        もし `generators_for` が生成元を返さない場合（国際記号など）、`get_all_operations` にフォールバックし、
        全ての群要素を生成元と見なして返します。
    :param symbol: 点群シンボル。
    :type symbol: str
    :returns: ラベルと生成元行列のペアのリスト。
    :rtype: List[Tuple[str, numpy.ndarray]]
    :raises ValueError: サポートされていない点群シンボルが指定された場合（`build_group`経由）。
    """
    gens = generators_for(symbol)
    if gens:
        return gens
    # generator未定義（国際記号など）の場合は全要素へフォールバック
    return get_all_operations(symbol)

def elements_from_generators(generators: List[np.ndarray]) -> List[Tuple[str, np.ndarray]]:
    """
    生成元行列のリストから閉包をとり、ラベル付きの全要素を返します。

    概要:
        与えられた生成元行列のリストから、それらによって生成される全てのユニークな群要素を計算し、
        それぞれの操作に簡易ラベルを付けて返します。
    詳細説明:
        `unique_closure` を使用して生成元から全ての群要素行列を計算し、
        `label_elements` で各行列にラベルを付けます。
    :param generators: 群の生成元であるNumPy行列のリスト。
    :type generators: List[numpy.ndarray]
    :returns: ラベルと群要素行列のペアのリスト。
    :rtype: List[Tuple[str, numpy.ndarray]]
    """
    mats = unique_closure(generators)
    return label_elements(mats)

# Point-group character tables, abstract class sizes, and helpers for Γ_3N/Γ_T/Γ_R.

import numpy as _np
from collections import Counter as _Counter, defaultdict as _defaultdict
from typing import Dict as _Dict, List as _List, Tuple as _Tuple, Optional as _Optional

# ---------- Schoenflies <-> HM (補完) ----------
# 既存のマップを尊重しつつ、追加グループを補う（C4h, C3h, I, Ih など）
_S_to_H.update({
    "C4h":"4/m",
    "Ch":"m",
    "C3h":"C3h",   # 非結晶・抽象モード
    "I":"I",       # 正二十面体群（抽象モード）
    "Ih":"Ih",     # 完全二十面体群（抽象モード）
})
_H_to_S.update({
    "4/m":"C4h",
    "m":"Cs",      # alias
    # 非結晶は自分自身を返す
    "C3h":"C3h",
})

# ---------- Character tables ----------
# 既知の主要点群 + 非結晶 (I, Ih, C3h) + C4h（可換）
# （実数表示／必要箇所は複素数を許容）
_phi  = (1 + 5**0.5)/2
_phip = (1 - 5**0.5)/2

def _gen_C4h_table() -> _Dict[str, _Dict[str, complex]]:
    """
    C4h点群の指標表を生成します。

    概要:
        C4h点群のクラスと既約表現の指標を含む辞書を動的に構築して返します。
    詳細説明:
        C4hは可換群であり、その既約表現は1次元です。
        各既約表現は、a (0から3) と偶奇性 (g/u) の組み合わせでタグ付けされます。
    :returns: C4h点群の指標表。
    :rtype: Dict[str, Dict[str, complex]]
    """
    classes = ["E","C4","C4^3","C2","i","S4","S4^3","σh"]
    irreps = {}
    for a in (0,1,2,3):
        for par, tag in ((+1,"g"),(-1,"u")):
            lab = f"Γ{a}{tag}"
            def phase(k): return _np.exp(1j*_np.pi/2 * a * k)
            irreps[lab] = {
                "E":   1.0,
                "C4":  phase(1),
                "C4^3":phase(3),
                "C2":  phase(2),
                "i":   par * 1.0,
                "S4":  par * phase(1),
                "S4^3":par * phase(3),
                "σh":  par * phase(2),
            }
    return {"classes": classes, "irreps": irreps}

# 主要表（必要最小限。C系/Cnv/Cnh/D系/T/Th/Td/O/Oh/C3h/C4h/I/Ih を含む）
# フル表は長いのでここでは代表的なものを実装（vib_irreps で使う範囲をカバー）
PG_CHAR_TABLES: _Dict[str, _Dict[str, _Dict[str, complex]]] = {
    # --- C, Cnv, Cnh（代表） ---
    "C1":{"classes":["E"], "irreps":{"A":{"E":1}}},
    "Ci":{"classes":["E","i"], "irreps":{"Ag":{"E":1,"i":1}, "Au":{"E":1,"i":-1}}},
    "Cs":{"classes":["E","σ"], "irreps":{"A'":{"E":1,"σ":1}, "A''":{"E":1,"σ":-1}}},
    "Ch":{"classes":["E","σ"], "irreps":{"A'":{"E":1,"σ":1}, "A''":{"E":1,"σ":-1}}},
    "C2":{"classes":["E","C2"], "irreps":{"A":{"E":1,"C2":1}, "B":{"E":1,"C2":-1}}},
    "C3":{"classes":["E","C3","C3^2"], "irreps":{"A":{"E":1,"C3":1,"C3^2":1}, "E":{"E":2,"C3":-1,"C3^2":-1}}},
    "C4":{"classes":["E","C4","C2","C4^3"], "irreps":{
        "A":{"E":1,"C4":1,"C2":1,"C4^3":1},
        "B":{"E":1,"C4":-1,"C2":1,"C4^3":-1},
        "E":{"E":2,"C4":0,"C2":-2,"C4^3":0}
    }},
    "C6":{"classes":["E","C6","C3","C2","C3^2","C6^5"], "irreps":{
        "A":{"E":1,"C6":1,"C3":1,"C2":1,"C3^2":1,"C6^5":1},
        "B":{"E":1,"C6":-1,"C3":1,"C2":-1,"C3^2":1,"C6^5":-1},
        "E1":{"E":2,"C6":1,"C3":-1,"C2":-2,"C3^2":-1,"C6^5":1},
        "E2":{"E":2,"C6":-1,"C3":-1,"C2":2,"C3^2":-1,"C6^5":-1}
    }},
    "C2v":{"classes":["E","C2","σv","σv'"], "irreps":{
        "A1":{"E":1,"C2":1,"σv":1,"σv'":1},
        "A2":{"E":1,"C2":1,"σv":-1,"σv'":-1},
        "B1":{"E":1,"C2":-1,"σv":1,"σv'":-1},
        "B2":{"E":1,"C2":-1,"σv":-1,"σv'":1}
    }},
    "C3v":{"classes":["E","C3","C3^2","σv"], "irreps":{
        "A1":{"E":1,"C3":1,"C3^2":1,"σv":1},
        "A2":{"E":1,"C3":1,"C3^2":1,"σv":-1},
        "E":{"E":2,"C3":-1,"C3^2":-1,"σv":0}
    }},
    "C4v":{"classes":["E","C4","C4^3","C2","σv","σd"], "irreps":{
        "A1":{"E":1,"C4":1,"C4^3":1,"C2":1,"σv":1,"σd":1},
        "A2":{"E":1,"C4":1,"C4^3":1,"C2":1,"σv":-1,"σd":-1},
        "B1":{"E":1,"C4":-1,"C4^3":-1,"C2":1,"σv":1,"σd":-1},
        "B2":{"E":1,"C4":-1,"C4^3":-1,"C2":1,"σv":-1,"σd":1},
        "E":{"E":2,"C4":0,"C4^3":0,"C2":-2,"σv":0,"σd":0}
    }},
    "C6v":{"classes":["E","C6","C6^5","C3","C3^2","C2","σv","σd"], "irreps":{
        "A1":{"E":1,"C6":1,"C6^5":1,"C3":1,"C3^2":1,"C2":1,"σv":1,"σd":1},
        "A2":{"E":1,"C6":1,"C6^5":1,"C3":1,"C3^2":1,"C2":1,"σv":-1,"σd":-1},
        "B1":{"E":1,"C6":-1,"C6^5":-1,"C3":1,"C3^2":1,"C2":-1,"σv":1,"σd":-1},
        "B2":{"E":1,"C6":-1,"C6^5":-1,"C3":1,"C3^2":1,"C2":-1,"σv":-1,"σd":1},
        "E1":{"E":2,"C6":1,"C6^5":1,"C3":-1,"C3^2":-1,"C2":-2,"σv":0,"σd":0},
        "E2":{"E":2,"C6":-1,"C6^5":-1,"C3":-1,"C3^2":-1,"C2":2,"σv":0,"σd":0}
    }},
    "C2h":{"classes":["E","C2(z)","i","σh"], "irreps":{
        "Ag":{"E":1,"C2(z)":1,"i":1,"σh":1},
        "Bg":{"E":1,"C2(z)":-1,"i":1,"σh":-1},
        "Au":{"E":1,"C2(z)":1,"i":-1,"σh":-1},
        "Bu":{"E":1,"C2(z)":-1,"i":-1,"σh":1},
    }},
    # --- D 系（代表） ---
    "D2":{"classes":["E","C2(x)","C2(y)","C2(z)"], "irreps":{
        "A":{"E":1,"C2(x)":1,"C2(y)":1,"C2(z)":1},
        "B1":{"E":1,"C2(x)":1,"C2(y)":-1,"C2(z)":-1},
        "B2":{"E":1,"C2(x)":-1,"C2(y)":1,"C2(z)":-1},
        "B3":{"E":1,"C2(x)":-1,"C2(y)":-1,"C2(z)":1},
    }},
    "D2h":{"classes":["E","C2(x)","C2(y)","C2(z)","i","σ(xy)","σ(xz)","σ(yz)"], "irreps":{
        "Ag":{"E":1,"C2(x)":1,"C2(y)":1,"C2(z)":1,"i":1,"σ(xy)":1,"σ(xz)":1,"σ(yz)":1},
        "B1g":{"E":1,"C2(x)":1,"C2(y)":-1,"C2(z)":-1,"i":1,"σ(xy)":1,"σ(xz)":-1,"σ(yz)":-1},
        "B2g":{"E":1,"C2(x)":-1,"C2(y)":1,"C2(z)":-1,"i":1,"σ(xy)":-1,"σ(xz)":1,"σ(yz)":-1},
        "B3g":{"E":1,"C2(x)":-1,"C2(y)":-1,"C2(z)":1,"i":1,"σ(xy)":-1,"σ(xz)":-1,"σ(yz)":1},
        "Au":{"E":1,"C2(x)":1,"C2(y)":1,"C2(z)":1,"i":-1,"σ(xy)":-1,"σ(xz)":-1,"σ(yz)":-1},
        "B1u":{"E":1,"C2(x)":1,"C2(y)":-1,"C2(z)":-1,"i":-1,"σ(xy)":-1,"σ(xz)":1,"σ(yz)":1},
        "B2u":{"E":1,"C2(x)":-1,"C2(y)":1,"C2(z)":-1,"i":-1,"σ(xy)":1,"σ(xz)":-1,"σ(yz)":1},
        "B3u":{"E":1,"C2(x)":-1,"C2(y)":-1,"C2(z)":1,"i":-1,"σ(xy)":1,"σ(xz)":1,"σ(yz)":-1},
    }},
    "D3":{"classes":["E","C3","C2'"], "irreps":{
        "A1":{"E":1,"C3":1,"C2'":1},
        "A2":{"E":1,"C3":1,"C2'":-1},
        "E":{"E":2,"C3":-1,"C2'":0},
    }},
    "D3d":{"classes":["E","C3","C2'","i","S6","σd"], "irreps":{
        "A1g":{"E":1,"C3":1,"C2'":1,"i":1,"S6":1,"σd":1},
        "A2g":{"E":1,"C3":1,"C2'":-1,"i":1,"S6":-1,"σd":-1},
        "Eg":{"E":2,"C3":-1,"C2'":0,"i":2,"S6":0,"σd":0},
        "A1u":{"E":1,"C3":1,"C2'":1,"i":-1,"S6":-1,"σd":-1},
        "A2u":{"E":1,"C3":1,"C2'":-1,"i":-1,"S6":1,"σd":1},
        "Eu":{"E":2,"C3":-1,"C2'":0,"i":-2,"S6":0,"σd":0},
    }},
    "D3h":{"classes":["E","C3","C2'","σh","S3","σv"], "irreps":{
        "A1'":{"E":1,"C3":1,"C2'":1,"σh":1,"S3":1,"σv":1},
        "A2'":{"E":1,"C3":1,"C2'":-1,"σh":1,"S3":1,"σv":-1},
        "E'":{"E":2,"C3":-1,"C2'":0,"σh":2,"S3":-1,"σv":0},
        "A1''":{"E":1,"C3":1,"C2'":1,"σh":-1,"S3":-1,"σv":-1},
        "A2''":{"E":1,"C3":1,"C2'":-1,"σh":-1,"S3":-1,"σv":1},
        "E''":{"E":2,"C3":-1,"C2'":0,"σh":-2,"S3":1,"σv":0},
    }},
    "D4":{"classes":["E","C4","C2(z)","C2'","C2''"], "irreps":{
        "A1":{"E":1,"C4":1,"C2(z)":1,"C2'":1,"C2''":1},
        "A2":{"E":1,"C4":1,"C2(z)":1,"C2'":-1,"C2''":-1},
        "B1":{"E":1,"C4":-1,"C2(z)":1,"C2'":1,"C2''":-1},
        "B2":{"E":1,"C4":-1,"C2(z)":1,"C2'":-1,"C2''":1},
        "E":{"E":2,"C4":0,"C2(z)":-2,"C2'":0,"C2''":0},
    }},
    "D2d":{"classes":["E","S4","C2(z)","C2'","σd"], "irreps":{
        "A1":{"E":1,"S4":1,"C2(z)":1,"C2'":1,"σd":1},
        "A2":{"E":1,"S4":1,"C2(z)":1,"C2'":-1,"σd":-1},
        "B1":{"E":1,"S4":-1,"C2(z)":1,"C2'":1,"σd":-1},
        "B2":{"E":1,"S4":-1,"C2(z)":1,"C2'":-1,"σd":1},
        "E":{"E":2,"S4":0,"C2(z)":-2,"C2'":0,"σd":0},
    }},
    "D4h":{"classes":["E","C4","C2(z)","C2'","C2''","i","S4","σh","σv","σd"], "irreps":{
        "A1g":{"E":1,"C4":1,"C2(z)":1,"C2'":1,"C2''":1,"i":1,"S4":1,"σh":1,"σv":1,"σd":1},
        "A2g":{"E":1,"C4":1,"C2(z)":1,"C2'":-1,"C2''":-1,"i":1,"S4":1,"σh":1,"σv":-1,"σd":-1},
        "B1g":{"E":1,"C4":-1,"C2(z)":1,"C2'":1,"C2''":-1,"i":1,"S4":-1,"σh":1,"σv":1,"σd":-1},
        "B2g":{"E":1,"C4":-1,"C2(z)":1,"C2'":-1,"C2''":1,"i":1,"S4":-1,"σh":1,"σv":-1,"σd":1},
        "Eg":{"E":2,"C4":0,"C2(z)":-2,"C2'":0,"C2''":0,"i":2,"S4":0,"σh":2,"σv":0,"σd":0},
        "A1u":{"E":1,"C4":1,"C2(z)":1,"C2'":1,"C2''":1,"i":-1,"S4":-1,"σh":-1,"σv":-1,"σd":-1},
        "A2u":{"E":1,"C4":1,"C2(z)":1,"C2'":-1,"C2''":-1,"i":-1,"S4":-1,"σh":-1,"σv":1,"σd":1},
        "B1u":{"E":1,"C4":-1,"C2(z)":1,"C2'":1,"C2''":-1,"i":-1,"S4":1,"σh":-1,"σv":-1,"σd":1},
        "B2u":{"E":1,"C4":-1,"C2(z)":1,"C2'":-1,"C2''":1,"i":-1,"S4":1,"σh":-1,"σv":1,"σd":-1},
        "Eu":{"E":2,"C4":0,"C2(z)":-2,"C2'":0,"C2''":0,"i":-2,"S4":0,"σh":-2,"σv":0,"σd":0},
    }},
    "D6":{"classes":["E","C6","C3","C2(z)","C2'","C2''"], "irreps":{
        "A1":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":1,"C2''":1},
        "A2":{"E":1,"C6":1,"C3":1,"C2(z)":1,"C2'":-1,"C2''":-1},
        "B1":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":1,"C2''":-1},
        "B2":{"E":1,"C6":-1,"C3":1,"C2(z)":-1,"C2'":-1,"C2''":1},
        "E1":{"E":2,"C6":1,"C3":-1,"C2(z)":-2,"C2'":0,"C2''":0},
        "E2":{"E":2,"C6":-1,"C3":-1,"C2(z)":2,"C2'":0,"C2''":0},
    }},
    "D6h":{"classes":["E","C6","C3","C2(z)","C2'","C2''","i","S3","S6","σh","σd","σv"], "irreps":{
        "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},
        "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},
        "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},
        "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},
        "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},
        "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},
        "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},
        "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},
        "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},
        "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},
        "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},
        "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},
    }},
    # --- 立方 ---
    "T":{"classes":["E","C3","C2"], "irreps":{
        "A":{"E":1,"C3":1,"C2":1},
        "E":{"E":2,"C3":-1,"C2":2},
        "T":{"E":3,"C3":0,"C2":-1},
    }},
    "Td":{"classes":["E","C3","C2","S4","σd"], "irreps":{
        "A1":{"E":1,"C3":1,"C2":1,"S4":1,"σd":1},
        "A2":{"E":1,"C3":1,"C2":1,"S4":-1,"σd":-1},
        "E":{"E":2,"C3":-1,"C2":2,"S4":0,"σd":0},
        "T1":{"E":3,"C3":0,"C2":-1,"S4":1,"σd":-1},
        "T2":{"E":3,"C3":0,"C2":-1,"S4":-1,"σd":1},
    }},
    "Th":{"classes":["E","C3","C2","i","S6"], "irreps":{
        "Ag":{"E":1,"C3":1,"C2":1,"i":1,"S6":1},
        "Au":{"E":1,"C3":1,"C2":1,"i":-1,"S6":-1},
        "Eg":{"E":2,"C3":-1,"C2":2,"i":2,"S6":0},
        "Eu":{"E":2,"C3":-1,"C2":2,"i":-2,"S6":0},
        "Tg":{"E":3,"C3":0,"C2":-1,"i":3,"S6":-1},
        "Tu":{"E":3,"C3":0,"C2":-1,"i":-3,"S6":1},
    }},
    "O":{"classes":["E","C3","C4","C2(ax)","C2(di)"], "irreps":{
        "A1":{"E":1,"C3":1,"C4":1,"C2(ax)":1,"C2(di)":1},
        "A2":{"E":1,"C3":1,"C4":-1,"C2(ax)":1,"C2(di)":-1},
        "E":{"E":2,"C3":-1,"C4":0,"C2(ax)":2,"C2(di)":0},
        "T1":{"E":3,"C3":0,"C4":1,"C2(ax)":-1,"C2(di)":-1},
        "T2":{"E":3,"C3":0,"C4":-1,"C2(ax)":-1,"C2(di)":1},
    }},
    "Oh":{"classes":["E","C3","C4","C2(ax)","C2(di)","i","S6","S4","σh","σd"], "irreps":{
        "A1g":{"E":1,"C3":1,"C4":1,"C2(ax)":1,"C2(di)":1,"i":1,"S6":1,"S4":1,"σh":1,"σd":1},
        "A2g":{"E":1,"C3":1,"C4":-1,"C2(ax)":1,"C2(di)":-1,"i":1,"S6":-1,"S4":-1,"σh":1,"σd":-1},
        "Eg":{"E":2,"C3":-1,"C4":0,"C2(ax)":2,"C2(di)":0,"i":2,"S6":0,"S4":0,"σh":2,"σd":0},
        "T1g":{"E":3,"C3":0,"C4":1,"C2(ax)":-1,"C2(di)":-1,"i":3,"S6":-1,"S4":1,"σh":-1,"σd":-1},
        "T2g":{"E":3,"C3":0,"C4":-1,"C2(ax)":-1,"C2(di)":1,"i":3,"S6":1,"S4":-1,"σh":-1,"σd":1},
        "A1u":{"E":1,"C3":1,"C4":1,"C2(ax)":1,"C2(di)":1,"i":-1,"S6":-1,"S4":-1,"σh":-1,"σd":-1},
        "A2u":{"E":1,"C3":1,"C4":-1,"C2(ax)":1,"C2(di)":-1,"i":-1,"S6":1,"S4":1,"σh":-1,"σd":1},
        "Eu":{"E":2,"C3":-1,"C4":0,"C2(ax)":2,"C2(di)":0,"i":-2,"S6":0,"S4":0,"σh":-2,"σd":0},
        "T1u":{"E":3,"C3":0,"C4":1,"C2(ax)":-1,"C2(di)":-1,"i":-3,"S6":1,"S4":-1,"σh":1,"σd":1},
        "T2u":{"E":3,"C3":0,"C4":-1,"C2(ax)":-1,"C2(di)":1,"i":-3,"S6":-1,"S4":1,"σh":1,"σd":-1},
    }},
    # --- C3h, C4h ---
    "C3h":{"classes":["E","C3","σh","S3"], "irreps":{
        "A'":{"E":1,"C3":1,"σh":1,"S3":1},
        "A''":{"E":1,"C3":1,"σh":-1,"S3":-1},
        "E'":{"E":2,"C3":-1,"σh":2,"S3":-1},
        "E''":{"E":2,"C3":-1,"σh":-2,"S3":1},
    }},
}
PG_CHAR_TABLES["C4h"] = _gen_C4h_table()

# --- Icosahedral (抽象) ---
PG_CHAR_TABLES.update({
    "I":{"classes":["E","C5","C5^2","C3","C2"], "irreps":{
        "A":{"E":1,"C5":1,"C5^2":1,"C3":1,"C2":1},
        "T1":{"E":3,"C5":_phi,"C5^2":_phip,"C3":0,"C2":-1},
        "T2":{"E":3,"C5":_phip,"C5^2":_phi,"C3":0,"C2":-1},
        "G":{"E":4,"C5":-1,"C5^2":-1,"C3":1,"C2":0},
        "H":{"E":5,"C5":0,"C5^2":0,"C3":-1,"C2":1},
    }},
    "Ih":{"classes":["E","C5","C5^2","C3","C2","i","S10","S10^3","S6","S2"], "irreps":{}},
})
def _fill_Ih_from_I():
    """
    Icosahedral (Ih) 点群の指標表をI (I) 点群の指標表から構築します。

    概要:
        Ih点群の指標表は、I点群の指標表に反転操作を追加することで生成されます。
    詳細説明:
        I点群の各既約表現 (irreps) に対して、偶パリティ ('g') と奇パリティ ('u') の2つの新しい表現を生成します。
        これらの表現には、反転 (i) や回映 (S10, S6など) 操作の指標が含まれます。
    """
    base = PG_CHAR_TABLES["I"]["irreps"]
    irr = {}
    for name, row in base.items():
        gn = name+"g"; un = name+"u"
        irr[gn] = dict(row); irr[gn].update({"i":+row["E"], "S10":+row["C5"], "S10^3":+row["C5^2"], "S6":+row["C3"], "S2":+row["C2"]})
        irr[un] = dict(row); irr[un].update({"i":-row["E"], "S10":-row["C5"], "S10^3":-row["C5^2"], "S6":-row["C3"], "S2":-row["C2"]})
    PG_CHAR_TABLES["Ih"]["irreps"] = irr
_fill_Ih_from_I()

# 公開API
def character_table(pg: str) -> _Dict[str, _Dict[str, complex]]:
    """
    指定された点群の指標表（classes, irreps）を返します。

    概要:
        与えられた点群シンボルに対応する指標表を検索し、そのクラスと既約表現の指標を返します。
    詳細説明:
        `PG_CHAR_TABLES` 辞書から対応する指標表を取得します。
        サポートされていない点群の場合は `ValueError` を発生させます。
        シンボルは `normalize_symbol` で正規化されます。
    :param pg: 点群シンボル。
    :type pg: str
    :returns: 指標表を含む辞書。キーは 'classes' (クラス名のリスト) と 'irreps' (既約表現ごとの指標の辞書)。
    :rtype: Dict[str, Dict[str, complex]]
    :raises ValueError: 指定された点群の指標表が利用できない場合。
    """
    s = normalize_symbol(pg)
    if s not in PG_CHAR_TABLES:
        raise ValueError(f"Character table for '{pg}' is not available.")
    return PG_CHAR_TABLES[s]

# 抽象モードのクラスサイズ（pymatgen/builder未対応の群用）
ABSTRACT_CLASS_SIZES: _Dict[str, _Dict[str,int]] = {
    "C3h":{"E":1, "C3":2, "σh":1, "S3":2},             # |G|=6
    "I":  {"E":1, "C5":12, "C5^2":12, "C3":20, "C2":15},# |G|=60
    "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
}

# ---------- 分類ラベル（クラス名）付け：表に合わせる ----------
_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])
_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])]
_C2_DIAGS3D = [
    _np.array([1,1,0.0]), _np.array([1,-1,0.0]), _np.array([-1,1,0.0]), _np.array([-1,-1,0.0]),
    _np.array([1,0,1.0]), _np.array([1,0,-1.0]), _np.array([-1,0,1.0]), _np.array([-1,0,-1.0]),
    _np.array([0,1,1.0]), _np.array([0,1,-1.0]), _np.array([0,-1,1.0]), _np.array([0,-1,-1.0]),
]
def _unit(v: _np.ndarray) -> _np.ndarray:
    """
    ベクトルを単位化します。

    概要:
        入力ベクトルを単位ベクトルに変換します。
    詳細説明:
        ベクトルのノルムが非常に小さい場合でもゼロ除算を避けるために小さな値を加えます。
    :param v: 単位化するベクトル。
    :type v: numpy.ndarray
    :returns: 単位ベクトル。
    :rtype: numpy.ndarray
    """
    v=_np.asarray(v,float); n=_np.linalg.norm(v); return v/(n+1e-15)

def _close_to(v: _np.ndarray, cands: _List[_np.ndarray], tol: float = 0.1) -> bool:
    """
    ベクトルが候補ベクトルのいずれかに近似しているかを判定します。

    概要:
        与えられた単位ベクトルが、候補リスト内のいずれかの単位ベクトルに指定されたトレランス内で近似するかどうかをチェックします。
    詳細説明:
        入力ベクトルと各候補ベクトルの内積の絶対値を計算し、それが `1 - tol` より大きい場合に近似と見なします。
    :param v: 比較する単位ベクトル。
    :type v: numpy.ndarray
    :param cands: 比較対象の候補ベクトルリスト。
    :type cands: List[numpy.ndarray]
    :param tol: 近似を判定する許容誤差。
    :type tol: float
    :returns: ベクトルが候補群に近似していればTrue、そうでなければFalse。
    :rtype: bool
    """
    v=_unit(v); best=-1
    for c in cands:
        d=abs(float(_unit(c)@v))
        if d>best: best=d
    return best >= (1-tol)

def _orth_keep_det(R: _np.ndarray) -> _np.ndarray:
    """
    行列を直交化し、行列式の符号を維持します。

    概要:
        入力行列を最も近い直交行列に変換し、元の行列式の符号が正であれば新しい行列式も正に、負であれば負になるように調整します。
    詳細説明:
        特異値分解 (SVD) を使用して行列を直交化します。
        もし直交化後の行列式が元の符号と異なる場合、SVDのU行列の最後の列の符号を反転させて調整します。
    :param R: 直交化する行列。
    :type R: numpy.ndarray
    :returns: 直交化され、行列式の符号が維持された行列。
    :rtype: numpy.ndarray
    """
    U,_,Vt = _np.linalg.svd(R); R_=U@Vt
    if _np.linalg.det(R_)<0: U[:,-1]*=-1; R_=U@Vt
    return R_

def _rot_axis(R: _np.ndarray) -> _Optional[_np.ndarray]:
    """
    回転行列の回転軸を抽出します。

    概要:
        与えられた回転行列の固定軸（回転軸）を実固有ベクトルとして特定し、正規化されたベクトルを返します。
    詳細説明:
        回転行列の固有値を計算し、固有値1に対応する実固有ベクトルを回転軸と見なします。
        もし固有値1に対応する固有ベクトルが見つからない場合や、実数部が1に近似しない場合はNoneを返します。
    :param R: 回転行列。
    :type R: numpy.ndarray
    :returns: 回転軸を示す単位ベクトル、またはNone。
    :rtype: Optional[numpy.ndarray]
    """
    vals,vecs=_np.linalg.eig(R); idx=_np.argmax(_np.real(vals))
    if _np.isclose(vals[idx].real,1.0,atol=1e-5):
        return _unit(_np.real(vecs[:,idx]))
    return None

def _angle_from_trace(R: _np.ndarray) -> float:
    """
    回転行列のトレースから回転角度を計算します。

    概要:
        回転行列のトレース情報を使用して、その回転が表す角度（ラジアン）を計算します。
    詳細説明:
        回転行列Rのトレース `tr(R)` は `1 + 2*cos(theta)` の関係があります。
        この式を逆算して `theta = arccos((tr(R)-1)/2)` を求めます。
        数値誤差を考慮して `(tr(R)-1)/2` の値は [-1, 1] にクリップされます。
    :param R: 回転行列。
    :type R: numpy.ndarray
    :returns: 回転角度（ラジアン）。
    :rtype: float
    """
    return _np.arccos(float(_np.clip((_np.trace(R)-1)/2,-1,1)))

def classify_op_for_table(pg: str, R: _np.ndarray, tol: float = 1e-6) -> str:
    """
    回転行列を、指定された点群の指標表中のクラス名に対応づけて分類します。

    概要:
        与えられた対称操作行列を、点群のコンテキストに基づいて標準的なクラスラベル（例: "C2", "C2(x)", "σh"）に分類します。
    詳細説明:
        まず行列を直交化し、行列式を±1に修正します。
        その後、行列式、トレース、固有ベクトル、そして特定の点群（例: D2h, D4h）の軸の方向に基づいて操作を分類します。
        例えば、C2操作は点群によって "C2(x)", "C2(y)", "C2(z)"、または単に "C2" とラベル付けされます。
    :param pg: 点群シンボル (例: "C2v", "D2h")。
    :type pg: str
    :param R: 分類する3x3の対称操作行列。
    :type R: numpy.ndarray
    :param tol: 浮動小数点比較の許容誤差。
    :type tol: float
    :returns: 指標表のクラス名に対応する文字列ラベル。
    :rtype: str
    """
    R = _orth_keep_det(R); det=float(_np.linalg.det(R))
    if _np.allclose(R, _np.eye(3), atol=tol): return "E"
    if _np.allclose(R, -_np.eye(3), atol=tol): return "i"
    # mirrors
    if det<0 and _np.allclose(R@R, _np.eye(3), atol=1e-6):
        vals,vecs=_np.linalg.eig(R)
        nrm=_unit(_np.real(vecs[:, _np.argmin(_np.real(vals)) ]))
        nz=abs(nrm[2])
        if nz>1-1e-3: return "σh"
        vscore=max(abs(nrm@_unit(v)) for v in [_X,_Y,-_X,-_Y])
        dscore=max(abs(nrm@_unit(v)) for v in _XY_DIAGS)
        if vscore>=dscore: return "σv"
        else: return "σd"
    # proper
    if det>0:
        th=_angle_from_trace(R); 
        if th<1e-6: return "E"
        n=int(round(2*_np.pi/th))
        ax=_rot_axis(R)
        if n==2:
            if pg in ("D2","D2h"):
                if _close_to(ax,[_X,-_X]): return "C2(x)"
                if _close_to(ax,[_Y,-_Y]): return "C2(y)"
                if _close_to(ax,[_Z,-_Z]): return "C2(z)"
            if pg in ("C2h",) and _close_to(ax,[_Z,-_Z]): return "C2(z)"
            if pg in ("D4","D4h","D2d","C4h","C4v","Oh","O","D6","D6h"):
                if _close_to(ax,[_Z,-_Z]): return "C2(z)"
                if _close_to(ax,[_X,-_X,_Y,-_Y]): return "C2'"
                if _close_to(ax,_XY_DIAGS): return "C2''"
                if pg in ("O","Oh"):
                    if _close_to(ax,[_X,-_X,_Y,-_Y,_Z,-_Z]): return "C2(ax)"
                    if _close_to(ax,_C2_DIAGS3D): return "C2(di)"
            return "C2"
        if n==3:
            return "C3" if th<=_np.pi/1.5 + 1e-3 else "C3^2" # 120deg vs 240deg
        if n==4:
            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]
        if n==5:
            if _np.isclose(th,2*_np.pi/5,atol=1e-3): return "C5"
            if _np.isclose(th,4*_np.pi/5,atol=1e-3): return "C5^2"
            return "C5"
        if n==6:
            if _np.isclose(th,_np.pi/3,atol=1e-3): return "C6"
            # 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)
            # 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.
            # This needs careful handling for C6.
            # For simplicity, if _angle_from_trace returns an angle associated with C6, we label it as C6.
            # If the angle is 2pi/3 (C3 equivalent within C6), it would be caught by n=3.
            # For this context, C6 should be specific to 60 deg rotation.
            return "C6"
        return f"C{n}"
    # improper
    if det<0:
        A=-R; th=_angle_from_trace(A); n=int(round(2*_np.pi/th))
        if n==4: return "S4" if A[0,1]<0 else "S4^3"
        if n==6: return "S6"
        if n==3: return "S3"
        if n==10:
            if _np.isclose(th,2*_np.pi/5,atol=5e-3): return "S10"
            if _np.isclose(th,4*_np.pi/5,atol=5e-3): return "S10^3"
            return "S10"
        return f"S{n}"
    return "?"

# ---------- クラス集約・Γ_3N/Γ_T/Γ_R ----------
def class_aggregation(pg: str, raw_labels: _List[str]) -> _Tuple[_List[str], _Dict[str,str]]:
    """
    生の操作ラベルを、指定された点群の指標表のクラス名に集約します。

    概要:
        `classify_op_for_table` によって生成された詳細な操作ラベルを、
        点群の指標表で定義されているクラス名にマッピングし、集約します。
    詳細説明:
        点群の指標表からクラスのリストを取得し、入力された生のラベルを
        これらのクラス名に対応付ける辞書を構築します。
        例えば、"σ_v(φ=0)" や "σ_v(φ=90)" のような複数のσv面が単一の "σv" クラスに集約されます。
    :param pg: 点群シンボル。
    :type pg: str
    :param raw_labels: 各対称操作に対する生の文字列ラベルのリスト。
    :type raw_labels: List[str]
    :returns: (指標表のクラス名のリスト, 生ラベルからクラス名へのマッピング辞書) のタプル。
    :rtype: Tuple[List[str], Dict[str, str]]
    """
    classes = character_table(pg)["classes"]
    setc=set(classes); cmap={}
    for lab in raw_labels:
        if lab in setc: cmap[lab]=lab; continue
        if lab=="C2" and "C2(z)" in setc: cmap[lab]="C2(z)"; continue
        if lab.startswith("σv") and "σv" in setc: cmap[lab]="σv"; continue
        if lab.startswith("σd") and "σd" in setc: cmap[lab]="σd"; continue
        if lab in ("C2'", "C2''") and lab in setc: cmap[lab]=lab; continue
        if lab=="C2" and "C2'" in setc: cmap[lab]="C2'"; continue
        if lab in ("C2(ax)","C2(di)") and lab in setc: cmap[lab]=lab; continue
        if lab.startswith("C4") and "C4" in setc and "C4^3" not in setc: cmap[lab]="C4"; continue
        if lab.startswith("C3") and "C3" in setc: cmap[lab]="C3"; continue
        if lab.startswith("S3") and "S6" in setc and "S3" not in setc: cmap[lab]="S6"; continue
        if lab in ("S10","S10^3") and lab in setc: cmap[lab]=lab; continue
        if lab.startswith("S4") and "S4" in setc and "S4^3" not in setc: cmap[lab]="S4"; continue
        if lab.startswith("σ") and "σ" in setc: cmap[lab]="σ"; continue
        cmap[lab]=lab
    return classes, cmap

def _parse_power(label: str, default_n: _Optional[int] = None) -> _Tuple[str, _Optional[int], int]:
    """
    操作ラベルから回転軸の次数と指数を解析します。

    概要:
        "C3^2" のような操作ラベルを解析し、操作の種類 ('C'/'S')、次数 (3)、指数 (2) を抽出します。
    詳細説明:
        ラベルに "^" が含まれる場合、ベースと指数に分割します。
        ベース部分から操作の種類（C, Sなど）と次数を抽出します。
        次数が数値でない場合は `default_n` を使用します。
    :param label: 解析する操作ラベル。
    :type label: str
    :param default_n: 次数が解析できない場合のデフォルト値。
    :type default_n: Optional[int]
    :returns: (操作の種類, 次数, 指数) のタプル。
    :rtype: Tuple[str, Optional[int], int]
    """
    if "^" in label:
        base,p = label.split("^",1); p=int(p)
    else:
        base,p = label,1
    kind=base[0]
    try: n=int(base[1:])
    except ValueError: n=default_n
    return kind,n,p

def polar_trace_from_label(lab: str) -> float:
    """
    操作ラベルから、3次元の直交変換行列のトレースを計算します。

    概要:
        与えられた対称操作のラベルに基づいて、その操作の3x3行列のトレース（対角成分の和）を返します。
    詳細説明:
        'E' は3、'i' は-3。
        鏡映操作 ('σ') は1。
        回転操作 ('C') は `1 + 2*cos(theta)` を計算します。
        回映操作 ('S') は `2*cos(theta) - 1` を計算します。
        角度 `theta` はラベルから解析される次数 `n` と指数 `p` に基づきます (`2*pi*p/n`)。
    :param lab: 対称操作のラベル。
    :type lab: str
    :returns: その操作の3x3行列のトレース。
    :rtype: float
    """
    if lab=="E": return 3.0
    if lab=="i": return -3.0
    if lab.startswith("σ"): return 1.0
    if lab.startswith("C"):
        _,n,p=_parse_power(lab); 
        if n is None: return 0.0 # Should not happen for valid C_n
        ang=2*_np.pi*(p%n)/n
        return 1.0 + 2.0*_np.cos(ang)
    if lab.startswith("S"):
        _,n,p=_parse_power(lab); 
        if n is None: return 0.0 # Should not happen for valid S_n
        ang=2*_np.pi*(p%n)/n
        return 2.0*_np.cos(ang) - 1.0
    return 0.0

def det_from_label(lab: str) -> int:
    """
    操作ラベルから、その操作の行列式を返します。

    概要:
        与えられた対称操作のラベルに基づいて、その操作の行列式 (1または-1) を返します。
    詳細説明:
        'E' や 'C' (純粋回転) 操作は行列式が1です。
        'i' (反転)、'σ' (鏡映)、'S' (回映) 操作は行列式が-1です。
    :param lab: 対称操作のラベル。
    :type lab: str
    :returns: 行列式 (1または-1)。
    :rtype: int
    """
    if lab=="E": return 1
    if lab.startswith("C"): return 1
    return -1  # i, σ*, S*

def gamma_3N_characters(coords: _np.ndarray, op_mats: _List[_np.ndarray],
                        labels: _List[str], tol: float = 1e-5) -> _Dict[str, float]:
    """
    分子の全並進振動モード (Γ_3N) の指標を計算します。

    概要:
        与えられた原子座標、対称操作行列、およびそれらのラベルを使用して、
        分子の全振動モードの可約表現の指標を計算します。
    詳細説明:
        各対称操作Rに対して、その操作によって移動しない原子を特定します。
        移動しない原子については、Rのトレースが指標に貢献します。
        全ての操作についてこれを合計し、各ラベルに対応する総指標を算出します。
    :param coords: 原子座標のNumPy配列 (N, 3)。
    :type coords: numpy.ndarray
    :param op_mats: 対称操作行列のリスト。
    :type op_mats: List[numpy.ndarray]
    :param labels: 各操作行列に対応するラベルのリスト。
    :type labels: List[str]
    :param tol: 原子位置の比較に用いる許容誤差。
    :type tol: float
    :returns: 各操作ラベルに対するΓ_3Nの指標値の辞書。
    :rtype: Dict[str, float]
    """
    chars=_defaultdict(float)
    for R,lab in zip(op_mats, labels):
        chi=0.0
        for r in coords:
            rp = R @ r
            if _np.linalg.norm(rp - r) < tol:
                chi += float(_np.trace(R))
        chars[lab]+=chi
    return dict(chars)

def gamma_trans_characters(labels: _List[str], class_map: _Dict[str,str]) -> _Dict[str,float]:
    """
    分子の並進振動モード (Γ_T) の指標を計算します。

    概要:
        対称操作のラベルとクラスマップを使用して、分子の並進モードの可約表現の指標を計算します。
    詳細説明:
        各操作ラベルについて、その操作の3x3行列のトレースを `polar_trace_from_label` を用いて計算します。
        これらのトレース値をクラスマップに従って集約し、各クラスのΓ_T指標を求めます。
    :param labels: 各対称操作のラベルのリスト。
    :type labels: List[str]
    :param class_map: 生ラベルから指標表クラス名へのマッピング辞書。
    :type class_map: Dict[str, str]
    :returns: 各クラスに対するΓ_Tの指標値の辞書。
    :rtype: Dict[str, float]
    """
    d=_defaultdict(float)
    for lab in labels:
        key=class_map.get(lab,lab)
        d[key]+=polar_trace_from_label(lab)
    return dict(d)

def gamma_rot_characters(labels: _List[str], class_map: _Dict[str,str]) -> _Dict[str,float]:
    """
    分子の回転振動モード (Γ_R) の指標を計算します。

    概要:
        対称操作のラベルとクラスマップを使用して、分子の回転モードの可約表現の指標を計算します。
    詳細説明:
        各操作ラベルについて、その操作の3x3行列のトレースにその行列式 (`det_from_label`) を乗じた値を計算します。
        これは、回転操作 (det=1) にはトレースそのままを、回転-反転操作 (det=-1) にはトレースに-1を乗じた値を意味します。
        これらの値をクラスマップに従って集約し、各クラスのΓ_R指標を求めます。
    :param labels: 各対称操作のラベルのリスト。
    :type labels: List[str]
    :param class_map: 生ラベルから指標表クラス名へのマッピング辞書。
    :type class_map: Dict[str, str]
    :returns: 各クラスに対するΓ_Rの指標値の辞書。
    :rtype: Dict[str, float]
    """
    d=_defaultdict(float)
    for lab in labels:
        key=class_map.get(lab,lab)
        d[key]+= det_from_label(lab) * polar_trace_from_label(lab)
    return dict(d)

def reduce_to_classes(vec_by_label: _Dict[str,float], classes: _List[str], class_map: _Dict[str,str]) -> _Dict[str,float]:
    """
    ラベルごとの指標ベクトルを指標表のクラスに集約します。

    概要:
        詳細な操作ラベルに対応する指標値を、点群の指標表で定義されたクラス名に対応する指標値に集約します。
    詳細説明:
        `class_map` を使用して、各ラベルの指標値を対応するクラスに加算します。
        最終結果は、指標表の全てのクラス名を含む辞書として返され、対応する指標値がないクラスには0が割り当てられます。
    :param vec_by_label: 各操作ラベルに対する指標値の辞書。
    :type vec_by_label: Dict[str, float]
    :param classes: 指標表で定義されたクラス名のリスト。
    :type classes: List[str]
    :param class_map: 生ラベルから指標表クラス名へのマッピング辞書。
    :type class_map: Dict[str, str]
    :returns: 各クラスに対する集約された指標値の辞書。
    :rtype: Dict[str, float]
    """
    agg=_defaultdict(float)
    for lab,val in vec_by_label.items():
        key=class_map.get(lab,lab)
        agg[key]+=val
    return {c:agg.get(c,0.0) for c in classes}

def group_order(symbol: str) -> int:
    """
    点群の位数を返します。

    概要:
        指定された点群シンボルに対応する群の位数（要素数）を返します。
    詳細説明:
        `build_group` で構築可能な群については、実際に構築された操作の数を返します。
        `ABSTRACT_CLASS_SIZES` に定義されている抽象群については、その合計クラスサイズを返します。
        どちらにも対応しない場合は `ValueError` を発生させます。
    :param symbol: 点群シンボル。
    :type symbol: str
    :returns: 点群の位数。
    :rtype: int
    :raises ValueError: サポートされていない、または抽象定義されていない点群シンボルが指定された場合。
    """
    s=normalize_symbol(symbol)
    try:
        return len(build_group(s))
    except Exception:
        if s in ABSTRACT_CLASS_SIZES:
            return sum(ABSTRACT_CLASS_SIZES[s].values())
        raise ValueError(f"Group order for '{symbol}' is not available.")

def group_ops(symbol: str) -> _List[_np.ndarray]:
    """
    点群の全ての対称操作（3x3行列）を返します。

    概要:
        指定された点群シンボルに対応する全ての対称操作行列のリストを返します。
    詳細説明:
        `build_group` 関数を呼び出して操作行列を構築し、`snap_matrix` で正規化します。
        `build_group` で対応していない抽象群の場合は、`ValueError` が発生します。
    :param symbol: 点群シンボル。
    :type symbol: str
    :returns: 点群の全ての対称操作行列のリスト。
    :rtype: List[numpy.ndarray]
    :raises ValueError: 抽象群など、`build_group` で操作行列を構築できない場合。
    """
    s=normalize_symbol(symbol)
    mats = build_group(s)  # 例外で抽象群へ
    return [snap_matrix(M) for M in mats]

def decompose_irreps(chars_by_class: _Dict[str,float], pg: str,
                     class_sizes_override: _Optional[_Dict[str,int]]=None,
                     order_override: _Optional[int]=None) -> _Dict[str,int]:
    """
    クラスごとの指標ベクトルを既約表現へ直交分解します。

    概要:
        与えられた可約表現の指標ベクトルを、指定された点群の既約表現に分解し、
        各既約表現の多重度を計算します。
    詳細説明:
        群論の直交性定理 (`m_i = (1/h) * sum(chi_reducible(R) * chi_irreducible(R)* * N_R)`) を使用します。
        `h` は群の位数、`N_R` はクラスRの要素数、`chi(R)` はクラスRの指標です。
        クラスサイズと群の位数は、`class_sizes_override` や `order_override` で明示的に指定できます。
        指定がない場合、`group_order` と `classify_op_for_table` を用いて自動的に計算されます。
    :param chars_by_class: 各クラスに対する可約表現の指標値の辞書。
    :type chars_by_class: Dict[str, float]
    :param pg: 点群シンボル。
    :type pg: str
    :param class_sizes_override: クラスサイズを明示的に指定する辞書（クラス名: サイズ）。
    :type class_sizes_override: Optional[Dict[str, int]]
    :param order_override: 群の位数を明示的に指定する整数。
    :type order_override: Optional[int]
    :returns: 各既約表現名とその多重度（整数）の辞書。
    :rtype: Dict[str, int]
    :raises ValueError: 指定された点群の指標表が利用できない場合、または群の位数を特定できない場合。
    """
    tbl = character_table(pg); irreps = tbl["irreps"]
    if class_sizes_override is not None:
        class_sizes = _Counter(class_sizes_override)
        h = order_override if order_override is not None else sum(class_sizes.values())
    else:
        h = group_order(pg)
        ops = group_ops(pg)
        raw_labels = [classify_op_for_table(pg, R) for R in ops]
        _, cmap = class_aggregation(pg, raw_labels)
        class_sizes = _Counter(cmap.get(l,l) for l in raw_labels)
    mults={}
    for ir,row in irreps.items():
        s=0.0+0.0j
        for cl,chi_ir in row.items():
            s += class_sizes.get(cl,0) * _np.conj(chi_ir) * chars_by_class.get(cl,0.0)
        m = s / h
        mults[ir] = int(round(float(_np.real(m))))
    return mults

def pretty_irreps(pg: str, mults: _Dict[str,int]) -> str:
    """
    既約表現の多重度を見やすい和の形式で整形します。

    概要:
        既約表現の多重度を示す辞書を受け取り、それを "A1 + 2E + T2" のような
        人間が読みやすい文字列形式に変換します。
    詳細説明:
        一般的な点群の既約表現には標準的な並び順があります。
        この関数では、`order_map` を使用して、指定された点群の既約表現を
        その慣習的な順序で並べ替えます。
        多重度が1より大きい場合は `2E` のように係数を付け、1の場合は `A1` のように表現名を直接使用します。
        多重度が0の表現は省略されます。
    :param pg: 点群シンボル。
    :type pg: str
    :param mults: 各既約表現名とその多重度（整数）の辞書。
    :type mults: Dict[str, int]
    :returns: 整形された既約表現の和の文字列。
    :rtype: str
    """
    # 代表的な並び（見栄え用、未知は辞書順）
    order_map = {
        "C2v": ["A1","A2","B1","B2"],
        "C3v": ["A1","A2","E"],
        "C4v": ["A1","A2","B1","B2","E"],
        "C6v": ["A1","A2","B1","B2","E1","E2"],
        "C1": ["A"], "Ci":["Ag","Au"], "Cs":["A'","A''"], "Ch":["A'","A''"],
        "C2":["A","B"], "C3":["A","E"], "C4":["A","B","E"], "C6":["A","B","E1","E2"],
        "C2h":["Ag","Bg","Au","Bu"],
        "C3h":["A'","A''","E'","E''"],
        "C4h":[f"Γ{a}g" for a in (0,1,2,3)] + [f"Γ{a}u" for a in (0,1,2,3)],
        "D2":["A","B1","B2","B3"],
        "D2h":["Ag","B1g","B2g","B3g","Au","B1u","B2u","B3u"],
        "D3":["A1","A2","E"], "D3d":["A1g","A2g","Eg","A1u","A2u","Eu"], "D3h":["A1'","A2'","E'","A1''","A2''","E''"],
        "D4":["A1","A2","B1","B2","E"], "D2d":["A1","A2","B1","B2","E"], "D4h":["A1g","A2g","B1g","B2g","Eg","A1u","A2u","B1u","B2u","Eu"],
        "D6":["A1","A2","B1","B2","E1","E2"], "D6h":["A1g","A2g","B1g","B2g","E1g","E2g","A1u","A2u","B1u","B2u","E1u","E2u"],
        "T":["A","E","T"], "Td":["A1","A2","E","T1","T2"], "Th":["Ag","Au","Eg","Eu","Tg","Tu"],
        "O":["A1","A2","E","T1","T2"], "Oh":["A1g","A2g","Eg","T1g","T2g","A1u","A2u","Eu","T1u","T2u"],
        "I":["A","T1","T2","G","H"],
        "Ih":["Ag","T1g","T2g","Gg","Hg","Au","T1u","T2u","Gu","Hu"],
    }
    seq = order_map.get(normalize_symbol(pg), sorted(character_table(pg)["irreps"].keys()))
    parts=[]
    for ir in seq:
        m=mults.get(ir,0)
        if m>0: parts.append(f"{m}{ir}" if m>1 else ir)
    return " + ".join(parts) if parts else "0"
