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

lattice_plane.py をダウンロード

lattice_plane.py
lattice_plane.py
  1"""
  2格子面 (hkl) を3Dで可視化するスクリプト
  3
  4このスクリプトは、指定された格子定数とミラー指数 (hkl) に基づいて、
  5結晶の単位格子とその中の (hkl) 面を3Dで描画します。
  6コマンドライン引数としてミラー指数h, k, lを受け入れ、それらの値に応じて面を可視化します。
  7描画されるのは単位格子に最も近い (hkl) 面の一部です。
  8
  9:doc:`lattice_plane_usage`
 10"""
 11import sys
 12import numpy as np
 13import matplotlib.pyplot as plt
 14from mpl_toolkits.mplot3d.art3d import Poly3DCollection
 15
 16# ─── 格子定数とミラー指数 ───────────────────────
 17a = 1.0  # a 軸長さ
 18b = 1.2  # b 軸長さ
 19c = 0.8  # c 軸長さ
 20alpha, beta, gamma = 90, 100, 110  # α, β, γ (度)
 21h, k, l = 1, 2, 3  # ミラー指数
 22# ───────────────────────────────────────────
 23
 24plane_color = 'cyan'
 25edge_color = 'blue'
 26plane_alpha = 0.1
 27
 28# フォントを MS Gothic に設定
 29plt.rcParams['font.family'] = 'MS Gothic'
 30
 31
 32def init_frac_cell():
 33    """
 34    分数座標系における単位格子の頂点と辺を初期化します。
 35
 36    この関数は、立方晶を想定した分数座標の8つの頂点と、
 37    それらの頂点を結ぶ12本の辺のインデックスのリストを生成します。
 38    これらの情報は、単位格子を表現するために使用されます。
 39
 40    :returns: (fverts, edges)
 41    :rtype: (numpy.ndarray, list[tuple[int, int]])
 42    :returns fverts: 分数座標系における単位格子の8つの頂点の配列。
 43    :returns edges: 単位格子の辺を定義する頂点インデックスのペアのリスト。
 44    """
 45    fverts = np.array([
 46        [0,0,0], [1,0,0], [1,1,0], [0,1,0],
 47        [0,0,1], [1,0,1], [1,1,1], [0,1,1]
 48    ])
 49    edges = [
 50        (0,1),(1,2),(2,3),(3,0),
 51        (4,5),(5,6),(6,7),(7,4),
 52        (0,4),(1,5),(2,6),(3,7)
 53    ]
 54    return fverts, edges
 55
 56
 57def compute_frac_polygon(h: int, k: int, l: int, fverts: np.ndarray, edges: list[tuple[int, int]]) -> np.ndarray | None:
 58    """
 59    ミラー指数 (hkl) によって定義される平面と単位格子の辺との交点を計算し、ポリゴンとして返します。
 60
 61    この関数は、平面の方程式 hx + ky + lz = 1 を使用して、
 62    単位格子を構成する各辺との交点を求めます。
 63    交点が辺の内部 (0 <= t <= 1) にある場合のみ採用し、
 64    それらの交点を多角形の頂点として収集します。
 65    最後に、これらの頂点を平面上の角度に基づいてソートし、
 66    凸多角形を形成するように並べ替えます。
 67
 68    :param h: ミラー指数 h。
 69    :type h: int
 70    :param k: ミラー指数 k。
 71    :type k: int
 72    :param l: ミラー指数 l。
 73    :type l: int
 74    :param fverts: 分数座標系における単位格子の頂点配列。
 75    :type fverts: numpy.ndarray
 76    :param edges: 単位格子の辺を定義する頂点インデックスのペアのリスト。
 77    :type edges: list[tuple[int, int]]
 78    :returns: 計算された交点を角度順に並べたポリゴン頂点の配列。交点が存在しない場合はNone。
 79    :rtype: numpy.ndarray or None
 80    """
 81    pts = []
 82    for i0, i1 in edges:
 83        v0, v1 = fverts[i0], fverts[i1]
 84        d = v1 - v0
 85        denom = h*d[0] + k*d[1] + l*d[2]
 86        if abs(denom) < 1e-8: # 平面と辺が平行な場合
 87            continue
 88        t = (1 - (h*v0[0] + k*v0[1] + l*v0[2])) / denom
 89        if 0 <= t <= 1:
 90            pts.append(v0 + t*d)
 91    if not pts:
 92        return None
 93    
 94    # 浮動小数点数の誤差により重複する点を考慮し、ユニークな点を抽出
 95    # rtolとatolを調整して重複判定の閾値を設定することも可能だが、ここではデフォルトを使用
 96    pts = np.unique(np.array(pts), axis=0)
 97
 98    # 交点をソートしてポリゴンを形成
 99    # 平面内の任意の中心点と、平面内の直交する2つのベクトルu, vを計算
100    cen = pts.mean(axis=0)
101    n = np.array([h, k, l], dtype=float)
102    n /= np.linalg.norm(n) # 法線ベクトル
103
104    # nに直交するベクトルuを計算
105    # nが[1,0,0]に平行でない限り、[1,0,0]との外積が有効
106    # nが[1,0,0]に平行な場合は[0,1,0]との外積を使用
107    if abs(np.dot(n, [1,0,0])) < 0.99: # ほぼ平行でない場合
108        u = np.cross(n, [1,0,0])
109    else: # ほぼ平行な場合
110        u = np.cross(n, [0,1,0])
111    u /= np.linalg.norm(u) # 単位ベクトル化
112    v = np.cross(n, u) # uとnに直交するvを計算
113    
114    # 各点を中心からのベクトルとu, vの内積を使って角度を計算しソート
115    ang = [np.arctan2(np.dot(p-cen, v), np.dot(p-cen, u)) for p in pts]
116    return pts[np.argsort(ang)]
117
118
119def main():
120    """
121    スクリプトのメインエントリポイント。単位格子と指定されたミラー指数面を描画します。
122
123    この関数は、コマンドライン引数からミラー指数 (h, k, l) を読み込み、
124    結晶の格子定数と角度に基づいて実空間の格子ベクトルを計算します。
125    その後、`init_frac_cell` と `compute_frac_polygon` を呼び出して、
126    分数座標系の単位格子頂点と (hkl) 面のポリゴンを取得します。
127    これらの分数座標を実空間座標に変換し、Matplotlibの3Dプロットを用いて、
128    単位格子、(hkl) 面、格子軸ラベル、およびミラーインターセプトの注釈を描画します。
129    最終的に、生成されたプロットを画像ファイルとして保存し、画面に表示します。
130
131    :returns: なし
132    :rtype: None
133    """
134    global h, k, l
135
136    # コマンドライン引数処理
137    argv = sys.argv
138    nargs = len(argv)
139    if nargs > 1: h = int(argv[1])
140    if nargs > 2: k = int(argv[2])
141    if nargs > 3: l = int(argv[3])
142
143    # ラジアン変換
144    alpha_r, beta_r, gamma_r = np.deg2rad(alpha), np.deg2rad(beta), np.deg2rad(gamma)
145    cos_alpha, cos_beta, cos_gamma = np.cos(alpha_r), np.cos(beta_r), np.cos(gamma_r)
146    sin_gamma = np.sin(gamma_r)
147
148    # 格子ベクトル (実座標系)
149    # 結晶学の慣例に従い、a1をx軸に沿わせ、a2をxy平面に配置、a3は空間に配置
150    a1 = np.array([a, 0.0, 0.0])
151    a2 = np.array([b * cos_gamma, b * sin_gamma, 0.0])
152    
153    # a3の成分計算
154    a3_x = c * cos_beta
155    a3_y = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma
156    # a3_zは三つのベクトルが作る体積が正になるように計算
157    a3_z_squared = 1 - cos_beta**2 - ((cos_alpha - cos_beta * cos_gamma)/sin_gamma)**2
158    a3_z = c * np.sqrt(max(0, a3_z_squared)) # 浮動小数点誤差で負になる可能性があるのでmax(0, ...)
159    a3 = np.array([a3_x, a3_y, a3_z])
160    
161    # 分数座標から実座標への変換行列M
162    M = np.column_stack((a1, a2, a3))
163
164    # 分数格子
165    fverts, edges = init_frac_cell()
166
167    # 平面ポリゴン
168    frac_poly = compute_frac_polygon(h, k, l, fverts, edges)
169    # 分数座標ポリゴンを実座標に変換
170    cart_poly = np.dot(M, frac_poly.T).T if frac_poly is not None else None
171    # 分数座標単位格子頂点を実座標に変換
172    cverts = np.dot(M, fverts.T).T
173
174    # プロット設定
175    fig = plt.figure(figsize=(6,6))
176    ax = fig.add_subplot(projection='3d')
177    ax.set_box_aspect((1,1,1)) # 3Dプロットのアスペクト比を揃える
178    ax.grid(False) # グリッドを非表示に
179    ax.set_axis_off() # 軸を非表示に
180    # 軸ペインの背景色を白に設定
181    ax.xaxis.set_pane_color((1,1,1,1))
182    ax.yaxis.set_pane_color((1,1,1,1))
183    ax.zaxis.set_pane_color((1,1,1,1))
184
185    # 単位格子描画
186    for i0, i1 in edges:
187        xs, ys, zs = zip(cverts[i0], cverts[i1])
188        ax.plot(xs, ys, zs, 'k-', lw=1)
189
190    # (hkl) 面描画
191    if cart_poly is not None and len(cart_poly) >= 3: # ポリゴンが有効かつ3点以上で構成されている場合
192        ax.add_collection3d(
193            Poly3DCollection(
194                [cart_poly], facecolors=plane_color,
195                edgecolors=edge_color, alpha=plane_alpha
196            )
197        )
198
199    # 格子軸ベクトルラベル(少し外側にオフセット)
200    offset_factor = 1.05
201    label_pos_a = a1 * offset_factor
202    label_pos_b = a2 * offset_factor
203    label_pos_c = a3 * offset_factor
204
205    def label_vec(vec: np.ndarray, label: str):
206        """格子軸ラベルをプロットに配置するヘルパー関数。"""
207        ax.text(*vec, label, fontsize=14,
208                fontfamily='MS Gothic', ha='center', va='center')
209
210    label_vec(label_pos_a, 'a')
211    label_vec(label_pos_b, 'b')
212    label_vec(label_pos_c, 'c')
213
214    # ミラーインターセプト注記(オフセット適用)
215    offset_alabel = -0.1
216    offset_blabel = -0.2
217    offset_clabel = -0.05
218
219    if h != 0: # hが0でない場合、a軸との交点が存在
220        pos_h = np.dot(M, np.array([1/h, 0, 0])) # 分数座標[1/h,0,0]を実座標に変換
221        pos_h_offset = [pos_h[0], pos_h[1] + offset_alabel, pos_h[2]]
222        ax.text(*pos_h_offset, f'$1/{{{h}}}$', fontsize=12,
223                fontfamily='MS Gothic', ha='center', va='bottom')
224
225    if k != 0: # kが0でない場合、b軸との交点が存在
226        pos_k = np.dot(M, np.array([0, 1/k, 0])) # 分数座標[0,1/k,0]を実座標に変換
227        pos_k_offset = [pos_k[0] + offset_blabel, pos_k[1], pos_k[2]]
228        ax.text(*pos_k_offset, f'$1/{{{k}}}$', fontsize=12,
229                fontfamily='MS Gothic', ha='right', va='center')
230
231    if l != 0: # lが0でない場合、c軸との交点が存在
232        pos_l = np.dot(M, np.array([0, 0, 1/l])) # 分数座標[0,0,1/l]を実座標に変換
233        pos_l_offset = [pos_l[0], pos_l[1] + offset_clabel, pos_l[2]]
234        ax.text(*pos_l_offset, f'$1/{{{l}}}$', fontsize=12,
235                fontfamily='MS Gothic', ha='right', va='bottom')
236
237    # 視点調整
238    ax.view_init(elev=20, azim=30) # 仰角と方位角を設定
239    ax.dist = 12 # カメラとオブジェクト間の距離を設定
240
241    plt.tight_layout() # レイアウトを自動調整
242    plt.savefig(f"lattice_plane{h}{k}{l}.png",
243                dpi=300, bbox_inches='tight', transparent=True) # 画像として保存
244    plt.show() # プロットを表示
245
246
247# ───────────────────────────────────────────
248# ここから実行
249# ───────────────────────────────────────────
250if __name__ == "__main__":
251    main()