#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PyVistaベースの3D薄膜トランジスタ(TFT)構造ビューア。
本スクリプトは、オリジナルの `tft.py` の `--mode=model3d` 部分をPyVistaを使用して再実装したスタンドアロンアプリケーションです。
既存のコードは一切変更せず、適切なDocstringを追加しています。
機能
--------
- tklibへの依存がありません。
- Zバッファによる適切な3Dレンダリング(matplotlibのmplot3dよりもこの用途に適しています)。
- ランチャーフレンドリーな0/1コマンドラインオプション。
- 対話型表示および/または画像保存。
- TFT全体が視界に収まるように調整された初期カメラ。
単位
-----
入力ジオメトリはW, L, dgにSI単位(オリジナルスクリプトと同じ)を使用します。
補助的な厚さパラメータはnm単位です。描画はnm単位で行われます。
z方向は`--zscale`で視覚的に拡大できます。
関連リンク: :doc:`draw_tft3d_usage`
"""
from __future__ import annotations
import argparse
import sys
import traceback
from dataclasses import dataclass
from typing import List, Tuple
import numpy as np
try:
import pyvista as pv
except Exception:
print("\n[ERROR] pyvista の import に失敗しました。", file=sys.stderr)
print("以下に traceback を表示します。\n", file=sys.stderr)
traceback.print_exc()
print("to install pyvista: pip install pyvista")
print("(VTK も必要なら一緒に入ります。環境によっては 'pip install pyvista vtk' でもOK)")
input("\nPress ENTER to terminate>>\n")
sys.exit(1)
[ドキュメント]
@dataclass
class Layer:
"""
3D構造の単一レイヤーを表すデータクラス。
各レイヤーは名前、3Dバウンディングボックス、色、不透明度、およびオプションのグループラベルを持ちます。
:param name: `str` レイヤーの名前。
:param bounds: `Tuple[float, float, float, float, float, float]` レイヤーのバウンディングボックス (xmin, xmax, ymin, ymax, zmin, zmax)。
:param color: `str` レイヤーの色。
:param opacity: `float` レイヤーの不透明度 (0.0から1.0)。
:param group_label: `str | None` レイヤーのグループを示すオプションのラベル。
"""
name: str
bounds: Tuple[float, float, float, float, float, float] # xmin xmax ymin ymax zmin zmax
color: str
opacity: float = 1.0
group_label: str | None = None
@property
def center(self) -> Tuple[float, float, float]:
"""
レイヤーのバウンディングボックスの中心座標を返します。
:returns: `Tuple[float, float, float]` レイヤーの中心座標 (x, y, z)。
"""
xmin, xmax, ymin, ymax, zmin, zmax = self.bounds
return ((xmin + xmax) * 0.5, (ymin + ymax) * 0.5, (zmin + zmax) * 0.5)
[ドキュメント]
def positive_float(value: str) -> float:
"""
正の浮動小数点数を引数として解析するArgparse型チェッカー。
入力文字列を浮動小数点数に変換し、その値が正であることを検証します。
0以下の場合は `argparse.ArgumentTypeError` を発生させます。
:param value: `str` Argparseによって渡される入力文字列。
:returns: `float` 解析された正の浮動小数点数。
:raises argparse.ArgumentTypeError: 値が正でない場合。
"""
x = float(value)
if x <= 0:
raise argparse.ArgumentTypeError("value must be positive")
return x
[ドキュメント]
def parse_figsize(text: str) -> Tuple[float, float]:
"""
幅と高さの文字列(例: "8,6")をタプルとして解析するArgparse型チェッカー。
入力文字列をコンマで分割し、それぞれの部分を浮動小数点数に変換して幅と高さのタプルを生成します。
値が正であることを検証します。
:param text: `str` Argparseによって渡される "幅,高さ" 形式の入力文字列。
:returns: `Tuple[float, float]` 解析された (幅, 高さ) のタプル。
:raises argparse.ArgumentTypeError: フォーマットが不正な場合、または幅/高さの値が正でない場合。
"""
try:
w, h = [float(s.strip()) for s in text.split(",")]
except Exception as exc:
raise argparse.ArgumentTypeError("figsize must be like 8,6") from exc
if w <= 0 or h <= 0:
raise argparse.ArgumentTypeError("figsize values must be positive")
return w, h
[ドキュメント]
def build_parser() -> argparse.ArgumentParser:
"""
TFT 3Dビューアのコマンドライン引数を定義するArgparseパーサーを構築します。
TFTの幾何学的パラメータ、視覚的設定、カメラ制御、出力オプションなど、
スクリプトの動作を制御するための多くの引数を設定します。
:returns: `argparse.ArgumentParser` 構築されたArgparseパーサーインスタンス。
"""
parser = argparse.ArgumentParser(
description="Draw a 3D conceptual model of a thin-film transistor (TFT) using PyVista.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
# Original TFT geometry inputs
parser.add_argument("--W", type=positive_float, default=300.0e-6,
help="channel width W [m]")
parser.add_argument("--L", type=positive_float, default=50.0e-6,
help="channel length L [m]")
parser.add_argument("--dg", type=positive_float, default=100.0e-9,
help="gate-insulator thickness [m]")
# Additional structural parameters for drawing (nm)
parser.add_argument("--d_sub", type=positive_float, default=300.0,
help="substrate thickness [nm]")
parser.add_argument("--d_gate", type=positive_float, default=100.0,
help="gate thickness [nm]")
parser.add_argument("--d_semi", type=positive_float, default=100.0,
help="semiconductor thickness [nm]")
parser.add_argument("--d_sd", type=positive_float, default=100.0,
help="source/drain thickness [nm]")
parser.add_argument("--L_sd", type=positive_float, default=50.0e3,
help="source/drain length along x [nm]")
# Visual parameters
parser.add_argument("--zscale", type=positive_float, default=80.0,
help="visual magnification factor for z direction")
parser.add_argument("--figsize", type=parse_figsize, default=(8.0, 6.0),
help="figure size as width,height [inch]")
parser.add_argument("--dpi", type=int, default=120,
help="reference DPI to determine the render window size")
parser.add_argument("--title", type=str, default="Thin-film transistor structure",
help="window / figure title")
parser.add_argument("--background", type=str, default="white",
help="background color")
# Camera / display
parser.add_argument("--camera_scale", type=positive_float, default=1.9,
help="larger value = camera farther away")
parser.add_argument("--camera_margin", type=positive_float, default=1.08,
help="extra margin factor when fitting the initial camera")
parser.add_argument("--parallel", type=int, default=0, choices=[0, 1],
help="use parallel projection")
parser.add_argument("--edges", type=int, default=1, choices=[0, 1],
help="draw mesh edges")
parser.add_argument("--legend", type=int, default=1, choices=[0, 1],
help="show legend")
parser.add_argument("--labels", type=int, default=0, choices=[0, 1],
help="show layer labels")
parser.add_argument("--axes", type=int, default=0, choices=[0, 1],
help="show axes")
# Launcher-friendly 0/1 options
parser.add_argument("--save", type=int, default=0, choices=[0, 1],
help="save screenshot to --outfile")
parser.add_argument("--show", type=int, default=1, choices=[0, 1],
help="show interactive window")
parser.add_argument("--pause", type=int, default=1, choices=[0, 1],
help="pause before termination when --show=0")
parser.add_argument("--outfile", type=str, default="tft3d.png",
help="output image file when --save=1")
return parser
[ドキュメント]
def build_layers(args: argparse.Namespace) -> Tuple[List[Layer], dict]:
"""
コマンドライン引数に基づいて、TFT構造の各レイヤーを定義します。
基板、ゲート、絶縁体、半導体、ソース/ドレインなどのTFTの物理的な層を `Layer` オブジェクトのリストとして構築します。
各レイヤーのサイズと位置は、入力された幾何学的パラメータとZ方向の視覚的スケール因子に基づいて計算されます。
:param args: `argparse.Namespace` コマンドライン引数を格納するオブジェクト。
:returns: `Tuple[List[Layer], dict]` 構築された `Layer` オブジェクトのリストと、計算された寸法情報を含む辞書のタプル。
"""
W_nm = args.W * 1.0e9
L_nm = args.L * 1.0e9
d_ins_nm = args.dg * 1.0e9
x_tot = 2.0 * args.L_sd + L_nm
y_tot = W_nm
c_sub = "cyan"
c_gate = "silver"
c_ins = "lightblue"
c_semi = "orange"
c_sd = "silver"
layers: List[Layer] = []
z = 0.0
layers.append(Layer(
"substrate",
(0.0, x_tot, 0.0, y_tot, z, z + args.d_sub * args.zscale),
c_sub,
1.0,
"substrate",
))
z += args.d_sub
layers.append(Layer(
"gate",
(0.0, x_tot, 0.0, y_tot, z * args.zscale, (z + args.d_gate) * args.zscale),
c_gate,
1.0,
"gate / source / drain",
))
z += args.d_gate
layers.append(Layer(
"insulator",
(0.0, x_tot, 0.0, y_tot, z * args.zscale, (z + d_ins_nm) * args.zscale),
c_ins,
1.0,
"insulator",
))
z += d_ins_nm
layers.append(Layer(
"semiconductor",
(0.0, x_tot, 0.0, y_tot, z * args.zscale, (z + args.d_semi) * args.zscale),
c_semi,
1.0,
"semiconductor",
))
z += args.d_semi
layers.append(Layer(
"source",
(0.0, args.L_sd, 0.0, y_tot, z * args.zscale, (z + args.d_sd) * args.zscale),
c_sd,
0.85,
"gate / source / drain",
))
layers.append(Layer(
"drain",
(x_tot - args.L_sd, x_tot, 0.0, y_tot, z * args.zscale, (z + args.d_sd) * args.zscale),
c_sd,
0.85,
"gate / source / drain",
))
z += args.d_sd
dims = {
"W_nm": W_nm,
"L_nm": L_nm,
"d_ins_nm": d_ins_nm,
"x_tot": x_tot,
"y_tot": y_tot,
"z_tot_nm": z,
"z_tot_vis": z * args.zscale,
}
return layers, dims
[ドキュメント]
def get_scene_bounds(layers: List[Layer], margin_xy: float = 0.04, margin_z: float = 0.06) -> Tuple[float, float, float, float, float, float]:
"""
すべてのTFTレイヤーを含むシーン全体の最小/最大バウンディングボックスを計算します。
各レイヤーのバウンディングボックスを走査し、シーン全体のX, Y, Z方向の最小値と最大値を決定します。
指定されたマージンファクターを適用して、ビューポートに余白を追加します。
:param layers: `List[Layer]` TFT構造の `Layer` オブジェクトのリスト。
:param margin_xy: `float` X-Y平面におけるバウンディングボックスの追加マージン率。
:param margin_z: `float` Z方向におけるバウンディングボックスの追加マージン率。
:returns: `Tuple[float, float, float, float, float, float]` (xmin, xmax, ymin, ymax, zmin, zmax) の形式でシーンのバウンディングボックス。
"""
xmin = min(layer.bounds[0] for layer in layers)
xmax = max(layer.bounds[1] for layer in layers)
ymin = min(layer.bounds[2] for layer in layers)
ymax = max(layer.bounds[3] for layer in layers)
zmin = min(layer.bounds[4] for layer in layers)
zmax = max(layer.bounds[5] for layer in layers)
dx = max(xmax - xmin, 1.0)
dy = max(ymax - ymin, 1.0)
dz = max(zmax - zmin, 1.0)
mx = margin_xy * dx
my = margin_xy * dy
mz = margin_z * dz
return (xmin - mx, xmax + mx, ymin - my, ymax + my, zmin - mz, zmax + mz)
[ドキュメント]
def set_camera_direction(plotter: pv.Plotter, focal: Tuple[float, float, float], maxdim: float,
direction: Tuple[float, float, float], viewup: Tuple[float, float, float],
camera_scale: float, bounds: Tuple[float, float, float, float, float, float],
parallel: int, margin: float = 1.08) -> None:
"""
PyVistaのカメラ位置と視点方向を設定します。
指定された焦点、カメラ距離、方向、ビューアップベクトルに基づいてカメラを設定します。
並行投影の有効/無効を切り替え、シーンのバウンディングボックスに合わせてカメラをリセットします。
:param plotter: `pv.Plotter` PyVistaのプロッターインスタンス。
:param focal: `Tuple[float, float, float]` カメラの焦点座標 (x, y, z)。
:param maxdim: `float` シーンの最大寸法。
:param direction: `Tuple[float, float, float]` カメラから焦点への方向ベクトル。
:param viewup: `Tuple[float, float, float]` カメラのビューアップベクトル。
:param camera_scale: `float` カメラの距離を調整するためのスケール因子。
:param bounds: `Tuple[float, float, float, float, float, float]` シーンのバウンディングボックス。
:param parallel: `int` 並行投影を使用するかどうか (0: 無効, 1: 有効)。
:param margin: `float` カメラフィット時の追加マージン因子。
:returns: 戻り値はありません。
"""
d = np.asarray(direction, dtype=float)
n = np.linalg.norm(d)
if n <= 0:
d = np.array([1.0, -1.0, 0.8], dtype=float)
n = np.linalg.norm(d)
d /= n
dist = camera_scale * margin * maxdim
pos = tuple(np.asarray(focal, dtype=float) + d * dist)
plotter.camera_position = [pos, focal, viewup]
if parallel:
plotter.enable_parallel_projection()
else:
plotter.disable_parallel_projection()
try:
plotter.reset_camera(bounds=bounds)
except Exception:
try:
plotter.reset_camera()
except Exception:
pass
# Slight additional zoom-out to ensure the device stays inside the window.
try:
plotter.camera.Zoom(0.95)
except Exception:
pass
def _camera_pan(plotter: pv.Plotter, dx: float, dy: float, dz: float) -> None:
"""
カメラと焦点位置を相対的に移動(パン)させます。
現在のカメラ位置と焦点ポイントを取得し、指定されたdx, dy, dzだけ移動させます。
移動後、シーンを再レンダリングします。
:param plotter: `pv.Plotter` PyVistaのプロッターインスタンス。
:param dx: `float` X方向の移動量。
:param dy: `float` Y方向の移動量。
:param dz: `float` Z方向の移動量。
:returns: 戻り値はありません。
"""
try:
cam = plotter.camera
pos = np.array(cam.GetPosition(), dtype=float)
focal = np.array(cam.GetFocalPoint(), dtype=float)
shift = np.array([dx, dy, dz], dtype=float)
cam.SetPosition(*(pos + shift))
cam.SetFocalPoint(*(focal + shift))
plotter.render()
except Exception:
pass
[ドキュメント]
def install_camera_keybindings(plotter: pv.Plotter, camera_info: dict) -> None:
"""
PyVistaプロッターにカメラ制御用のキーバインドをインストールします。
回転 (j/l/i/k)、ズーム (+/-)、パン (a/d/w/s/u/o)、ビューのリセット (r)、
プリセットビュー (x/y/z)、並行投影の切り替え (p) など、
インタラクティブなカメラ操作のためのキーイベントハンドラを設定します。
:param plotter: `pv.Plotter` PyVistaのプロッターインスタンス。
:param camera_info: `dict` `configure_camera` 関数から返されたカメラ設定情報を含む辞書。
:returns: 戻り値はありません。
"""
bounds = camera_info["bounds"]
maxdim = camera_info["maxdim"]
focal = camera_info["focal"]
camera_scale = camera_info["camera_scale"]
parallel = camera_info["parallel"]
margin = camera_info["margin"]
rot_deg = 6.0
pan_step = 0.04 * maxdim
def do_render():
try:
plotter.render()
except Exception:
pass
def cam_azim(angle: float):
try:
plotter.camera.Azimuth(angle)
do_render()
except Exception:
pass
def cam_elev(angle: float):
try:
plotter.camera.Elevation(angle)
do_render()
except Exception:
pass
def cam_zoom(factor: float):
try:
plotter.camera.Zoom(factor)
do_render()
except Exception:
pass
def reset_view():
set_camera_direction(
plotter, focal, maxdim, camera_info["default_direction"], camera_info["default_viewup"],
camera_scale, bounds, parallel, margin
)
do_render()
def view_x():
set_camera_direction(plotter, focal, maxdim, (1.0, 0.0, 0.0), (0.0, 0.0, 1.0), camera_scale, bounds, parallel, margin)
do_render()
def view_y():
set_camera_direction(plotter, focal, maxdim, (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), camera_scale, bounds, parallel, margin)
do_render()
def view_z():
set_camera_direction(plotter, focal, maxdim, (0.0, 0.0, 1.0), (0.0, 1.0, 0.0), camera_scale, bounds, parallel, margin)
do_render()
def toggle_parallel():
try:
if plotter.camera.GetParallelProjection():
plotter.disable_parallel_projection()
else:
plotter.enable_parallel_projection()
do_render()
except Exception:
pass
# Rotation
plotter.add_key_event("j", lambda: cam_azim(+rot_deg))
plotter.add_key_event("l", lambda: cam_azim(-rot_deg))
plotter.add_key_event("i", lambda: cam_elev(+rot_deg))
plotter.add_key_event("k", lambda: cam_elev(-rot_deg))
# Zoom
plotter.add_key_event("plus", lambda: cam_zoom(1.12))
plotter.add_key_event("equal", lambda: cam_zoom(1.12))
plotter.add_key_event("minus", lambda: cam_zoom(0.90))
# Pan
plotter.add_key_event("a", lambda: _camera_pan(plotter, -pan_step, 0.0, 0.0))
plotter.add_key_event("d", lambda: _camera_pan(plotter, +pan_step, 0.0, 0.0))
plotter.add_key_event("w", lambda: _camera_pan(plotter, 0.0, +pan_step, 0.0))
plotter.add_key_event("s", lambda: _camera_pan(plotter, 0.0, -pan_step, 0.0))
plotter.add_key_event("u", lambda: _camera_pan(plotter, 0.0, 0.0, +0.5 * pan_step))
plotter.add_key_event("o", lambda: _camera_pan(plotter, 0.0, 0.0, -0.5 * pan_step))
# Presets and toggle
plotter.add_key_event("r", reset_view)
plotter.add_key_event("x", view_x)
plotter.add_key_event("y", view_y)
plotter.add_key_event("z", view_z)
plotter.add_key_event("p", toggle_parallel)
help_text = (
"Mouse: rotate / pan / zoom | "
"Keys: j/l azim, i/k elev, +/- zoom, a/d/w/s/u/o pan, r reset, x/y/z preset, p parallel"
)
try:
plotter.add_text(help_text, position="lower_left", font_size=10, color="black")
except Exception:
pass
print("\nCamera controls:")
print(" Mouse : rotate / pan / zoom")
print(" j / l : azimuth rotate")
print(" i / k : elevation rotate")
print(" + / - : zoom in / out")
print(" a / d : pan x- / x+")
print(" w / s : pan y+ / y-")
print(" u / o : pan z+ / z-")
print(" r : reset camera")
print(" x/y/z : preset views")
print(" p : toggle parallel projection")
[ドキュメント]
def add_layer_labels(plotter: pv.Plotter, layers: List[Layer]) -> None:
"""
TFT構造の各レイヤーにテキストラベルを追加します。
各 `Layer` オブジェクトの中心座標または特定のオフセット位置に、
レイヤー名を示すラベルを配置します。ソース/ドレインレイヤーは、表示のために少し上にオフセットされます。
:param plotter: `pv.Plotter` PyVistaのプロッターインスタンス。
:param layers: `List[Layer]` TFT構造の `Layer` オブジェクトのリスト。
:returns: 戻り値はありません。
"""
points = []
labels = []
for layer in layers:
if layer.name in ("source", "drain"):
# Put the S/D labels slightly above the top face.
x, y, z = layer.center
z = layer.bounds[5] + 0.03 * max(layer.bounds[5] - layer.bounds[4], 1.0)
points.append((x, y, z))
labels.append(layer.name)
else:
points.append(layer.center)
labels.append(layer.name)
plotter.add_point_labels(
points,
labels,
font_size=12,
shape=None,
fill_shape=False,
margin=0,
text_color="black",
always_visible=True,
)
[ドキュメント]
def add_legend(plotter: pv.Plotter) -> None:
"""
PyVistaプロッターに色の凡例を追加します。
TFT構造の主要な材料(基板、ゲート/ソース/ドレイン、絶縁体、半導体)と
その対応する色を示す凡例ボックスをプロッターに表示します。
:param plotter: `pv.Plotter` PyVistaのプロッターインスタンス。
:returns: 戻り値はありません。
"""
legend_entries = [
["substrate", "cyan"],
["gate / source / drain", "silver"],
["insulator", "lightblue"],
["semiconductor", "orange"],
]
plotter.add_legend(legend_entries, bcolor="white", border=True, face="rectangle")
[ドキュメント]
def render_tft(args: argparse.Namespace):
"""
コマンドライン引数に基づいてTFTの3Dモデルをレンダリングします。
`build_layers` 関数でTFTレイヤーを構築し、PyVistaプロッターを初期化します。
各レイヤーを3Dボックスとしてプロッターに追加し、必要に応じてラベル、凡例、軸を表示します。
カメラを設定し、インタラクティブモードの場合はキーバインドをインストールします。
最終的に、表示またはファイルへのスクリーンショット保存を行います。
:param args: `argparse.Namespace` コマンドライン引数を格納するオブジェクト。
:returns: `pv.Plotter` レンダリングに使用されたPyVistaプロッターインスタンス。
"""
layers, dims = build_layers(args)
print("\n# TFT 3D structure (PyVista)")
print(f"W = {dims['W_nm'] / 1e3:10.4g} um")
print(f"L = {dims['L_nm'] / 1e3:10.4g} um")
print(f"dg = {dims['d_ins_nm']:10.4g} nm")
print(f"x_total = {dims['x_tot'] / 1e3:10.4g} um")
print(f"y_total = {dims['y_tot'] / 1e3:10.4g} um")
print(f"z_total = {dims['z_tot_nm']:10.4g} nm")
print(f"zscale = {args.zscale:10.4g} (visual only)")
win_w = max(320, int(args.figsize[0] * args.dpi))
win_h = max(240, int(args.figsize[1] * args.dpi))
off_screen = (args.show == 0)
plotter = pv.Plotter(off_screen=off_screen, window_size=(win_w, win_h), title=args.title)
plotter.set_background(args.background)
try:
plotter.enable_trackball_style()
except Exception:
pass
for layer in layers:
mesh = pv.Box(bounds=layer.bounds)
plotter.add_mesh(
mesh,
color=layer.color,
opacity=layer.opacity,
show_edges=bool(args.edges),
edge_color="black",
line_width=1.0,
smooth_shading=False,
lighting=True,
name=layer.name,
)
if args.labels:
add_layer_labels(plotter, layers)
if args.legend:
add_legend(plotter)
if args.axes:
plotter.show_axes()
else:
try:
plotter.hide_axes()
except Exception:
pass
# Improve depth perception slightly.
try:
plotter.enable_anti_aliasing()
except Exception:
pass
try:
plotter.add_title(args.title, font_size=14)
except Exception:
pass
camera_info = configure_camera(plotter, layers, dims, args.camera_scale, args.parallel, args.camera_margin)
if args.show:
install_camera_keybindings(plotter, camera_info)
if args.show:
if args.save:
plotter.show(screenshot=args.outfile, auto_close=True)
print(f"\nSaved figure to: {args.outfile}")
else:
plotter.show(auto_close=True)
else:
if args.save:
plotter.show(screenshot=args.outfile, auto_close=True)
print(f"\nSaved figure to: {args.outfile}")
else:
# Render once off-screen so camera etc. are evaluated.
plotter.show(auto_close=True)
return plotter
[ドキュメント]
def main() -> None:
"""
スクリプトのメインエントリポイント。
Argparseパーサーを構築し、コマンドライン引数を解析し、
`render_tft` 関数を呼び出してTFT構造をレンダリングします。
`--show=0` で `args.pause` が設定されている場合は、終了前にユーザー入力を待ちます。
:returns: 戻り値はありません。
"""
parser = build_parser()
args = parser.parse_args()
render_tft(args)
if args.pause and args.show == 0:
input("\nPress ENTER to terminate>>\n")
if __name__ == "__main__":
main()