7.10 Example — Doublet Lens Pupil

PDF section 7.10. Source script: KrakenOS/Examples/Examp_Doublet_Lens_Pupil.py.

Uses PupilCalc Tool to read the entrance- and exit-pupil parameters of the doublet with an inserted aperture stop, then samples a fan pattern at field ±2° and traces it through the system.

Beams generated from the PupilCalc pattern

Figure 17. Beams generated by PupilCalc — the position of the aperture-stop surface determines the pupil layout.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Example: Doublet Lens with Pupil Calculation

This script demonstrates how to simulate a doublet lens system with pupil calculation using KrakenOS.
The system includes:
  - Object Plane (source)
  - First lens surface (convex, BK7)
  - Second lens surface (concave, F2)
  - Air gap (with modified thickness)
  - Pupil surface (defining the pupil of the system)
  - Image Plane (detector)

The script performs a pupil calculation using the PupilCalc tool and then generates a ray pattern based on the pupil.
It traces the rays for two field orientations (FieldY = 2.0 and FieldY = -2.0) and finally displays a 2D visualization.

Author: Joel Herrera V.
Date: 10/03/2025
"""

from importlib import metadata

# =============================================================================
# Check if KrakenOS is installed.
# If not, assume that the code is run from a downloaded GitHub folder and add the relative path.
# =============================================================================
required = {'KrakenOS'}
installed = {dist.metadata["Name"] for dist in metadata.distributions() if dist.metadata.get("Name")}
missing = {pkg for pkg in required if pkg not in installed}

if missing:
    print("KrakenOS is not installed. Using local GitHub folder.")
    import sys
    sys.path.append("../..")  # Adjust this path if necessary

import KrakenOS as Kos  # Import KrakenOS for optical simulation

# =============================================================================
# Define the optical surfaces for the doublet lens system with pupil.
#
# The system is composed of:
#   - Object Plane: Source plane (flat, air)
#   - First Lens Surface: Convex surface made of BK7
#   - Second Lens Surface: Concave surface made of F2
#   - Air Gap: Modified air gap between lens and pupil/image plane
#   - Pupil: Pupil surface with specific displacement and nominal positions
#   - Image Plane: Detector where the final image is formed
# =============================================================================

# Object Plane (Source)
P_Obj = Kos.surf()
P_Obj.Rc = 0.0
P_Obj.Thickness = 100          # Distance to the next surface (mm)
P_Obj.Glass = "AIR"
P_Obj.Diameter = 30.0
P_Obj.Name = "P_Obj"

# First Lens Surface (convex, BK7)
L1a = Kos.surf()
L1a.Rc = 9.284706570002484E+001  # Convex curvature
L1a.Thickness = 6.0              # Lens thickness (mm)
L1a.Glass = "BK7"
L1a.Diameter = 30.0
L1a.Axicon = 0                  # No axicon effect

# Second Lens Surface (concave, F2)
L1b = Kos.surf()
L1b.Rc = -3.071608670000159E+001  # Concave curvature
L1b.Thickness = 3.0              # Separation between surfaces (mm)
L1b.Glass = "F2"
L1b.Diameter = 30

# Air Gap before pupil and image plane
L1c = Kos.surf()
# The thickness is set to (9.737604742910693E+001 - 40) mm to account for design constraints.
L1c.Rc = -7.819730726078505E+001
L1c.Thickness = 9.737604742910693E+001 - 40
L1c.Glass = "AIR"
L1c.Diameter = 30

# Pupil Surface
pupil = Kos.surf()
pupil.Rc = 0
pupil.Thickness = 40.0         # Thickness of the pupil element (mm)
pupil.Glass = "AIR"
pupil.Diameter = 3
pupil.Name = "Pupil"
pupil.DespY = 0.0              # Y displacement (if any)
pupil.Nm_Pos = [-10, 10]       # Name label position

# Image Plane (Detector)
P_Ima = Kos.surf()
P_Ima.Rc = 0.0
P_Ima.Thickness = 0.0
P_Ima.Glass = "AIR"
P_Ima.Diameter = 20.0
P_Ima.Name = "P_Ima"
P_Ima.Nm_Pos = [-10, 10]       # Name label position

# =============================================================================
# Assemble the optical surfaces into a system.
#
# The order of surfaces in list A defines the optical sequence.
# =============================================================================
A = [P_Obj, L1a, L1b, L1c, pupil, P_Ima]
config_1 = Kos.Setup()
Doublet = Kos.system(A, config_1)
RayKeeper = Kos.raykeeper(Doublet)  # Container to store traced rays

# =============================================================================
# Pupil Calculation
#
# Use the PupilCalc tool to compute pupil parameters.
#
# Parameters:
#  - sup: Index of the surface where the pupil is defined (here, 4, as pupil is the 5th surface)
#  - W: Wavelength (0.4)
#  - AperType: Type of aperture ("STOP")
#  - AperVal: Aperture value (3)
# =============================================================================
W = 0.4        # Wavelength
sup = 4        # Surface index for pupil (0-based indexing: pupil is the 5th surface)
AperVal = 3    # Aperture value
AperType = "STOP"  # Aperture type

Pup = Kos.PupilCalc(Doublet, sup, W, AperType, AperVal)

# Print pupil parameters for analysis
print("Input pupil radius:")
print(Pup.RadPupInp)
print("Input pupil position:")
print(Pup.PosPupInp)
print("Output pupil radius:")
print(Pup.RadPupOut)
print("Output pupil position:")
print(Pup.PosPupOut)
print("Output pupil position relative to the focal plane:")
print(Pup.PosPupOutFoc)
print("Output pupil orientation:")
print(Pup.DirPupSal)
print("Airy disk diameter at focal distance (micrometers):")
print(Pup.FocusAiryRadius)

# Extract the output pupil orientation (direction cosines)
[L, M, N] = Pup.DirPupSal
print("Pupil output direction cosines:", L, M, N)

# =============================================================================
# Generate the ray pattern based on the pupil calculation.
#
# The pupil configuration is used to generate a set of rays on the image plane.
# Two field patterns are generated: one for FieldY = 2.0 and one for FieldY = -2.0.
# =============================================================================

# First field pattern: FieldY = 2.0
Pup.Samp = 7           # Number of samples
Pup.Ptype = "fany"      # Pattern type (e.g., fan-like distribution)
Pup.FieldType = "angle" # Field defined in terms of angle
Pup.FieldY = 2.0       # Field parameter (positive Y direction)
x, y, z, L, M, N = Pup.Pattern2Field()  # Generate field pattern

# Trace rays for the first pattern
for i in range(len(x)):
    pSource_0 = [x[i], y[i], z[i]]   # Ray origin
    dCos = [L[i], M[i], N[i]]         # Ray direction cosines
    Doublet.Trace(pSource_0, dCos, W)
    RayKeeper.push()

# Second field pattern: FieldY = -2.0
Pup.FieldY = -2.0
x, y, z, L, M, N = Pup.Pattern2Field()  # Generate field pattern for negative FieldY
for i in range(len(x)):
    pSource_0 = [x[i], y[i], z[i]]
    dCos = [L[i], M[i], N[i]]
    Doublet.Trace(pSource_0, dCos, W)
    RayKeeper.push()

# =============================================================================
# Display the final ray tracing result.
#
# You can choose between 3D and 2D visualization. Here, a 2D plot is generated.
# =============================================================================
# Uncomment the following line for 3D visualization:
# Kos.display3d(Doublet, RayKeeper, 2)
Kos.display2d(Doublet, RayKeeper, 0, 1)