はじめに
今回はZemaxとLumericalを連携した面型受光器の光電流のシミュレーションについて、デモ的な解析を試してみたので、ご紹介します。
今回の計算対象は以下の図になります。
図のようにレンズを通した幾何光学計算はZemaxで行い、集光した波面を取り出して、それをLumericalにインポートして光電流のシミュレーションを行います。
面型受光器の光電流シミュレーションのワークフロー
今回は以下のように計算してみました。
①Zemaxでレンズなどを設定後、imageにおける波面データをエクスポート(保存)
②波面データをLumericalに読み込めるように、データをpythonで加工
③加工したデータをLumerical FDTDにインポートしFDTDシミュレーションにより、受光器の光吸収層(今回はGe)での吸光度を計算
④計算した吸光度をもとにキャリア生成率をLumerical Chargeに設定してシミュレーションを実施し、光電流のバイアス電圧依存性を計算
Zemaxの計算配置
こちらがZemaxの計算配置になります。今回はデモ的に両側が曲面のレンズを設定してみました。

右側に示してあるのがimage面におけるhuygens PSFになります。このhuygens PSFを今回はLumericalに取り込んでみます。
huygens PSFのデータは、以下のように表示をtextにして、保存すればtxt形式で保存できます。

今回はintensityデータをlumericalに取り込めるようにしてみます。
データ加工用のpythonスクリプト
こちらがZemaxのhuygens PSFデータをlumericalに取り込めるように加工するためのpythonスクリプトです。
このスクリプトは、Zemaxで計算した点像分布を読み取り、それをもとにLumerical FDTDで使える光源として再構成するためのものです。Zemaxの出力ファイルから強度分布を取得し、振幅分布に変換したうえで、所定の空間座標と波長情報を持つ電場データを作成しています。その電場分布を、+x方向に進む平面状の入射光としてLumerical側に渡し、インポートソースとして設定します。結果として、Zemaxで得られた像面や瞳面の情報を、そのままFDTDシミュレーションの入射条件として使えるようにしています。光学設計と電磁場解析を連携させるための橋渡しをするスクリプトです。
import sys
sys.path.append(r"C:\Program Files\Lumerical\v261\api\python")
import numpy as np
import lumapi
# ==================================================
# User settings
# ==================================================
txt_file = r"D:\Lateral_PD_w_Lumerical_Zemax\test_lens1.txt"
fsp_path = r"D:\Lateral_PD_w_Lumerical_Zemax\vpd_FDTD.fsp"
grid_size = 64
dx_um = 0.864
wavelength_um = 1.55
transpose_xy = False
use_component = "Ex" # "Ex" or "Ey"
source_x_um = 1.0 # 光源位置 [µm]
source_name = "zemax_src"
# ==================================================
# Read Zemax listing (UTF-16)
# ==================================================
data = []
read_data = False
with open(txt_file, "r", encoding="utf-16") as f:
for line in f:
line = line.strip()
if "Values are relative intensity" in line:
read_data = True
continue
if not read_data or line == "":
continue
for token in line.replace("\t", " ").split():
try:
data.append(float(token))
except ValueError:
pass
data = np.array(data)
print("Parsed data size:", data.size)
if data.size != grid_size * grid_size:
raise ValueError(f"Data size mismatch: {data.size}")
# ==================================================
# Amplitude (intensity -> amplitude)
# ==================================================
I = data.reshape((grid_size, grid_size))
A = np.sqrt(I)
A /= A.max()
if transpose_xy:
A = A.T
# ==================================================
# Coordinates
# +x propagation → y–z plane
# ==================================================
dx = dx_um * 1e-6
coord = (np.arange(grid_size) - (grid_size - 1) / 2) * dx
x = np.array([0.0]) # Nx = 1(面)
y = coord.copy()
z = coord.copy()
# ==================================================
# Frequency
# ==================================================
c0 = 299792458.0
lam = wavelength_um * 1e-6
f = np.array([c0 / lam])
# ==================================================
# Field array (Nx=1, Ny, Nz, 3)
# ==================================================
E = np.zeros((1, grid_size, grid_size, 3), dtype=np.complex128)
if use_component == "Ex":
E[0, :, :, 0] = A
elif use_component == "Ey":
E[0, :, :, 1] = A
else:
raise ValueError("use_component must be 'Ex' or 'Ey'")
# ==================================================
# Send to Lumerical
# ==================================================
with lumapi.FDTD(fsp_path) as fdtd:
# --- send arrays ---
fdtd.putv("x", x)
fdtd.putv("y", y)
fdtd.putv("z", z)
fdtd.putv("f", f)
fdtd.putv("E", E)
fdtd.eval('?"Variables transferred";')
# --- create imported source ---
fdtd.eval(f'''
addimportedsource;
set("name","{source_name}");
# +x propagation
set("injection axis","x");
set("direction","Forward");
# source position
set("x",{source_x_um * 1e-6});
set("y",0);
set("z",0);
# dataset
ds = rectilineardataset("Zemax_source", x, y, z);
ds.addparameter("f", f);
ds.addattribute("E", E);
select("{source_name}");
importdataset(ds);
''')
fdtd.save()
print("Done:")
print(" +x incidence")
print(f" source x = {source_x_um} um")
print(" y–z plane amplitude distribution")
print(" half-pixel shift corrected")
Lumerical FDTDシミュレーション
上のpythonスクリプトを実行すると用意したプロジェクトファイルにZemaxの計算結果が光源としてインポートされます。今回はSiGe受光器を設定して、光吸収層であるGeにLumericalのobject libraryであるpower absorbedを設定し吸光度を計算することにします。
FDTDシミュレーションが終了すると、光吸収によって生成されたキャリレートが.mat形式で出力されるのでその結果をCHARGEで取り込んで光電流のシミュレーションを行います。
下の図の灰色の線の箇所にimport 光源を設定しZemaxで計算~Pythonで加工した結果を取り込んでいます。

Lumerical CHARGEによる光電流シミュレーション
こちらがCHARGEの設定画面です。
今回は構造の対称性から、全体を半分に制限してシミュレーションすることにしました。
図のGe領域(光吸収層)には、FDTDシミュレーションで求めた光電流の生成率をインポートします。
設定はLumerical object libraryにあるoptical generation rateを設定して(光吸収層全体に広げる)、FDTDシミュレーションの出力ファイルを設定します。
あとは、電圧スイープを設定し通常のCHARGEシミュレーションを実行すれば、光電流のバイアス依存性のシミュレーションが行えます。

光電流のバイアス依存性のシミュレーション結果
以下が光電流のバイアス依存性のシミュレーション結果になります。電流値の単位はアンペア(A)です。
バイアス電圧は逆バイアスで設定してます。

まとめ
今回はデモ的に以上のワークフローでZemax-Lumerical-Python連携の面型受光器のシミュレーションを試してみました。レンズ形状や、受光器の構成は仮の設定で行いました。この一連のフローでレンズ~受光器の応答まで計算できるようになります。
このフローを利用して、受光器の小信号応答などを求めてみるのも良いでしょう。
※本記事は筆者個人の見解であり、所属組織の公式見解を示すものではありません。
問い合わせフォームのご連絡
問い合わせ
光学シミュレーションソフトの導入や技術相談、
設計解析委託をお考えの方はサイバネットシステムにお問合せください。
光学ソリューションサイトについては以下の公式サイトを参照:
👉 [光学ソリューションサイト(サイバネット)]
光学分野のエンジニアリングサービスについては以下の公式サイトを参照:
👉 [光学エンジニアリングサービス(サイバネット)]
