2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Optiland]フリーの光学設計ツール:Optilandを触ってみる:Pythonでレンズ設計・PSF・MTFを可視化

2
Last updated at Posted at 2026-05-26

はじめに

本記事では、Optilandを使って、

  • 既存レンズ(Cooke Triplet)の解析
  • 自分でレンズを構築(単レンズ)
  • Spot Diagram / PSF / MTF の可視化

まで一通り体験してみたので、ご紹介します。

1.この記事でやること

今回は下記のように2パターンのレンズを用意して計算してみました。
どちらもspyder環境で実行してます。

  • レンズを2パターンで用意
    • ① CookeTriplet(既存モデル)
    • ② 自作シングレットレンズ
  • 評価指標
    • Spot Diagram(収差確認)
    • PSF(点像分布)
    • MTF(空間周波数特性)
      をそれぞれのケースについて解析を実行

2. Cooke Triplet を使った解析について(スクリプトの説明)

Cooke Tripletは典型的な3枚構成のレンズで、Optilandのsampleスクリプト内にも納められています。
2.1 レンズ生成のコマンド

from optiland.samples.objectives import CookeTriplet

lens = CookeTriplet()

2.2 Fieldとは?
今回重要なのが Field(視野) の定義です。

fields = [
    (0, 0.0),  # 中心
    (0, 0.7),  # 中間視野
    (0, 1.0),  # 最大視野(外周)
]

✅ ポイントは
・(Hx, Hy) の形式
・正規化視野(Normalized Field)
であることです。
今回はx方向を0で固定して、y方向に視野を変えた場合の挙動を解析してみます。

2.3 レンズレイアウト

lens.draw(num_rays=5, distribution="line_y")

今回はy方向に光線を並べた解析としています。

2.4 Spot Diagram

from optiland.analysis import SpotDiagram

spot = SpotDiagram(
    lens,
    fields=fields,
    wavelengths="primary",
    num_rings=6,
    distribution="hexapolar",
)
spot.view()

このコードでは、まず SpotDiagram クラスを呼び出して、レンズの性能評価の準備をしています。
ここで指定しているのは、「どの条件でレイ(光線)を飛ばして評価するか」です。
設定したレンズに対して、「ちゃんと像を作れているか」をチェックします。
次に fields は視野位置を表しています。これは空間中の異なる位置にある点光源を意味しており、中心、少し外れた位置、画面の端といったように、複数の場所から光を入れ中央部と端部の差分をチェックします。
wavelengths="primary" は、どの波長の光で評価するかを指定しています。ここでは代表波長のみで評価していますが、複数波長を使えば色収差(色ごとのズレ)も見えるようになります。
次に重要なのが distribution="hexapolar" と num_rings=6 です。
これは「どのようにレイを打つか」を指定しています。
光は1本ではなく、「束」として扱います。つまり、同じ点から来た光でも、レンズのいろいろな場所を通るように複数のレイを飛ばします。そのとき、瞳(アパーチャ)上にレイをどう配置するかがこの部分で決まります。
hexapolar というのは、瞳の中でレイを円対称にリング状に配置する方法で、光学系が回転対称である場合に非常に相性が良い配置です。さらに num_rings=6 によって、そのリングの数を指定しており、これが増えるほどレイのサンプリングが細かくなり、結果がより正確になります。 次に、各視野位置に対して点光源を置きます。
次に、それぞれの点光源から多数のレイを発生させ、レンズの中を通して像面まで追跡します。
そして、それぞれのレイが最終的に像面のどこに到達したかを記録します。実際のレンズでは収差があるため、レイごとに微妙に違う位置に到達します。
その結果、像面上には
「たくさんの点の集まり」
ができます。この点の集合がそのまま Spot Diagram になります。

2.5 PSF(Point Spread Function)
次に点光源が像面でどう広がるかをPSFによって評価します。

from optiland.psf import FFTPSF

psf = FFTPSF(
    lens,
    field=(0, 0),
    wavelength="primary",
    num_rays=128,
)
psf.view()

2.6 MTF(Modulation Transfer Function)
MTFではコントラスト伝達特性を評価します。

from optiland.mtf import FFTMTF

mtf = FFTMTF(
    lens,
    fields=fields,
    wavelength="primary",
    num_rays=128,
)
mtf.view()

3.スクリプト全体

こちらがスクリプト全体になります。

import numpy as np
import matplotlib.pyplot as plt

from optiland.samples.objectives import CookeTriplet
from optiland.analysis import SpotDiagram

# PSF / MTF は analysis ではなく専用モジュールから読む
from optiland.psf import FFTPSF
from optiland.mtf import FFTMTF


# =========================
# 0. バージョン・API確認
# =========================
import optiland
import inspect

print("Optiland version:", getattr(optiland, "__version__", "unknown"))


# =========================
# 1. 初期レンズ
# =========================
lens = CookeTriplet()

print("trace signature:")
print(inspect.signature(lens.trace))


# =========================
# 2. Field設定
# =========================
# NG:
# fields = (0, 0.7, 1.0)
#
# OK:
# 各fieldは (Hx, Hy) の2要素タプル
fields = [
    (0, 0.0),
    (0, 0.7),
    (0, 1.0),
]

wavelength = "primary"


# =========================
# 3. レンズ描画
# =========================
try:
    lens.draw(num_rays=5, distribution="line_y")
    plt.show()
except Exception as e:
    print("[WARN] lens.draw failed:")
    print(e)


# =========================
# 4. Spot Diagram
# =========================
try:
    spot = SpotDiagram(
        lens,
        fields=fields,
        wavelengths=wavelength,
        num_rings=6,
        distribution="hexapolar",
    )
    spot.view()
    plt.show()

except Exception as e:
    print("[ERROR] SpotDiagram failed:")
    print(e)


# =========================
# 5. PSF
# =========================
try:
    # まず中央視野だけ
    psf = FFTPSF(
        lens,
        field=(0, 0),
        wavelength=wavelength,
        num_rays=128,
    )
    psf.view()
    plt.show()

except Exception as e:
    print("[ERROR] FFTPSF failed:")
    print(e)


# =========================
# 6. MTF
# =========================
try:
    mtf = FFTMTF(
        lens,
        fields=fields,
        wavelength=wavelength,
        num_rays=128,
    )
    mtf.view()
    plt.show()

except Exception as e:
    print("[ERROR] FFTMTF failed:")
    print(e)

4.実行結果

こちらが実行結果になります。
光線追跡のシミュレーション結果
スクリーンショット 2026-05-25 190722.png
視野像のシミュレーション結果
スクリーンショット 2026-05-25 190741.png
PSFの計算結果
スクリーンショット 2026-05-25 190751.png
MTFの解析結果
スクリーンショット 2026-05-25 190801.png

5.カスタムでレンズを実装する場合

上記の事例ではOptilandに実装すみのsampleをもとにして解析を行いましたが、カスタム設計のレンズを実装して解析することもできます。
今回はシングルレンズをカスタムで実装して、解析を試してみます。
** 5.1 実行スクリプト**
こちらが今回作成したスクリプトです。
シングルレンズに対して、波長と視野(角度)を変えて入射してみます。

import numpy as np
import matplotlib.pyplot as plt
import inspect

import optiland
from optiland import optic

from optiland.analysis import SpotDiagram
from optiland.psf import FFTPSF
from optiland.mtf import FFTMTF


# ============================================================
# 0. Environment check
# ============================================================
print("Optiland version:", getattr(optiland, "__version__", "unknown"))


# ============================================================
# 1. Build a simple singlet lens from scratch
# ============================================================
lens = optic.Optic()

# ------------------------------------------------------------
# Surface definition
# ------------------------------------------------------------
# Surface 0: object at infinity
lens.surfaces.add(
    index=0,
    radius=float("inf"),
    thickness=float("inf"),
)

# Surface 1: first lens surface
# radius: positive convex surface
# thickness: lens center thickness
# material: N-BK7
# is_stop=True means aperture stop is located here
lens.surfaces.add(
    index=1,
    radius=50.0,
    thickness=5.0,
    material="N-BK7",
    is_stop=True,
)

# Surface 2: second lens surface
# radius: negative convex surface from image side
# thickness: initial distance to image plane
lens.surfaces.add(
    index=2,
    radius=-50.0,
    thickness=45.0,
)

# Surface 3: image plane
lens.surfaces.add(
    index=3,
)


# ============================================================
# 2. Aperture, fields, wavelengths
# ============================================================

# Entrance pupil diameter
lens.set_aperture(
    aperture_type="EPD",
    value=10.0,
)

# Field type: angle
lens.fields.set_type("angle")

# Field points
# y=0: on-axis
# y=5: intermediate field
# y=10: off-axis field
lens.fields.add(y=0.0)
lens.fields.add(y=5.0)
lens.fields.add(y=10.0)

# Wavelengths [um]
# F, d, C lines
lens.wavelengths.add(value=0.4861)
lens.wavelengths.add(value=0.5876, is_primary=True)
lens.wavelengths.add(value=0.6563)


# ============================================================
# 3. Solve image plane position
# ============================================================

# Let Optiland solve the image distance paraxially
lens.updater.image_solve()


# ============================================================
# 4. Print trace API
# ============================================================
print("trace signature:")
print(inspect.signature(lens.trace))


# ============================================================
# 5. Draw lens layout
# ============================================================
try:
    lens.draw(num_rays=5, distribution="line_y")
    plt.title("Simple Singlet Lens Layout")
    plt.savefig("01_simple_singlet_layout.png", dpi=300, bbox_inches="tight")
    plt.show()

except Exception as e:
    print("[WARN] lens.draw failed:")
    print(e)


# ============================================================
# 6. Analysis fields
# ============================================================
# Important:
# Each field must be a tuple of (Hx, Hy)
fields = [
    (0, 0.0),
    (0, 0.5),
    (0, 1.0),
]

wavelength = "primary"


# ============================================================
# 7. Spot Diagram
# ============================================================
try:
    spot = SpotDiagram(
        lens,
        fields=fields,
        wavelengths=wavelength,
        num_rings=6,
        distribution="hexapolar",
    )
    spot.view()
    plt.savefig("02_simple_singlet_spot.png", dpi=300, bbox_inches="tight")
    plt.show()

except Exception as e:
    print("[ERROR] SpotDiagram failed:")
    print(e)


# ============================================================
# 8. PSF
# ============================================================
try:
    psf = FFTPSF(
        lens,
        field=(0, 0),
        wavelength=wavelength,
        num_rays=128,
    )
    psf.view()
    plt.savefig("03_simple_singlet_psf.png", dpi=300, bbox_inches="tight")
    plt.show()

except Exception as e:
    print("[ERROR] FFTPSF failed:")
    print(e)


# ============================================================
# 9. MTF
# ============================================================
try:
    mtf = FFTMTF(
        lens,
        fields=fields,
        wavelength=wavelength,
        num_rays=128,
    )
    mtf.view()
    plt.savefig("04_simple_singlet_mtf.png", dpi=300, bbox_inches="tight")
    plt.show()

except Exception as e:
    print("[ERROR] FFTMTF failed:")
    print(e)


print("Finished.")

6.実行結果

こちらが実行結果になります。
スクリーンショット 2026-05-25 191231.png
スクリーンショット 2026-05-25 191241.png
スクリーンショット 2026-05-25 191249.png
スクリーンショット 2026-05-25 191257.png

感想

今回試しにOptilandを触ってみましたが、生成AIにコードを書かせて、それを貼り付け実行だけで大部分ができてしまうので、非常に有用なツールであると印象を持ちました。

次回は、最適化機能を試してみたいと思います。

※本記事は筆者個人の見解であり、所属組織の公式見解を示すものではありません。

問い合わせフォームのご連絡

問い合わせ

光学シミュレーションソフトの導入や技術相談、
設計解析委託をお考えの方はサイバネットシステムにお問合せください。

光学ソリューションサイトについては以下の公式サイトを参照:
👉 [光学ソリューションサイト(サイバネット)]

光学分野のエンジニアリングサービスについては以下の公式サイトを参照:
👉 [光学エンジニアリングサービス(サイバネット)]

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?