bz_draw.py ダウンロード/コピー
bz_draw.py
bz_draw.py
1#!/usr/bin/env python3
2"""
3第一ブリルアンゾーンまたは実空間のウィグナー・ザイツセル描画スクリプト。
4
5このスクリプトは、指定された結晶学的パラメータと格子タイプに基づいて、
6第一ブリルアンゾーン(逆空間)またはウィグナー・ザイツセル(実空間)を3Dで可視化します。
7さらに、周囲の周期的なセルをタイリング表示する機能、高対称点のパス表示(逆空間の場合)、
8およびインタラクティブなパラメータ調整機能(スライダーとApplyボタン)を提供します。
9
10:doc:`bz_draw_usage`
11"""
12import argparse
13import numpy as np
14import sys, math, functools
15import matplotlib.pyplot as plt
16from matplotlib.widgets import Slider, Button
17from mpl_toolkits.mplot3d.art3d import Poly3DCollection
18from scipy.spatial import Voronoi
19import warnings
20warnings.filterwarnings(
21 "ignore",
22 category=DeprecationWarning,
23 module=r"spglib\.spglib",
24 message=r"dict interface \(SpglibDataset\['.*'\]\) is deprecated"
25)
26
27# ================== 幾何ユーティリティ ==================
28def build_conventional_basis(a, b, c, alpha, beta, gamma):
29 """
30 従来の結晶学的パラメータから直交座標系における格子基底ベクトルを構築します。
31
32 最初の基底ベクトルをx軸に沿わせ、2番目の基底ベクトルをxy平面に置き、
33 3番目の基底ベクトルを正のz成分を持つように配置します。
34
35 :param a: 格子定数 a (Å)
36 :type a: float
37 :param b: 格子定数 b (Å)
38 :type b: float
39 :param c: 格子定数 c (Å)
40 :type c: float
41 :param alpha: 角度 alpha (度)
42 :type alpha: float
43 :param beta: 角度 beta (度)
44 :type beta: float
45 :param gamma: 角度 gamma (度)
46 :type gamma: float
47 :returns: 構築された3x3の格子基底行列。各列が基底ベクトル (a_vec, b_vec, c_vec) を表します。
48 :rtype: numpy.ndarray
49 """
50 ar, br, gr = np.radians([alpha, beta, gamma])
51 a_vec = np.array([a, 0.0, 0.0])
52 b_vec = np.array([b*np.cos(gr), b*np.sin(gr), 0.0])
53 cx = c*np.cos(br)
54 denom = np.sin(gr) if abs(np.sin(gr)) > 1e-12 else 1e-12
55 cy = c*(np.cos(ar) - np.cos(br)*np.cos(gr)) / denom
56 cz2 = c**2 - cx**2 - cy**2
57 cz = math.sqrt(max(cz2, 0.0))
58 c_vec = np.array([cx, cy, cz])
59 return np.column_stack((a_vec, b_vec, c_vec)) # 列: a,b,c
60
61def primitive_from_centering(A_conv, lattice_tag):
62 """
63 従来の格子基底と格子タイプから原始格子基底を計算します。
64
65 与えられた従来の格子基底に対し、指定されたセンタリングタイプ(P, I, Fなど)に
66 対応する変換行列を適用して原始格子基底を導出します。
67
68 :param A_conv: 従来の3x3格子基底行列(列が基底ベクトル)
69 :type A_conv: numpy.ndarray
70 :param lattice_tag: 格子タイプを示す文字列 (例: 'P', 'I', 'F', 'SC', 'BCC', 'FCC', 'A', 'B', 'C')
71 :type lattice_tag: str
72 :returns: 原始3x3格子基底行列(列が原始基底ベクトル)
73 :rtype: numpy.ndarray
74 :raises ValueError: 未知の格子タイプまたは未サポートの格子タイプが指定された場合
75 """
76 tag = lattice_tag.strip().upper()
77 alias = {'SC':'P','P':'P','BCC':'I','I':'I','FCC':'F','F':'F','A':'A','B':'B','C':'C'}
78 if tag not in alias: raise ValueError(f"Unknown lattice '{lattice_tag}'")
79 tag = alias[tag]
80 if tag == 'P':
81 P = np.eye(3)
82 elif tag == 'I':
83 P = np.array([[-0.5, 0.5, 0.5],
84 [ 0.5, -0.5, 0.5],
85 [ 0.5, 0.5, -0.5]])
86 elif tag == 'F':
87 P = np.array([[0.0, 0.5, 0.5],
88 [0.5, 0.0, 0.5],
89 [0.5, 0.5, 0.0]])
90 elif tag == 'R':
91 P = np.array([[ 2/3, -1/3, -1/3],
92 [ 1/3, 1/3, -2/3],
93 [ 1/3, 1/3, 1/3]])
94 elif tag == 'A':
95 P = np.array([[1.0, 0.0, 0.0],
96 [0.0, 0.5, 0.5],
97 [0.0, -0.5, 0.5]])
98 elif tag == 'B':
99 P = np.array([[0.5, 0.0, 0.5],
100 [0.0, 1.0, 0.0],
101 [0.5, 0.0, -0.5]])
102 elif tag == 'C':
103 P = np.array([[ 0.5, 0.5, 0.0],
104 [ 0.5, -0.5, 0.0],
105 [ 0.0, 0.0, 1.0]])
106 else:
107 raise ValueError(f"Unsupported lattice '{lattice_tag}'")
108 return A_conv @ P # 列が原始基底
109
110def order_polygon_vertices_on_plane(P):
111 """
112 平面上にある多角形の頂点を反時計回りに並べ替えるインデックスを返します。
113
114 頂点の重心を計算し、SVD(特異値分解)を用いて平面の法線ベクトルを決定します。
115 その後、基準ベクトルと法線に垂直なベクトルを生成し、これらの座標系で各頂点の角度を計算してソートします。
116 この順序はMatplotlibで多角形を描画する際に重要です。
117
118 :param P: 多角形の頂点群を表すNx3のNumPy配列。
119 :type P: numpy.ndarray
120 :returns: 頂点を反時計回りに並べ替えるためのインデックスの配列。
121 :rtype: numpy.ndarray
122 """
123 C = P.mean(axis=0); Q = P - C
124 if len(P) <= 2: return np.arange(len(P))
125 _, _, Vt = np.linalg.svd(Q, full_matrices=False)
126 n = Vt[-1]
127 ref = Q[0]
128 if np.linalg.norm(np.cross(ref, n)) < 1e-12:
129 ref = Q[1] if len(Q)>1 else np.array([1.0,0.0,0.0])
130 u = ref - np.dot(ref, n)*n
131 if np.linalg.norm(u) < 1e-12:
132 tmp = np.array([1.0,0.0,0.0]);
133 if abs(np.dot(tmp,n)) > 0.9: tmp = np.array([0.0,1.0,0.0])
134 u = tmp - np.dot(tmp, n)*n
135 u /= np.linalg.norm(u)
136 v = np.cross(n, u)
137 ang = np.arctan2(Q @ v, Q @ u)
138 return np.argsort(ang)
139
140def reciprocal_points_from_basis(B, n):
141 """
142 与えられた基底ベクトルとシェル数に基づいて、格子点または逆格子点の配列を生成します。
143
144 各基底ベクトルの整数倍(-nからnまで)の組み合わせで、中心の点を含む広範囲の点を生成します。
145 これはVoronoi図の計算に必要な点群を提供します。
146
147 :param B: 3x3の基底ベクトル行列(列が基底ベクトル)
148 :type B: numpy.ndarray
149 :param n: 各基底ベクトルに沿って考慮するシェル数(例: n=1なら-1,0,1の係数を使用)
150 :type n: int
151 :returns: 生成された点群のNx3のNumPy配列
152 :rtype: numpy.ndarray
153 """
154 pts = []
155 b1,b2,b3 = B[:,0], B[:,1], B[:,2]
156 for i in range(-n,n+1):
157 for j in range(-n,n+1):
158 for k in range(-n,n+1):
159 pts.append(i*b1 + j*b2 + k*b3)
160 return np.array(pts)
161
162def get_cell_faces_and_neighbors(vor, point_index):
163 """
164 Voronoi図の中心点に関連する面の頂点インデックスと隣接点のインデックスを取得します。
165
166 Voronoiオブジェクトのridge_pointsとridge_verticesを走査し、
167 指定された点に関連する有限な面(-1を含まない、3頂点以上)を抽出します。
168
169 :param vor: scipy.spatial.Voronoiオブジェクト
170 :type vor: scipy.spatial.Voronoi
171 :param point_index: 中心となる点の、Voronoi計算に用いた点群におけるインデックス
172 :type point_index: int
173 :returns: (faces, neighbors) のタプル。
174 facesは各面の頂点インデックスのリストのリスト、
175 neighborsは各面に対応する隣接点のインデックスのリスト。
176 :rtype: tuple[list[numpy.ndarray], list[int]]
177 """
178 faces, neighbors = [], []
179 for (p,q), v_idx in zip(vor.ridge_points, vor.ridge_vertices):
180 if point_index not in (p,q): continue
181 if -1 in v_idx or len(v_idx) < 3: continue
182 faces.append(np.array(v_idx))
183 neighbors.append(q if p==point_index else p)
184 return faces, neighbors
185
186def cell_polygons(vor, point_index):
187 """
188 Voronoi図の中心点に関連する多角形(セル面)と隣接点のリストを生成します。
189
190 `get_cell_faces_and_neighbors` を利用して面の頂点インデックスを取得し、
191 `order_polygon_vertices_on_plane` で頂点を反時計回りに順序付けて多角形オブジェクトを構築します。
192
193 :param vor: scipy.spatial.Voronoiオブジェクト
194 :type vor: scipy.spatial.Voronoi
195 :param point_index: 中心となる点の、Voronoi計算に用いた点群におけるインデックス
196 :type point_index: int
197 :returns: (polygons, neighbors) のタプル。
198 polygonsは各面の頂点座標のNx3 NumPy配列のリスト、
199 neighborsは各面に対応する隣接点のインデックスのリスト。
200 :rtype: tuple[list[numpy.ndarray], list[int]]
201 """
202 polys = []
203 faces, neighbors = get_cell_faces_and_neighbors(vor, point_index)
204 for f in faces:
205 poly = vor.vertices[f]
206 poly = poly[order_polygon_vertices_on_plane(poly)]
207 polys.append(poly)
208 return polys, neighbors
209
210# ================== SeeK-path(キャッシュ付き) ==================
211def _key_from_Aprim(A_prim, symprec):
212 """
213 原始格子基底行列と対称操作の許容誤差からキャッシュキーを生成します。
214
215 `functools.lru_cache` で使用するためのキーです。
216 浮動小数点数の比較を安定させるため、行列の要素は指定された精度で丸められます。
217
218 :param A_prim: 原始3x3格子基底行列(列が基底ベクトル)
219 :type A_prim: numpy.ndarray
220 :param symprec: 対称操作の許容誤差
221 :type symprec: float
222 :returns: キャッシュキーとして使用されるタプル
223 :rtype: tuple
224 """
225 # 行が a1,a2,a3 のフラット(数値は丸めてキャッシュ安定化)
226 Arows = A_prim.T
227 return (float(symprec),) + tuple(np.round(Arows.ravel(), 12))
228
229@functools.lru_cache(maxsize=128)
230def _seekpath_cached(key):
231 """
232 seekpath.get_path を呼び出し、結果をキャッシュする内部関数です。
233
234 `functools.lru_cache` デコレータにより、同じ入力(キャッシュキー)に対しては
235 実際に `seekpath` を呼び出さずに、キャッシュされた結果を返します。
236 これにより、インタラクティブな描画での再計算を高速化します。
237
238 :param key: `_key_from_Aprim` で生成されたキャッシュキー
239 :type key: tuple
240 :returns: (Bstd, kpts_abs, path) のタプル。
241 Bstdは標準逆格子基底行列、
242 kpts_absは高対称点の絶対座標辞書、
243 pathは高対称点パスのリスト。
244 :rtype: tuple[numpy.ndarray, dict[str, numpy.ndarray], list[tuple[str, str]]]
245 """
246 symprec = key[0]
247 Arows = np.array(key[1:]).reshape(3,3)
248 import seekpath
249 cell_tuple = (Arows, np.array([[0.0,0.0,0.0]]), [1])
250 res = seekpath.get_path(cell_tuple, with_time_reversal=True, symprec=symprec)
251 B_rows = np.array(res['reciprocal_primitive_lattice'])
252 Bstd = B_rows.T # 列が b1,b2,b3(2π含む)
253 kpts_abs = {lbl: Bstd @ np.array(frac) for lbl, frac in res['point_coords'].items()}
254 path = res['path']
255 return Bstd, kpts_abs, path
256
257def bz_and_kpath_from_seekpath_cached(A_prim, symprec=1e-7):
258 """
259 `seekpath` を用いて、第一ブリルアンゾーンの標準逆格子基底、高対称点、およびパスを取得します。
260
261 `_seekpath_cached` を通じて `functools.lru_cache` によるキャッシュを利用するため、
262 同じ原始格子基底に対しては `seekpath` の計算がスキップされ、高速に結果を返します。
263
264 :param A_prim: 原始3x3格子基底行列(列が基底ベクトル)
265 :type A_prim: numpy.ndarray
266 :param symprec: 対称操作の許容誤差
267 :type symprec: float
268 :returns: (Bstd, kpts_abs, path) のタプル。
269 Bstdは標準逆格子基底行列、
270 kpts_absは高対称点の絶対座標辞書、
271 pathは高対称点パスのリスト。
272 :rtype: tuple[numpy.ndarray, dict[str, numpy.ndarray], list[tuple[str, str]]]
273 """
274 key = _key_from_Aprim(A_prim, symprec)
275 return _seekpath_cached(key)
276
277def pretty_label(lbl):
278 """
279 高対称点のラベルを整形します(例: "GAMMA" を "Γ" に変換)。
280
281 :param lbl: 整形前の高対称点ラベル文字列
282 :type lbl: str
283 :returns: 整形された高対称点ラベル文字列
284 :rtype: str
285 """
286 return lbl.replace("GAMMA", "Γ")
287
288# ================== 着色&透明度ユーティリティ ==================
289def color_checker(i,j,k):
290 """
291 三次元インデックス (i,j,k) に基づいてチェッカーボード状の色を返します。
292
293 各インデックスの偶奇性から0から7までのインデックスを生成し、
294 定義されたパレットから色を選択します。
295
296 :param i: 第1軸方向のインデックス
297 :type i: int
298 :param j: 第2軸方向のインデックス
299 :type j: int
300 :param k: 第3軸方向のインデックス
301 :type k: int
302 :returns: HEX形式の色コード文字列 (例: '#1f77b4')
303 :rtype: str
304 """
305 palette = ['#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#7f7f7f']
306 idx = ((i & 1) << 0) | ((j & 1) << 1) | ((k & 1) << 2) # 0..7
307 return palette[idx]
308
309def color_hash(i,j,k, cmap=plt.cm.tab20):
310 """
311 三次元インデックス (i,j,k) に基づいてハッシュ値から色を生成します。
312
313 与えられた整数インデックスから一意のハッシュ値を生成し、
314 そのハッシュ値を正規化して指定されたカラーマップから色を抽出します。
315
316 :param i: 第1軸方向のインデックス
317 :type i: int
318 :param j: 第2軸方向のインデックス
319 :type j: int
320 :param k: 第3軸方向のインデックス
321 :type k: int
322 :param cmap: 使用するMatplotlibカラーマップ
323 :type cmap: matplotlib.colors.Colormap
324 :returns: RGBA形式の色タプル
325 :rtype: tuple
326 """
327 h = (i*73856093 ^ j*19349663 ^ k*83492791) & 0xffffffff
328 return cmap((h % 256)/255.0)
329
330def color_distance(t, rmax, cmap=plt.cm.viridis):
331 """
332 原点からの距離に基づいてグラデーション色を返します。
333
334 点の位置ベクトル `t` の原点からの距離を計算し、`rmax` で正規化して
335 指定されたカラーマップ `cmap` から色を抽出します。
336
337 :param t: 点の3D座標ベクトル
338 :type t: numpy.ndarray
339 :param rmax: 色のグラデーションの最大半径。これを超えると色が飽和します。
340 Noneまたは0以下の場合は常に最大値の色を返します。
341 :type rmax: float or None
342 :param cmap: 使用するMatplotlibカラーマップ
343 :type cmap: matplotlib.colors.Colormap
344 :returns: RGBA形式の色タプル
345 :rtype: tuple
346 """
347 r = np.linalg.norm(t); u = 0.0 if rmax is None or rmax<=0 else min(1.0, r/rmax)
348 return cmap(u)
349
350def alpha_fade(t, rmax, a_min=0.06, a_max=0.22, power=1.0):
351 """
352 原点からの距離に基づいて透明度をフェードさせる値を返します。
353
354 点の位置ベクトル `t` の原点からの距離が `rmax` に近づくにつれて、
355 透明度が `a_max` から `a_min` へとフェードアウトするように計算されます。
356 `power` パラメータでフェードの非線形性を調整できます。
357
358 :param t: 点の3D座標ベクトル
359 :type t: numpy.ndarray
360 :param rmax: 透明度フェードの最大半径。これを超えると透明度は `a_min` に近づきます。
361 Noneまたは0以下の場合は常に `a_max` を返します。
362 :type rmax: float or None
363 :param a_min: 最小透明度 (0.0=完全透明, 1.0=完全不透明)
364 :type a_min: float
365 :param a_max: 最大透明度 (0.0=完全透明, 1.0=完全不透明)
366 :type a_max: float
367 :param power: フェードの指数(例: 1.0=線形, 2.0=二乗)
368 :type power: float
369 :returns: 計算された透明度の値
370 :rtype: float
371 """
372 if rmax is None or rmax<=0: return a_max
373 r = np.linalg.norm(t); u = min(1.0, r/rmax); w = (1.0 - u)**power
374 return a_min + (a_max - a_min)*w
375
376# ================== 描画マネージャ(差分更新) ==================
377class SceneManager:
378 """
379 Matplotlibの3Dシーンを管理し、描画オブジェクト(アーティスト)の追加、クリア、更新を行います。
380
381 特に、描画のたびに既存のオブジェクトを効率的に削除し、新しいオブジェクトを追加することで、
382 インタラクティブな描画でのちらつきを抑え、高速な更新を可能にします。
383 """
384 def __init__(self, args):
385 """
386 シーンマネージャを初期化します。
387
388 MatplotlibのFigureと3D Axesを作成し、以前の描画オブジェクトを保持するためのリストを準備します。
389
390 :param args: コマンドライン引数を格納するargparse.Namespaceオブジェクト
391 :type args: argparse.Namespace
392 """
393 self.args = args
394 self.fig = plt.figure(figsize=(10,9))
395 self.ax = self.fig.add_subplot(111, projection='3d')
396 self.artists = [] # 前回描画したアーティストを保持(差し替え用)
397
398 def clear_artists(self):
399 """
400 前回描画したすべてのMatplotlibアーティストをシーンから削除します。
401
402 これにより、Axes自体は維持しつつ、内容だけを更新することができます。
403 """
404 # Axesは維持し、前回のアーティストだけ remove()
405 for a in self.artists:
406 try: a.remove()
407 except Exception: pass
408 self.artists = []
409
410 def add_artist(self, *objs):
411 """
412 新しいMatplotlibアーティストを管理リストに追加します。
413
414 描画されたアーティストは、後で `clear_artists` メソッドで効率的に削除できるように
415 このリストに格納されます。リストやタプルの形式で複数のアーティストを渡すことも可能です。
416
417 :param objs: シーンに追加するMatplotlibアーティストオブジェクト(一つまたは複数)
418 :type objs: matplotlib.artist.Artist or list or tuple
419 """
420 for o in objs:
421 if isinstance(o, (list, tuple)):
422 self.artists.extend(o)
423 elif o is not None:
424 self.artists.append(o)
425
426 def draw_scene(self, space, A_prim, Bstd, n_shell,
427 tile_nx, tile_ny, tile_nz, tile_rmax,
428 show_outside_points, highlight_face_points,
429 draw_connectors_to_faces, draw_kpath,
430 color_mode='checker', fade_power=1.0,
431 kpts_abs=None, kpath=None,
432 label_fontsize=10):
433 """
434 第一ブリルアンゾーンまたはウィグナー・ザイツセル、およびそのタイリングされたビューを描画します。
435
436 空間タイプ(実空間または逆空間)に応じてVoronoi図を構築し、
437 中心セル、面生成点、周期タイル、高対称点とパス(逆空間の場合のみ)を描画します。
438 既存のアーティストをクリアし、新しいアーティストを追加する差分更新方式を採用しています。
439
440 :param space: 描画する空間タイプ ('reciprocal' または 'real')
441 :type space: str
442 :param A_prim: 原始3x3格子基底行列(実空間の基底)
443 :type A_prim: numpy.ndarray
444 :param Bstd: 標準逆格子3x3基底行列(逆空間の基底、2π因子を含む)
445 :type Bstd: numpy.ndarray or None
446 :param n_shell: Voronoi計算のために考慮する格子点/逆格子点のシェル数
447 :type n_shell: int
448 :param tile_nx: x軸方向のタイルの最大数(0で非表示)
449 :type tile_nx: int
450 :param tile_ny: y軸方向のタイルの最大数(0で非表示)
451 :type tile_ny: int
452 :param tile_nz: z軸方向のタイルの最大数(0で非表示)
453 :type tile_nz: int
454 :param tile_rmax: タイル描画の半径上限(中心から距離 r <= rmax のセルのみ描画)
455 Noneまたは0以下の場合、距離による制限なし
456 :type tile_rmax: float or None
457 :param show_outside_points: セル外部の格子点/逆格子点を表示するかどうか
458 :type show_outside_points: bool
459 :param highlight_face_points: セルの面を定義する点を強調表示するかどうか
460 :type highlight_face_points: bool
461 :param draw_connectors_to_faces: 面を定義する点と原点を結ぶ破線を描画するかどうか
462 :type draw_connectors_to_faces: bool
463 :param draw_kpath: 高対称点パスを描画するかどうか(reciprocal空間のみ有効)
464 :type draw_kpath: bool
465 :param color_mode: タイルの色付けモード ('checker', 'byindex', 'distance')
466 :type color_mode: str
467 :param fade_power: 距離に応じた透明度フェードの指数
468 :type fade_power: float
469 :param kpts_abs: 高対称点の絶対座標を格納した辞書 (reciprocal空間の場合のみ)
470 :type kpts_abs: dict[str, numpy.ndarray] or None
471 :param kpath: 高対称点パスのリスト (reciprocal空間の場合のみ)
472 :type kpath: list[tuple[str, str]] or None
473 :param label_fontsize: 高対称点ラベルのフォントサイズ
474 :type label_fontsize: int
475 """
476 self.clear_artists()
477 ax = self.ax
478
479 if space == 'reciprocal':
480 G = reciprocal_points_from_basis(Bstd, n_shell)
481 vor = Voronoi(G)
482 idx_center = np.argmin(np.linalg.norm(G, axis=1))
483 polys, neighbor_idx = cell_polygons(vor, idx_center)
484 tile_basis = Bstd
485 label_axes = (r'$k_x$', r'$k_y$', r'$k_z$')
486 outside_pts = G
487 face_color_main = 'lightskyblue'
488 origin_label = 'Γ (origin)'
489 else:
490 R = reciprocal_points_from_basis(A_prim, n_shell)
491 vor = Voronoi(R)
492 idx_center = np.argmin(np.linalg.norm(R, axis=1))
493 polys, neighbor_idx = cell_polygons(vor, idx_center)
494 tile_basis = A_prim
495 label_axes = ('x','y','z')
496 outside_pts = R
497 face_color_main = 'peachpuff'
498 origin_label = 'origin'
499
500 # 外部点群
501 if show_outside_points:
502 mask = np.ones(len(outside_pts), dtype=bool); mask[idx_center] = False
503 sc = ax.scatter(outside_pts[mask,0], outside_pts[mask,1], outside_pts[mask,2],
504 s=10, color='gray', alpha=0.45,
505 label=('Reciprocal points' if space=='reciprocal' else 'Lattice points'))
506 self.add_artist(sc)
507
508 # 中心セル(外周のみ)
509 for poly in polys:
510 col = Poly3DCollection([poly], alpha=0.32, facecolor=face_color_main, edgecolor='none')
511 ax.add_collection3d(col)
512 self.add_artist(col)
513 ring = np.vstack([poly, poly[0]])
514 ln, = ax.plot(ring[:,0], ring[:,1], ring[:,2], color='k', linewidth=1.8)
515 self.add_artist(ln)
516
517 # 面生成点と点線
518 centers = outside_pts[np.array(neighbor_idx)] if len(neighbor_idx)>0 else np.empty((0,3))
519 if highlight_face_points and len(centers)>0:
520 sc2 = ax.scatter(centers[:,0], centers[:,1], centers[:,2], s=28, color='tab:blue', alpha=0.9, label='Face-defining points')
521 self.add_artist(sc2)
522 if draw_connectors_to_faces:
523 lines = []
524 for p in centers:
525 ln, = ax.plot([0,p[0]],[0,p[1]],[0,p[2]], linestyle=':', linewidth=1.2, color='k', alpha=0.9)
526 lines.append(ln)
527 self.add_artist(lines)
528
529 # 周期タイル
530 if any(n>0 for n in (tile_nx, tile_ny, tile_nz)):
531 b1,b2,b3 = tile_basis[:,0], tile_basis[:,1], tile_basis[:,2]
532 for i in range(-tile_nx, tile_nx+1):
533 for j in range(-tile_ny, tile_ny+1):
534 for k in range(-tile_nz, tile_nz+1):
535 if i==0 and j==0 and k==0: continue
536 t = i*b1 + j*b2 + k*b3
537 if tile_rmax is not None and np.linalg.norm(t) > tile_rmax + 1e-12:
538 continue
539 # 色
540 if color_mode == 'checker':
541 fc = color_checker(i,j,k)
542 elif color_mode == 'distance':
543 fc = color_distance(t, tile_rmax)
544 else:
545 fc = color_hash(i,j,k)
546 # 透明度
547 a = alpha_fade(t, tile_rmax, a_min=0.06, a_max=0.22, power=fade_power)
548 # セル描画
549 cols, lns = [], []
550 for poly in polys:
551 P = poly + t
552 c = Poly3DCollection([P], alpha=a, facecolor=fc, edgecolor='none')
553 ax.add_collection3d(c); cols.append(c)
554 ring = np.vstack([P, P[0]])
555 ln, = ax.plot(ring[:,0], ring[:,1], ring[:,2], color='#666666', linewidth=0.6); lns.append(ln)
556 self.add_artist(cols); self.add_artist(lns)
557
558 # 高対称点と k-path(逆格子のみ)
559 if space=='reciprocal' and kpts_abs:
560 texts, pts, segs = [], [], []
561 for lbl, k in kpts_abs.items():
562 sct = ax.scatter(k[0],k[1],k[2], s=40, color='crimson'); pts.append(sct)
563 txt = ax.text(k[0],k[1],k[2], " "+pretty_label(lbl), fontsize=label_fontsize, color='crimson'); texts.append(txt)
564 self.add_artist(pts); self.add_artist(texts)
565 if draw_kpath and kpath:
566 for a_lbl,b_lbl in kpath:
567 ka,kb = kpts_abs[a_lbl], kpts_abs[b_lbl]
568 seg = np.vstack([ka,kb])
569 ln, = ax.plot(seg[:,0], seg[:,1], seg[:,2], color='crimson', linewidth=1.6, alpha=0.85)
570 segs.append(ln)
571 self.add_artist(segs)
572
573 # 原点
574 origin = ax.scatter([0],[0],[0], color='k', s=50, label=origin_label)
575 self.add_artist(origin)
576
577 # 軸や凡例は1回だけ設定(差分更新では再設定しない)
578 basis = (Bstd if space=='reciprocal' else A_prim)
579 scale = 1.35 * np.linalg.norm(basis[:,0])
580 ax.set_xlim(-scale, scale); ax.set_ylim(-scale, scale); ax.set_zlim(-scale, scale)
581
582 if self.args.plot_limit is not None:
583 L = self.args.plot_limit
584 ax.set_xlim(-L, L)
585 ax.set_ylim(-L, L)
586 ax.set_zlim(-L, L)
587 ax.set_box_aspect([1,1,1])
588 ax.set_xlabel(label_axes[0]); ax.set_ylabel(label_axes[1]); ax.set_zlabel(label_axes[2])
589 ax.set_title('First Brillouin Zone (tiled)' if space=='reciprocal' else 'Wigner–Seitz cell (tiled)')
590 # 凡例は毎回作り直す(重複回避のため一旦消してから)
591 leg = ax.legend(loc='upper right'); self.add_artist(leg)
592
593 self.fig.tight_layout()
594 self.fig.canvas.draw_idle()
595
596# ================== 実行系 ==================
597def plot_once(args):
598 """
599 指定されたパラメータに基づいて、第一ブリルアンゾーンまたはウィグナー・ザイツセルを一度だけ描画します。
600
601 結晶学的パラメータから格子基底を構築し、`SceneManager` を使用してシーンを描画し、
602 結果をMatplotlibウィンドウに表示します。
603
604 :param args: コマンドライン引数を格納するargparse.Namespaceオブジェクト
605 :type args: argparse.Namespace
606 :raises SystemExit: `seekpath` の実行に失敗した場合
607 """
608 A_conv = build_conventional_basis(args.a, args.b, args.c, args.alpha, args.beta, args.gamma)
609 A_prim = primitive_from_centering(A_conv, args.lattice)
610
611 mgr = SceneManager(args)
612 if args.space == 'reciprocal':
613 try:
614 Bstd, kpts_abs, kpath = bz_and_kpath_from_seekpath_cached(A_prim)
615 except RuntimeError as e:
616 print(e, file=sys.stderr); sys.exit(1)
617 mgr.draw_scene('reciprocal', A_prim, Bstd, args.n_shell,
618 args.tile_nx, args.tile_ny, args.tile_nz, args.tile_rmax,
619 (not args.no_outside_points), (not args.no_highlight_face_points),
620 (not args.no_connectors), (not args.no_kpath),
621 color_mode=args.tile_color_mode, fade_power=args.fade_power,
622 kpts_abs=kpts_abs, kpath=kpath, label_fontsize=args.label_fontsize)
623 else:
624 mgr.draw_scene('real', A_prim, None, args.n_shell,
625 args.tile_nx, args.tile_ny, args.tile_nz, args.tile_rmax,
626 (not args.no_outside_points), (not args.no_highlight_face_points),
627 (not args.no_connectors), False,
628 color_mode=args.tile_color_mode, fade_power=args.fade_power,
629 kpts_abs=None, kpath=None, label_fontsize=args.label_fontsize)
630 plt.show()
631
632def plot_interactive_apply(args):
633 """
634 スライダーと「Apply」ボタンを備えたインタラクティブな描画インターフェースを提供します。
635
636 `a, b, c` の格子定数をスライダーで調整でき、「Apply」ボタンをクリックしたときにのみ
637 シーンが再描画されます。`seekpath` の結果はキャッシュされるため、
638 再描画時の計算コストが削減され、高速な更新が可能です。
639
640 :param args: コマンドライン引数を格納するargparse.Namespaceオブジェクト
641 :type args: argparse.Namespace
642 """
643 # 初期値
644 A_conv = build_conventional_basis(args.a, args.b, args.c, args.alpha, args.beta, args.gamma)
645 A_prim = primitive_from_centering(A_conv, args.lattice)
646 mgr = SceneManager(args)
647
648 def do_draw(a, b, c):
649 A_conv = build_conventional_basis(a, b, c, args.alpha, args.beta, args.gamma)
650 A_prim = primitive_from_centering(A_conv, args.lattice)
651 if args.space == 'reciprocal':
652 Bstd, kpts_abs, kpath = bz_and_kpath_from_seekpath_cached(A_prim) # キャッシュで高速化
653 mgr.draw_scene('reciprocal', A_prim, Bstd, args.n_shell,
654 args.tile_nx, args.tile_ny, args.tile_nz, args.tile_rmax,
655 (not args.no_outside_points), (not args.no_highlight_face_points),
656 (not args.no_connectors), (not args.no_kpath),
657 color_mode=args.tile_color_mode, fade_power=args.fade_power,
658 kpts_abs=kpts_abs, kpath=kpath, label_fontsize=args.label_fontsize)
659 else:
660 mgr.draw_scene('real', A_prim, None, args.n_shell,
661 args.tile_nx, args.tile_ny, args.tile_nz, args.tile_rmax,
662 (not args.no_outside_points), (not args.no_highlight_face_points),
663 (not args.no_connectors), False,
664 color_mode=args.tile_color_mode, fade_power=args.fade_power,
665 kpts_abs=None, kpath=None, label_fontsize=args.label_fontsize)
666
667 # 初回描画
668 do_draw(args.a, args.b, args.c)
669
670 # UI: スライダ + Apply ボタン(押したときのみ再描画)
671 plt.subplots_adjust(bottom=0.20)
672 ax_a = plt.axes([0.15, 0.08, 0.65, 0.03])
673 ax_b = plt.axes([0.15, 0.05, 0.65, 0.03])
674 ax_c = plt.axes([0.15, 0.02, 0.65, 0.03])
675 s_a = Slider(ax_a, 'a', 0.5*args.a, 2.0*args.a, valinit=args.a)
676 s_b = Slider(ax_b, 'b', 0.5*args.b, 2.0*args.b, valinit=args.b)
677 s_c = Slider(ax_c, 'c', 0.5*args.c, 2.0*args.c, valinit=args.c)
678
679 ax_btn = plt.axes([0.83, 0.02, 0.12, 0.09])
680 btn = Button(ax_btn, 'Apply')
681
682 busy = {'flag': False}
683 def on_apply(event):
684 if busy['flag']: # 処理中は無視
685 return
686 busy['flag'] = True
687 # ボタン表示を Redrawing... に変更
688 btn.label.set_text('Redrawing...')
689 btn.ax.set_facecolor('#dddddd')
690 mgr.fig.canvas.draw_idle()
691
692 try:
693 do_draw(s_a.val, s_b.val, s_c.val)
694 finally:
695 # ボタン表示を戻す
696 btn.label.set_text('Apply')
697 btn.ax.set_facecolor('#f0f0f0')
698 mgr.fig.canvas.draw_idle()
699 busy['flag'] = False
700
701 btn.on_clicked(on_apply)
702 plt.show()
703
704# ================== CLI ==================
705def main():
706 """
707 プログラムのエントリーポイントです。
708
709 コマンドライン引数を解析し、ユーザーの選択に応じて
710 第一ブリルアンゾーンまたはウィグナー・ザイツセルの描画機能
711 (一度きりの描画またはインタラクティブな描画)を呼び出します。
712 """
713 ap = argparse.ArgumentParser(description="Voronoi 多面体(第一BZ / Wigner–Seitz): キャッシュ・差分更新・Applyボタン対応")
714 ap.add_argument('--space', choices=['reciprocal','real'], default='reciprocal',
715 help="reciprocal: 第一BZ(SeeK-pathで高対称点) / real: 実空間 Wigner–Seitz")
716 ap.add_argument('--lattice', default='P', help="格子タイプ: P|SC, I|BCC, F|FCC, A, B, C")
717 ap.add_argument('--a', type=float, default=1.0); ap.add_argument('--b', type=float, default=1.0); ap.add_argument('--c', type=float, default=1.0)
718 ap.add_argument('--alpha', type=float, default=90.0); ap.add_argument('--beta', type=float, default=90.0); ap.add_argument('--gamma', type=float, default=90.0)
719 ap.add_argument('--n-shell', type=int, default=3, help='Voronoi 計算用に並べる格子点の範囲')
720 ap.add_argument('--tile-nx', type=int, default=0); ap.add_argument('--tile-ny', type=int, default=0); ap.add_argument('--tile-nz', type=int, default=0)
721 ap.add_argument('--tile-rmax', type=float, default=None,
722 help='タイル描画の半径上限(中心から距離 r ≤ rmax のセルのみ)')
723 ap.add_argument('--plot-limit', type=float, default=None,
724 help='軸の表示範囲を [-L, L] に強制的に制限する(内部座標、L=1.0で[-2pi/a, 2pi/a]に対応)')
725 ap.add_argument('--tile-color-mode', choices=['checker','byindex','distance'], default='checker',
726 help="タイル色: checker=チェッカー, byindex=ハッシュ, distance=距離グラデーション")
727 ap.add_argument('--fade-power', type=float, default=1.0, help='距離フェードの指数(1=線形, 2=二乗など)')
728 ap.add_argument('--no-outside-points', action='store_true')
729 ap.add_argument('--no-highlight-face-points', action='store_true')
730 ap.add_argument('--no-connectors', action='store_true')
731 ap.add_argument('--no-kpath', action='store_true')
732 ap.add_argument('--label-fontsize', type=int, default=10)
733 ap.add_argument('--interactive-apply', action='store_true', help='スライダ + Applyボタンで再描画')
734 ap.add_argument('--save', default=None, help='(一括描画時のみ)画像保存パス')
735
736 args = ap.parse_args()
737 if args.interactive_apply:
738 plot_interactive_apply(args)
739 else:
740 plot_once(args)
741
742if __name__ == "__main__":
743 main()