Cooke Triplet with Stock Lenses

Hemkumar Srinivas - May 2025

πŸ“˜ Overview

In this notebook, we design a Cooke Triplet β€” a classic three-element photographic lens known for correcting spherical, chromatic, and coma aberrations. Our goal is to construct a working prototype using off-the-shelf catalog lenses from vendors such as Thorlabs and Edmund Optics.

[1]:
import optiland.backend as be
from optiland import analysis, mtf, optic, psf, wavefront
from optiland import optimization

πŸ” Simulating an Object at Infinity

To simulate an object located at infinity, we set the aperture type to 'EPD' (Entrance Pupil Diameter). This configuration sends a bundle of collimated rays through the optical system, which mimics incoming light from a distant source.

[2]:
lens = optic.Optic()
# add aperture
lens.set_aperture(aperture_type="EPD", value=10)

πŸ”Ή First Lens β€” Best-Form Convex (Thorlabs LBF254-040-A)

We begin with a best-form positive lens from Thorlabs β€” model LBF254-040-A. This lens is used as the front element of the Cooke Triplet to converge the incoming rays.

[3]:
# first lens - best form convex lens
lens.surfaces.add(index=0, radius=be.inf, thickness=be.inf)
lens.surfaces.add(index=1, radius=24.02, thickness=6.5, material="N-BK7")
lens.surfaces.add(index=2, radius=-134.6, thickness=10)

πŸ”Ή Second Lens β€” Bi-Concave (Edmund Optics #45-929)

The middle element of the Cooke Triplet is a negative (bi-concave) lens. We’ll use Edmund Optics lens #45-929, made of N-SF11 flint glass.

Since the optical stop is typically located within or near the second lens in this design, we’ll place the stop on the second surface of this lens.

[4]:
lens.surfaces.add(index=3, radius=-31.82, thickness=2, material="N-SF11")
lens.surfaces.add(index=4, radius=31.82, thickness=10, is_stop=True)

πŸ”Ή Third Lens β€” Best-Form Convex (Again Thorlabs LBF254-040-A)

To complete the triplet, we reuse the same best-form convex lens (Thorlabs LBF254-040-A) as the rear positive element. This symmetric placement helps correct off-axis aberrations.

[5]:
lens.surfaces.add(index=5, radius=134.6, thickness=6.5, material="N-BK7")
lens.surfaces.add(index=6, radius=-24.02, thickness=50)
[6]:
# define surface for the image plane
lens.surfaces.add(index=7)

Now, let’s define the fields and wavelengths over which we would like to analyse the system.

[7]:
# add field
lens.fields.set_type(field_type="angle")
lens.fields.add(y=0)
lens.fields.add(y=5)
lens.fields.add(y=10)

# add wavelength
lens.wavelengths.add(value=0.48)
lens.wavelengths.add(value=0.55, is_primary=True)
lens.wavelengths.add(value=0.65)

Let’s see how the raytrace looks like.

[8]:
lens.draw()
../../_images/gallery_real_world_projects_Cooke_Triplet_with_Stock_Lenses_14_0.png

Looks like we are not far away from the optimal configuratuion. We will now use the optimizer to find the best distances between the lenses. So, we define the optimization problem and the operands as follows. The system will be optimized at 550 nm wavelength.

[9]:
problem = optimization.OptimizationProblem()

input_data = {
    "optic": lens,
    "surface_number": -1,
    "Hx": 0,
    "Hy": 0,
    "num_rays": 5,
    "wavelength": 0.55,
    "distribution": "hexapolar",
}

# add RMS spot size operand
problem.add_operand(
    operand_type="rms_spot_size",
    target=0,
    weight=1,
    input_data=input_data,
)
[10]:
problem.add_variable(lens, "thickness", surface_number=2)
problem.add_variable(lens, "thickness", surface_number=4)
problem.add_variable(lens, "thickness", surface_number=6)

Define optimizer and run

[11]:
optimizer = optimization.OptimizerGeneric(problem)
optimizer.optimize()
[11]:
  message: CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
  success: True
   status: 0
      fun: 0.00029382874404814514
        x: [ 2.836e-01  1.297e-02  4.061e+00]
      nit: 2
      jac: [ 2.051e-05  5.413e-05 -4.469e-05]
     nfev: 16
     njev: 4
 hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64>

Display results

[12]:
problem.info()
╒════╀════════════════════════╀═══════════════════╕
β”‚    β”‚   Merit Function Value β”‚   Improvement (%) β”‚
β•žβ•β•β•β•β•ͺ════════════════════════β•ͺ═══════════════════║
β”‚  0 β”‚            0.000293829 β”‚           99.9225 β”‚
β•˜β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•›
╒════╀════════════════╀══════════╀══════════════╀══════════════╀══════════╀═════════╀═════════╀════════════════╕
β”‚    β”‚ Operand Type   β”‚   Target β”‚ Min. Bound   β”‚ Max. Bound   β”‚   Weight β”‚   Value β”‚   Delta β”‚   Contrib. [%] β”‚
β•žβ•β•β•β•β•ͺ════════════════β•ͺ══════════β•ͺ══════════════β•ͺ══════════════β•ͺ══════════β•ͺ═════════β•ͺ═════════β•ͺ════════════════║
β”‚  0 β”‚ rms spot size  β”‚        0 β”‚              β”‚              β”‚        1 β”‚   0.017 β”‚   0.017 β”‚            100 β”‚
β•˜β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•›
╒════╀═════════════════╀═══════════╀═════════╀══════════════╀══════════════╕
β”‚    β”‚ Variable Type   β”‚   Surface β”‚   Value β”‚ Min. Bound   β”‚ Max. Bound   β”‚
β•žβ•β•β•β•β•ͺ═════════════════β•ͺ═══════════β•ͺ═════════β•ͺ══════════════β•ͺ══════════════║
β”‚  0 β”‚ thickness       β”‚         2 β”‚ 12.8359 β”‚              β”‚              β”‚
β”‚  1 β”‚ thickness       β”‚         4 β”‚ 10.1297 β”‚              β”‚              β”‚
β”‚  2 β”‚ thickness       β”‚         6 β”‚ 50.6111 β”‚              β”‚              β”‚
β•˜β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•›

Draw the final lens

[13]:
lens.draw()
../../_images/gallery_real_world_projects_Cooke_Triplet_with_Stock_Lenses_23_0.png

Now, having optimized the distances between the lenses, we can look at the performance of the system :

[14]:
spot = analysis.SpotDiagram(lens)
spot.view()
../../_images/gallery_real_world_projects_Cooke_Triplet_with_Stock_Lenses_25_0.png
[15]:
distortion = analysis.Distortion(lens)
distortion.view()
../../_images/gallery_real_world_projects_Cooke_Triplet_with_Stock_Lenses_26_0.png
[16]:
grid = analysis.GridDistortion(lens)
grid.view()
../../_images/gallery_real_world_projects_Cooke_Triplet_with_Stock_Lenses_27_0.png
[17]:
geo_mtf = mtf.GeometricMTF(lens)
geo_mtf.view()
../../_images/gallery_real_world_projects_Cooke_Triplet_with_Stock_Lenses_28_0.png

To conclude, we have been able to construct a Cooke Triplet using simple off-the-shelf lenses. Together, these lenses could be mounted on cage systems from Thorlabs. The system seems to perform reasonably over a narrow field of view (about 5 degrees or less) and after this, it exhibits noticeable distortion. Nonetheless, this could be a quick DIY solution for some imaging needs in a lab (eg. Relay lens for a microscope, a compact projection system etc.)