光線行列によるJuliaとPythonを用いた光線追跡の計算

Julia

はじめに

光学の仕事に携わっていると光学計算ソフトウェアである、ZemaxやcodeVを使った計算を行うことが多い。しかし、これらのソフトウェアは有償でありながらレーザー光線の追跡に対してはオーバースペックとなることが多い。また、計算にも時間がかかることがある。そこで今回は手元のPythonやJuliaで簡単に光線行列の計算を行うことを目的として記事を書く。

ABCD行列とは

光線行列(ray matrix)[Yariv2010]、光線転送行列[富田1997]あるいはABCD行列とは光学系を2×2の行列で表し、光線を表すベクトルに対してこの行列を作用させることで光線の追跡を可能とする。基本的に幾何光学計算または通常ガウシアンビームの計算において利用されることが多い。光線行列計算には入力の光線に対して、出力の光線がどのようになるかを計算し、具体的には次のように計算する。光線が光軸からあまり離れていない条件下では近軸条件が成り立ち[伊藤1997]、入射光線の半径\(r_i\)と入射光線の広がり角\(\theta_i\)を用いて、光線の伝搬後の光線の半径\(r_o\)と広がり角\(\theta_o\)は次のように表される。

\begin{equation} \left[ \begin{array}{c} r_o\\ \theta_i \end{array} \right] = \left[ \begin{array}{c c} A & B\\ C & D \end{array} \right] \left[ \begin{array}{c} r_i\\ \theta_i \end{array} \right] \end{equation}

ここで\(A,B,C,D\)はそれぞれ光線行列の要素で各光学素子に対応する光線行列を次に示す。

光学素子 光線行列
平行平板:厚さ \( d\) \begin{bmatrix} 1 & d \\ 0 & 1 \end{bmatrix}
薄肉レンズ:焦点距離 \( f\) \begin{bmatrix} 1 & 0 \\ \frac{-1}{f} & 1 \end{bmatrix}
異なる屈折率を持つ媒質界面:屈折率\( n_1, n_2\) \begin{bmatrix} 1 & 0 \\ 0 & \frac{n_1}{n_2} \end{bmatrix}
球面ミラー:曲率半径 \( R\) \begin{bmatrix} 1 & 0 \\ \frac{-2}{R} & 1 \end{bmatrix}

また、複数の光学素子を作用させたいときは入力光線から光学素子に光に入っていく順に\(1,2,3…,n\)のとき、その光学素子の光線行列を\(M_1,M_2,M_3…M_n\)として、全体の光線行列\(M\)は次のように表せる[伊藤1997]。

\begin{equation}
M=M_1\cdot M_2 \cdot M_3 \dots M_n
\end{equation}

また、より厳密な計算において\( q\)パメータと呼ばれるものが用いられる。この計算の前提にはこれから計算するビームが基本ガウシアンビームであることの気を付けてほしい[Yariv2010][過去記事]。\(a\)パラメータはビームの波面の様子も調べることができ、次のように定義される。

\begin{equation} \frac{1}{q} = \frac{1}{R} -i \frac{\lambda}{\pi \omega n} \end{equation}

ここで\(R, \lambda, \omega, n\)はビーム同位相面の曲率半径、レーザーの波長、ビーム半径、媒質の屈折率を表す。また、\(q_{in}\)の光線が伝搬したときの\(q_{out}\)は

\begin{equation} \begin{bmatrix} q_{out}\\ 1 \end{bmatrix} = \begin{bmatrix} A&B\\ C&D \end{bmatrix} \begin{bmatrix} q_{in}\\ 1 \end{bmatrix} \end{equation}

となり、これを実際に解いてみると

\begin{equation}\label{eq:q-para}
q_{out} = \frac{Aq_{in}+B}{Cq_{in}+D}
\end{equation}

となる。ここまでが下準備となる。

実際の計算例:コリメート光学系

ここで実際の例を用いて例示を行う。次のようなコリメート光学系を考える。

このときの光学系の光線行列は次のように表せる

\begin{equation} \left[ \begin{array}{c} r_o\\ \theta_i \end{array} \right] = \left[ \begin{array}{c c} 1 & z\\ 0 & 1 \end{array} \right] \left[ \begin{array}{c c} 1 & 0\\ -\frac{1}{f} & 1 \end{array} \right] \left[ \begin{array}{c c} 1 & f\\ 0 & 1 \end{array} \right] \left[ \begin{array}{c} r_i\\ \theta_i \end{array} \right] \end{equation}

これを計算すると

\begin{equation} \left[ \begin{array}{c} r_o\\ \theta_i \end{array} \right] =\left[ \begin{array}{c} r_i + f\theta_i +\frac{r_i}{f} z\\ -\frac{r_i}{f} \end{array} \right] \end{equation}

また、\(q\)パラメータで計算すると

\begin{equation} \left[ \begin{array}{c} q_o\\ 1 \end{array} \right] =\left[ \begin{array}{c} q_i + f +\frac{q_i}{f} z\\ -\frac{q_i}{f} \end{array} \right] \end{equation}

と表される。これが意味するところはコリメート光は入射レーザーの広がり角に依存しないということがわかる。ここから実際に光線の変化の様子をPythonとJuliaで計算した。ここで\(f = 0.9mm, r_i = 0mm, \theta_i = 20deg\)とした。
幾何光学計算の例を以下に示す。コリメート光の条件ではレンズ透過後にコリメートされていることがわかる。

using PyPlot
plt = PyPlot
using PyCall
PyCall.PyDict(matplotlib["rcParams"])["font.family"] = "Meiryo";

f = 0.9 # lens focal length in mm
rin = 0 # input ray radius in mm : 8um
theta = 20 # angle in degrees
theta = deg2rad(theta) # convert to radians
lam = 940e-6 # wavelength in mm

input_matrix = [rin ; theta] # input ray matrix
lens_matrix = [1 0; -1/f 1] # lens matrix

z_list = range(0.0,10.0,step = 0.01) # distance from input ray in mm
r_result = []
theta_result = []

for z in z_list
    if z <= 0.9  # before lens
        M = [1 z;0 1]

    else   # after lens
        before_lens = [1 0.9;0 1]
        z_matrix = [1 z-0.9; 0 1]
        M = z_matrix * lens_matrix * before_lens
    end
    output_matrix = M * input_matrix

    r_out = output_matrix[1][1]
    theta_out = output_matrix[2][1]

    push!(r_result, r_out)
    push!(theta_result, rad2deg(theta_out)) # 角度[deg]で保存
end

plt.figure(figsize=(16, 5))
plt.subplot(1, 2, 1)
plt.plot(z_list, r_result, label="Ray Radius [mm]")
plt.xlabel("z (mm)")
plt.ylabel("ビーム半径 (mm)")
plt.legend()
plt.grid()

plt.subplot(1, 2, 2)
plt.plot(z_list, theta_result, label="Ray Angle [deg]")
plt.xlabel("z (mm)")
plt.ylabel("広がり角 (deg)")
plt.legend()
plt.grid()

display(gcf())
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

f = 0.9 # lens focal length in mm
rin = 0 # input ray radius in mm : 8um
theta = 20 # angle in degrees
theta = np.deg2rad(theta) # convert to radians
lam = 940e-6 # wavelength in mm

input_matrix = np.array([[rin],[theta]]) # input ray matrix
lens_matrix = np.array([[1, 0], [-1/f, 1]]) # lens matrix

z_list = np.arange(0,10,0.01) # distance from input ray in mm
r_result = []
theta_result = []

for z in z_list:
    if z <= 0.9:  # before lens
        M = np.array([[1, z], [0, 1]])

    else:   # after lens
        before_lens = np.array([[1, 0.9], [0, 1]])
        z_matrix = np.array([[1, z-0.9], [0, 1]])
        M = np.linalg.multi_dot([z_matrix, lens_matrix, before_lens])

    output_matrix = np.dot(M, input_matrix)

    r_out= output_matrix[0][0]
    theta_out = output_matrix[1][0]

    r_result.append(r_out)
    theta_result.append(np.rad2deg(theta_out)) # 角度[deg]で保存

plt.figure(figsize=(16, 5))
plt.subplot(1, 2, 1)
plt.plot(z_list, r_result, label='Ray Radius [mm]')
plt.xlabel("z (mm)")
plt.ylabel("ビーム半径 (mm)")
plt.legend()
plt.grid()

plt.subplot(1, 2, 2)
plt.plot(z_list, theta_result, label='Ray Angle [deg]')
plt.xlabel("z (mm)")
plt.ylabel("広がり角 (deg)")
plt.legend()
plt.grid()
plt.show()

続いて\(q\)パラメータを用いた計算結果を示す。ここでは光源の出射面での電場位相の曲率半径\(R\)は無限(~平面波)として、ビーム径を8umとした。レンズの倍率の計算[参考]から広がり角は求まるのでこれを漸近線として計算が合っているかを確認した。

# パラメータ設定
f = 0.9  # レンズの焦点距離 (mm)
rin = 8e-3  # 初期ビームウエスト半径 (mm) : w0
lam = 940e-6  # 波長 (mm)

# 初期qパラメータの計算
q_in = 1 / (1im * lam / (π * rin^2)) 

# ABCD行列の定義
input_matrix = [q_in ; 1]  # input ray matrix (q, 1)
lens_matrix = [1 0; -1 / f 1]  # lens matrix

z_list = range(0.0,10.0,step = 0.01)
q_result_radius = [] 

# レンズの位置
z_lens = 0.9 # mm

for z_val in z_list
    if z_val <= z_lens  # レンズの手前
        # 自由空間伝播
        M_prop = [1 z_val; 0 1]
        M_total = M_prop
    else  # レンズの後
        # レンズまでの伝播
        M_before_lens = [1 z_lens; 0 1]
        # レンズ通過後の残りの距離
        z_after_lens = z_val - z_lens
        M_after_lens_prop = [1 z_after_lens; 0 1]
        
        # システム全体のABCD行列
        M_total = M_after_lens_prop * lens_matrix * M_before_lens
    end

    A, B, C, D = M_total[1,1], M_total[1,2], M_total[2,1], M_total[2,2]
    q_out = (A * q_in + B) / (C * q_in + D)
    q_out = 1 / q_out
    #ここまでq_outの計算終わり

    # qパラメータを使ったビーム半径の計算
    q_out_im = imag(q_out)
    beam_radius = (lam / (π * abs(q_out_im)))
    push!(q_result_radius,beam_radius)
end
# --- 漸近線の計算 ---

# 後ろ側焦点距離 -> 近似線は後ろ側焦点から広がっていく
after_focus_postion = z_lens + f
w0_after_focus = f * lam / (π * rin) 
theta_ff_after_focus = lam / (π * w0_after_focus) 
# 漸近線の計算
z_asymptote = range(after_focus_postion, z_list[end], length = 200)
asymptote_radius = asymptote_radius = theta_ff_after_focus .* abs.(z_asymptote .- after_focus_postion)


# プロット
plt.figure(figsize=(10, 6))
plt.plot(z_list, q_result_radius, label="計算されたビーム半径 (mm)")
plt.plot(z_asymptote, asymptote_radius, "--", label="遠方場での漸近線")
# 新しいビームウエストの位置とサイズをプロット上に示す
plt.plot(after_focus_postion, w0_after_focus,"ro",  label="後ろ側焦点距離")


plt.xlabel("z (mm)")
plt.ylabel("ビーム半径 (mm)") # Y軸ラベルを修正
plt.legend()
plt.grid(true)
plt.ylim(0, maximum(q_result_radius)*1.1) # y軸の範囲を調整

plt.axvline(z_lens, color="gray", linestyle=":", label="レンズ位置 ")
plt.legend() # axvline のラベルを表示するために再度呼び出す
display(gcf())
# パラメータ設定
f = 0.9  # レンズの焦点距離 (mm)
rin = 8e-3  # 初期ビームウエスト半径 (mm) : w0
lam = 940e-6  # 波長 (mm)

# 初期qパラメータの計算
q_in = 1 / (1j * lam / (np.pi * rin**2)) 

# ABCD行列の定義
input_matrix = np.array([[q_in], [1]], dtype=complex)  # input ray matrix (q, 1)
lens_matrix = np.array([[1, 0], [-1 / f, 1]], dtype=complex)  # lens matrix

z_list = np.arange(0, 10, 0.01)  
q_result_radius = [] 

# レンズの位置
z_lens = 0.9 # mm

for z_val in z_list:
    if z_val <= z_lens:  # レンズの手前
        # 自由空間伝播
        M_prop = np.array([[1, z_val], [0, 1]], dtype=complex)
        M_total = M_prop
    else:  # レンズの後
        # レンズまでの伝播
        M_before_lens = np.array([[1, z_lens], [0, 1]], dtype=complex)
        # レンズ通過後の残りの距離
        z_after_lens = z_val - z_lens
        M_after_lens_prop = np.array([[1, z_after_lens], [0, 1]], dtype=complex)
        
        # システム全体のABCD行列
        M_total = np.linalg.multi_dot([M_after_lens_prop, lens_matrix, M_before_lens])


    A, B, C, D = M_total[0,0], M_total[0,1], M_total[1,0], M_total[1,1]
    q_out = (A * q_in + B) / (C * q_in + D)
    q_out = 1 / q_out
    #ここまでq_outの計算終わり

    # qパラメータを使ったビーム半径の計算
    q_out_im = np.imag(q_out)
    beam_radius = np.sqrt(lam / (np.pi * np.abs(q_out_im)))
    q_result_radius.append(beam_radius)

# --- 漸近線の計算 ---

# 後ろ側焦点距離 -> 近似線は後ろ側焦点から広がっていく
after_focus_postion = z_lens + f
w0_after_focus = f * lam / (np.pi * rin) 
theta_ff_after_focus = lam / (np.pi * w0_after_focus) 
# 漸近線の計算
z_asymptote = np.linspace(after_focus_postion, z_list[-1], 200)
asymptote_radius = theta_ff_after_focus * np.abs(z_asymptote - after_focus_postion)

# プロット
plt.figure(figsize=(10, 6))
plt.plot(z_list, q_result_radius, label="計算されたビーム半径 (mm)")
plt.plot(z_asymptote, asymptote_radius, '--', label=f"遠方場での漸近線 (θ ≈ {theta_ff_after_focus:.4f} rad)")
# 新しいビームウエストの位置とサイズをプロット上に示す
plt.plot(after_focus_postion, w0_after_focus, 'ro', label=f"後ろ側焦点距離 (z={after_focus_postion:.2f}mm, w0={w0_after_focus*1000:.1f}um)")


plt.xlabel("z (mm)")
plt.ylabel("ビーム半径 (mm)") # Y軸ラベルを修正
plt.legend()
plt.grid(True)
plt.ylim(0, max(q_result_radius)*1.1) # y軸の範囲を調整

plt.axvline(z_lens, color='gray', linestyle=':', label=f"レンズ位置 (z={z_lens}mm)")
plt.legend() # axvline のラベルを表示するために再度呼び出す
plt.show()

コメント

タイトルとURLをコピーしました