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