How to create a user-defined solve using the Zemax Programming Language (ZPL)

This article demonstrates how ZPL can be used to create user-defined solves, and it consists of two examples. The first explains how to create a ZPL solve to ensure the image plane of a sequential file has a Radius equivalent to the system’s Petzval curvature. The second shows instead how to constrain an Object position in the Non-Sequential Component Editor based on other another object’s parameters.

Authored By Nam-Hyong Kim, updated by Alessandra Croce


Article Attachments


Solves are functions which actively adjust specific values in an editor, such as the Lens Data Editor or the Non-Sequential Component Editor. They can be specified for example on the radius, conic or TCE of a surface, and are set by clicking the solve box for the cell you wish to place the solve on. While OpticStudio provides many default solve types, users may want to create their own. This is possible through the use of the Zemax Programming Language (ZPL).

ZPL macro solves are available for almost any cell in any editor (radius, thickness, parameters, multi-configs, etc.). Like any other solve, they can be accessed by clicking on the small box to the right of a parameter cell in the editor.

ZPL macro solves execute a ZPL macro to determine the solve value, which is returned to the editor using SOLVERETURN keyword. Once the macro describing the solve has been created and placed in the <Documents>\Zemax\Macros directory, the name of that macro can be entered in the "Macro:" setting in the solve window: 


Note that the name of the macro to be entered in the solve box is case insensitive, and that its extension (.ZPL) is not required.

There are a few rules to follow to ensure macro solves work as expected. Please refer to section "Tricks and gotchas" for more information.

Petzval Curvature solve example

Suppose we want a solve that automatically sets the radius of the image plane equal to the Petzval curvature. Of course, always start by checking that a solve isn’t already supported before writing a macro!

Load the file <Documents>\Zemax\Samples\Sequential\Objectives\Petzval.zmx


The macro solve will need to compute the Petzval curvature of the system and then return the value to OpticStudio. The Petzval curvature of the system is obtained via PETC optimization operand, that can be called directly in the macro via OPEV() and OCOD() functions:

  • OCOD() retrieves the unique numeric code corresponding to an optimization operand type
  • OPEV() computes an operand value. It requires as a first argument the unique code retrieved by OCOD(), followed by the optimization operand’s arguments

In our case:


Note that SOLVERETURN keyword is used to pass the data back to the editor. Save the macro as PETC.zpl in the Macros folder. Curvature solve value must be returned as curvature (1/R), not radius. All other solves return value directly

To apply the macro, select the solve box in the radius cell of the image surface and select ZPL macro solve type. Then type the macro name in the cell:


And voila! OpticStudio automatically updates the solve and places a Z in the Solve Type box to indicate it is active:


Object Position solve example

As a second example, we will require a non-sequential object to be 10 mm behind the end of another non-sequential object. To achieve this, we need to calculate the length of the previous object, and to use this to position the second object.

In the non-nequential file found in the Article Attachments, the first object is a rectangular Compound Parabolic Concentrator (CPC) object. This object has a maximum length defined as a parameter in the Non-Sequential Component Editor (NSCE). If we could guarantee that the object would always be this maximum length, we could simply use a pick-up solve with an offset to position object 2. However, the real length may be smaller than the maximum length parameter, so we must calculate the real length and use this value. Also, as this is a Rectangular CPC object, the real length may be different for the x and y slices: we must choose the smaller of the two. This is an ideal problem for the macro solve!

The length "L" of the CPC is given by the formula in help file section The Setup Tab > Editors Group (Setup Tab) > Non-Sequential Component Editor > Non-Sequential Geometry Objects > Compound Parabolic Concentrator (CPC):


Since this is a Rectangular CPC object, there are two associated length values: X and Y. The smaller of the two lengths is then taken as the length of the CPC by OpticStudio, unless this length is larger than the "Length" parameter. In that case, the length is the value of the "Length" parameter. The macro should therefore calculate the X and Y length of the CPC object by using the formula above, pick the smaller of the two values, add 10 mm offset and return that value to the Z Position parameter of the Rectangle object. If the smaller length between X and Y is still larger than the "Length" parameter value, then the macro should add 10 mm offset to the "Length" parameter value and return this value instead.

The REC_CPC.ZPL macro, performing the actions explained above, can be found in the Article Attachments:


As previously explained, the keyword SOLVERETURN is used to pass the desired value back to the NSCE. Once the macro is placed in the <Documents>\Zemax\Macros folder, click Macros...Refresh Macro List in the main menu.

In the solve box of parameter “Z Position” of object 2 (Rectangle), select ZPL Macro and type in the name of the macro solve:


Just like the previous example, the NSCE will show "Z" next to the parameter, indicating that a ZPL solve has been placed on that parameter:


Note what happens if you use Ctrl-A to toggle between the 2 configurations defined in the file. The Y angle parameter of the Rectangular CPC object changes between 20 and 40 degrees, hence changing the length of the object. As this happens, the ZPL solve applied to the Z Position of object 2 changes automatically to always place this object 10 mm behind the end of the CPC object, as per desired behavior.

Tricks and gotchas

Macro solves are very general, allowing virtually any computation to be used to determine a solve value. All functions and keywords supported by ZPL are available. There are no differences between ZPL macros executed from the macro menu or executed as a solve. However, you should take care when writing ZPL solves:

  • Many ZPL keywords and some functions should not be used in a macro solve. For example, calling UPDATE from the solve will update all solves, which will invoke the macro again, resulting in an infinite loop. Other keywords, such as INPUT, will require the user to enter data every time the solve is called, which may make its use impractical.
  • Only update the cell that the solve is applied to. The macro should not update other cells or otherwise modify the system (for example, it should not optimize).
  • Macro solves should never depend upon data in the editors that is subsequent to the solve, as this may also create erroneous data if the solve is called first and then the source data is modified by a separate, subsequent solve.
  • Macro solves may be placed prior to the stop surface only if the computations in the macro aren’t based upon any ray data. For more information, please refer to help file section The Programming Tab…About the ZPL > KEYWORDS (about the zpl) > SOLVEBEFORESTOP. 
  • To make solves robust and as general as possible, it is advisable to use SURC() and SOSO() functions when surface referencing is needed:
    • SURC() refers to surfaces via their (unique) comment string
    • SOSO() gets the surface/object number the solve is placed on
  • Bear in mind that, if you use a ZPL macro solve on any Radius cell in the Lens Data Editor, all solves for this parameter compute the curvature (1/R), and not the radius of curvature (R). This means that a solve applied to this parameter should return the inverse of the radius of curvature, not the radius itself.
  • The user is responsible for error-checking the data returned by the macro. For example, if the macro calls a RAYTRACE to evaluate a ray parameter, RAYE() should be used to ensure that no error (e.g. vignetting or TIR) has occurred. If an error occurs, the macro should exit without calling SOLVERETURN to ensure no value is returned to the cell. This is particularly important during optimization. The following code snippet gives an example of how to test for errors. 


  • In general, macro solves should be kept short and simple, and lengthy computations avoided.
  • Finally, note that OpticStudio makes no attempt to qualify or validate the solve macro. This feature offers great power and flexibility, but must be used carefully.



Was this article helpful?
0 out of 0 found this helpful



Please sign in to leave a comment.