#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
tkpointgroup.py
  - Pure NumPy point-group utilities extracted from point_group_inf.py
  - 提供機能（再利用向け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):
    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, angle_deg):
    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):
    n = _norm(normal).reshape(3,1)
    return np.eye(3) - 2.0*(n @ n.T)

def inversion():
    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):
    return min(_SNAP_VALUES, key=lambda v: abs(v - x))

def snap_matrix(M: np.ndarray) -> np.ndarray:
    """行列要素を代表値へスナップし、直交性/ det=±1 を保証。"""
    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) -> tuple:
    return tuple(np.round(np.asarray(M, float).flatten(), 10))

def unique_closure(generators: List[np.ndarray]) -> List[np.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):
    φ = math.radians(phi_deg)
    return np.array([math.sin(φ), -math.cos(φ), 0.0])
def diagonal_plane(phi_deg):
    return vertical_plane(phi_deg + 90.0)

# ========= 群ビルダー =========

def build_Cn(n):   return unique_closure([rot(ez, 360.0/n)])
def build_Cnv(n):  return unique_closure([rot(ez,360.0/n), mirror(vertical_plane(0.0))])
def build_Cnh(n):  return unique_closure([rot(ez,360.0/n), mirror(ez)])
def build_Sn(n):   return unique_closure([ mirror(ez) @ rot(ez,360.0/n) ])
def build_Dn(n):   return unique_closure([rot(ez,360.0/n), rot(ex,180.0)])
def build_Dnh(n):  return unique_closure([rot(ez,360.0/n), rot(ex,180.0), mirror(ez)])
def build_Dnd(n):  return unique_closure([rot(ez,360.0/n), rot(ex,180.0), mirror(diagonal_plane(0.0))])
def build_T():     return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0) ])
def build_Th():    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0), inversion() ])
def build_Td():
    S4z = mirror(ez) @ rot(ez,90.0)
    return unique_closure([ rot([1,1,1],120.0), rot(ex,180.0), S4z ])
def build_O():
    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():
    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:
    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:
    s = normalize_symbol(s)
    return _S_to_H.get(s, s)

def hm_to_schoenflies(h: str) -> 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]:
    return list(SUPPORTED)

def build_group(symbol: str) -> List[np.ndarray]:
    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:
    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]]:
    return [(classify_label(M), M) for M in mats]

def generators_for(symbol: str) -> List[Tuple[str, np.ndarray]]:
    """規約 generator（ラベル付き）。国際記号の多くは full ops にフォールバック。"""
    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=1e-8):
    """同一性判定用キー（トレランスつき）。"""
    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=1e-8):
    """一点 p からの軌道（等価点と、それを作る操作のインデックス）"""
    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=1e-8):
    """(q, op_ids) のリストからユニーク点を抽出。"""
    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=1e-8):
    """対称操作行列と x,y,z を受けて、重複削除した独立点を返す（代表点 & 生成操作のindex）。"""
    return dedup_points(orbit_for_point(ops, p, tol=tol), tol=tol)

# ========= 高レベルAPI =========

def get_all_operations(symbol: str) -> List[Tuple[str, np.ndarray]]:
    """点群シンボルから全要素（ラベル付き, 重複なし）を返す。"""
    mats = build_group(symbol)
    return label_elements(mats)

def get_generators(symbol: str) -> List[Tuple[str, np.ndarray]]:
    """点群シンボルから generator（ラベル付き）を返す。"""
    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]]:
    """generator 行列群から閉包をとり、ラベル付きで返す。"""
    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():
    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":{}},
})
# Ih を I から構成
def _fill_Ih_from_I():
    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）を返す。"""
    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): v=_np.asarray(v,float); n=_np.linalg.norm(v); return v/(n+1e-15)
def _close_to(v, cands, tol=0.1):
    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):
    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):
    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):
    return _np.arccos(float(_np.clip((_np.trace(R)-1)/2,-1,1)))

def classify_op_for_table(pg: str, R: _np.ndarray, tol=1e-6) -> str:
    """回転行列Rを、character table中のクラス名に対応づける簡易分類器。"""
    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 else "C3^2"
        if n==4:
            return "C4" if R[0,1]<0 else "C4^3"
        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"
            if _np.isclose(th,2*_np.pi/3,atol=1e-3): return "C3^2"
            if _np.isclose(th,_np.pi,atol=1e-3): return "C2"
            if _np.isclose(th,4*_np.pi/3,atol=1e-3): return "C3"
            if _np.isclose(th,5*_np.pi/3,atol=1e-3): 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]]:
    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=None):
    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:
    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
        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
        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:
    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=1e-5) -> _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]:
    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]:
    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]:
    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）。"""
    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

def group_ops(symbol: str) -> _List[_np.ndarray]:
    """点群の全操作（3x3）を返す。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]:
    """クラスごとの指標ベクトルを既約表現へ直交分解。"""
    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:
    """見やすい和表示（一般的な並び順）。"""
    # 代表的な並び（見栄え用、未知は辞書順）
    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"
