Soon, we will launch a new and updated customer portal, which is an important step toward providing our customers with one place to learn, interact, and get help.
Learn more.

How to build and optimize a singlet using ZOS-API with Python

In this article, we will show how to create a Singlet Lens, add targets with the Optimization Wizard and optimize using the new ZOS-API with Python. 
Thomas Aumeyr
Programming Zemax


An application programming interface (ZOS-API) has been developed for OpticStudio that enables connections to, and customization of, the application using the latest software technology. While ZOS-API relies on a COM interface, it is rooted in .NET libraries, and as such programming with the API can be done in a number of languages.
The connection between your application program and OpticStudio is classified in 1 of 4 Program Modes. These modes can be generally grouped into 2 categories: 1) Full Control (Standalone and User Extensions modes), in which the user generally has full control over the lens design and user interface and 2) Limited Access (User Operands and User Analysis modes), in which the user is locked down to working with a copy of the existing lens file.

Open new boilerplate template

For the purpose of this article we will focus on the Standalone Mode, as this is the only mode available for Python. Let’s create a boilerplate solution to begin with, so we need to select Python>Standalone Application

A Windows Explorer opens with the solution folder ‘..\Documents\Zemax\ZOS-API Projects\PythonStandaloneApplication’.
The Python project and script was tested with the following tools:
  • Python 3.4.3 for Windows (32-bit) ( - Python interpreter
  • Python for Windows Extensions (32-bit, Python 3.4) ( - for COM support
  • Microsoft Visual Studio Express 2013 for Windows Desktop ( - easy-to-use IDE
  • Python Tools for Visual Studio ( - integration into Visual Studio 
Note that Visual Studio and Python Tools make development easier, however the Python script should run without either installed. If Visual Studio is installed, it opens with your new solution. The solution contains a boilerplate code that can be used as the basis for any Standalone Application.

Make a New File and set the System 

We will design a singlet lens with the following specifications:
  • Light comes from infinity, with a 5 degree semi-field of view, and a single wavelength (d-line, .587 mm)
  • Collimated input light is to be focused to smallest RMS spot size, averaged across the field of view
  • F/10, EPD 40 mm
  • Made from N-BK7
  • Stop is a separate surface and is free to move but comes after the lens
  • Lens should be at least 3, and no more than 15 mm thick at center
  • Lens should have a minimum edge thickness of 3 mm, and air gaps should be a minimum of 0.5 mm
In your script, go to “# Insert Code Here” and then enter
        TheSystem = zosapi.TheSystem;
This represents a complete optical system, which corresponds to a single .ZMX file. Now create a string containing the file path for the new file. To make the script flexible, we will use the SamplesDir property to create file path .
        fileOut = zosapi.TheApplication.SamplesDir + "\Sequential\Objectives\Single Lens Example wizard+EFFL.zmx";
TheSystemData holds all the basic data of the System. We can use this to set aperture, field points, wavelengths, etc.
    # Aperture
    TheSystemData = TheSystem.SystemData;
    TheSystemData.Aperture.ApertureValue = 40;
  # Fields
  # Wavelength preset

For enumerated constants like WavelengthPreset, Python creates each constant as {enum name}_{enum value}.

Setting up the Lens Data Editor 

In the LDE, each row represents a surface defined in the System: Surface 0 is the Object and the last Surface is the Image. For a simple lens located before the stop, the front side of the lens would be Surface 1, the rear side of the lens is Surface 2 and the Stop would be at Surface 3.
  # Lens data
    TheLDE = TheSystem.LDE;
    Surface_1 = TheLDE.GetSurfaceAt(1);
    Surface_2 = TheLDE.GetSurfaceAt(2);
    Surface_3 = TheLDE.GetSurfaceAt(3);
    Surface_1.Thickness = 10.0;
    Surface_1.Comment = 'front of lens';
    Surface_1.Material = 'N-BK7';
    Surface_2.Thickness = 50.0;
    Surface_2.Comment = 'rear of lens';     
    Surface_3.Comment = 'Stop is free to move';   
    Surface_3.Thickness = 350.0;

We have 5 independent variables, which we can use to achieve our goal – the front and back radius of the lens, the lens thickness, the location of the stop and the image location.
  # Make thicknesses and radii variable

After this, the LDE will look like this

Setting up the Merit Function Editor

We will use the sequential Optimization Wizard to make a Default Merit Function target the Minimum Spot Radius also specifying the Glass/Air Boundary Values.

  # Merit functions
    TheMFE = TheSystem.MFE;    
    wizard = TheMFE.SEQOptimizationWizard;
    wizard.Type = 0;        # RMS
    wizard.Data = 1;        # Spot Radius
    wizard.Reference = 0;   # Centroid
    wizard.Ring = 2;        # 3 Rings
    wizard.Arm = 0;         # 6 Arms
    wizard.IsGlassUsed = True;
    wizard.GlassMin = 3;
    wizard.GlassMax = 15;
    wizard.GlassEdge = 3;
    wizard.IsAirUsed = True;
    wizard.AirMin = 0.5;
    wizard.AirMax = 1000;
    wizard.AirEdge = 0.5;
    wizard.IsAssumeAxialSymmetryUsed = True;

The OK button method is part of the CommonSettings of all wizards available in OpticStudio.

Since the lens is before the stop, we cannot use a F/# solve. We don’t know which ray is the marginal ray until it has hit the edge of the stop. This means we must use the EFFL operand in addition to the RMS spot default merit function. Just add an extra line at the top, target the EFL to 400mm. Remember to add a weight of 1.

    Operand_1.Target = 400.0;
    Operand_1.Weight = 1.0;

After this, the Merit Function will look like this

Local Optimization

Now let’s access the properties and methods for running a Local Optimization and set it up like in the Graphical User Interface.

  # Local optimisation till completion
    LocalOpt = TheSystem.Tools.OpenLocalOptimization();
    LocalOpt.Algorithm = constants.OptimizationAlgorithm_DampedLeastSquares;
    LocalOpt.Cycles = constants.OptimizationCycles_Automatic;
    LocalOpt.NumberOfCores = 8;
    baseTool = CastTo(LocalOpt, "ISystemTool")

Due to how the Python COM interface handles inheritance, OpenLocalOptimization() returns an ILocalOptimization interface, while all of the run/close/etc. are on the ISystemTool interface. Therefore, you need to cast the derived class object to a base class object to get access to things that live in the base.

Finally, save the lens file after the optimisation finishes.

  # Save and close



After running your script, open OpticStudio and check out your fully optimised lens.