The Zemax Programming Language (ZPL) allows users to write their own programs within OpticStudio. These programs can:
- Automate repetitive keyboard and mouse actions
- Perform calculations based on OpticStudio data
- Export data in specific formats
- Produce graphs and text listings of data
and much, much more!
Creating ZPL macros is easy. This article provides an overview of the key ZPL features, as well as examples of variables and description of the most important functions and keywords.
Authored By Dan Hill, updated by Alessandra Croce
ZPL is a case-insensitive “macro” or “scripting” language, and represents the simplest method of extending OpticStudio’s range of calculations. It is modelled after BASIC and it is an interpreted language. While this means that it’s very easy to code ZPL macros, it also implies that execution will be slower than fully compiled codes for complex calculations.
ZPL scripts can, however, call compiled functions (keywords and operands) inside OpticStudio, and can be of great use in the following cases:
- When you need a unique format for data
- To implement features or calculations not in the program, such as data retrieval, export, or simple plotting
- To optimize when there is no appropriate operand (custom operand creation)
- To create custom/complex solves (custom solve creation)
- To automate repetitive keyboard activity
Note that ZPL cannot be used to provide user-defined surfaces or objects. For this purpose, you can use DLLs instead.
ZPL consists of five basic concepts: variables, operations, functions, keywords and comments. These concepts are all introduced below, however more information can be found in OpticStudio help file section The Programming Tab > About the ZPL
The goal of the ZPL is to give optical engineers, who may not have much programming experience, a powerful programming tool which executes quickly and is easy to learn. Note, however, that OpticStudio also supports ZOS-API, which is an API-level interface allowing programmers access to OpticStudio’s features from external programs, such as MATLAB or Python. See "What is ZOS-API and what can it be used for?" for more details
The basic syntax for an assignment is:
variable = expression
where “ = ” means “set the value to”.
Variables can be either string or numeric, depending upon the type of data they contain. Expressions can be explicit values, other variables, or complex mathematical expressions/functions. Only array variables need to be declared before use, so for example
x = 7 works without declaring the numeric variable
x first. The following are examples of valid assignments:
x = 4
y = 3
z = x + y
String variables must have a $ character as the last character in their name:
newstring$ = "Here is the new string"
Variables are always held as double-precision 64-bit values. There are a few limitations on their names that are to be respected. Variable names should not:
- Be longer than 28 characters
- Include any special character that is used as operator (*, !, <, &, etc.)
- Include any space (use “ _ ” instead)
- Be the same of a keyword or a function
Array (or vector) variables are also supported in ZPL. There are 4 predefined vectors, called
VEC1, VEC2, VEC3, and
VEC4, mainly used by other commands, such as
GETMTF, etc., Their size is set by
User-defined array variables must be declared with the
DECLARE keyword before use. They can be made of up to 4 dimensions and hold double or integer data types.
RELEASE keyword must be used to free the memory allocated to the array.
User-defined array values can be assigned using this syntax:
arrayname(index1, index2, …) = value
Values can be retrieved using this syntax instead:
value = arrayname(index1, index2, …)
Commands in ZPL fall into two categories:
- Keywords make OpticStudio do or change something, for example:
RAYTRACEtraces a specific ray through the system
SETUNITSsets the system units
- Functions make OpticStudio report on something, such as:
y_height = RAYY(5), reporting the Y-coordinate of the ray intercept on surface 5
lens_units$ = $UNITS(), reporting the system’s lens units
Here we will give a few examples of both keywords and functions, however they span the whole range of ray-tracing and physical optics calculations available in OpticStudio. Please refer to OpticStudio help files for a complete listing of all supported functions, keywords, and general ZPL capabilities.
Keywords and functions are calls to compiled routines inside OpticStudio, and are usually very similar to how the relevant feature works in the Ribbon Bar.
They execute as fast as the internal commands, with just a small overhead for calling the command. So don’t consider ZPL to be “slow”! Do bear in mind, however, that calculations performed explicitly inside the macro are interpreted, and are therefore slow in comparison to a compiled program. If calculation speed inside the macro is important, use ZOS-API.NET instead.
Keywords can direct program flow (IF or FOR statements), or perform tasks such as ray tracing or modifying the lens prescription. For example, to tell OpticStudio to optimize a system for 10 cycles, you may send the command:
Again, this executes exactly the same code as when you press the Optimize button and select 10 Cycles in the graphical user interface.
Keywords and Operands
In the early days of Zemax, keywords like
GETZERNIKE were written to allow access to the corresponding analysis features. However, as the code base grew, it soon became apparent that this would become tedious. Therefore, access to most analysis functions is via their associated optimization operand. This can be performed via the merit function, or directly by calling the operand itself from the macro, using
OPEW() numeric functions. See "How to obtain the value for any optimization operand in a ZPL macro using OPEV and OPEW" for further details.
As previously mentioned, functions differ from keywords as they don’t do or change anything in the file, instead they make OpticStudio report on something. ZPL supports numerous functions, both numeric and string. Example of numeric functions are:
x = SQRT(y)
y = ABSO(z*TANG(e*numpoints))
pi = 4*ATAN(1)
Instead, string functions begin with a $ symbol, such as:
surf3_glass$ = $GLASS(3)
Unlike keywords, functions can only be used on the right-hand side of an assignment, or in expressions which are arguments to keywords. Certain functions, such as PWAV() (reporting the system’s primary wavelength), return a value which does not depend upon the argument, and therefore it is not required to provide one. The parentheses, however, are still required.
ZPL supports basic numeric operations (addition, subtraction, division, multiplication) and string operations (addition). Examples of both operations are:
Z = (X + 1)/(Y * 6)
R$ = "Hello " + "World", which assigns string
"Hello World" to variable
Other numeric and string functions, such as
SINE(), SQRT(), $GETSTRING(), etc., can be used for more complex operations. The order of operations can be customized using parentheses, with the default otherwise being: functions (e.g.
SQRT()), logical operators (e.g.
>), multiplication and division, addition and subtraction.
ZPL also supports logical operators, both numeric and string. These can be used for logical tests and comparisons on multiple expressions. The result of a logical operation is either true (any non-zero value) or false (zero). Similarly, OpticStudio treats zero as FALSE and any non-zero value as TRUE.
Below is a table of numeric logical operators:
An example of numeric logical operator is:
IF(A > 5) THEN PRINT "A is greater than 5"
Below is a table of string logical operators
An example of string logical operator is:
IF (C$ $== B$) THEN PRINT "Strings are identical"
It is common to mistake assignment operator “
= ” and logical operator “
= = ”.
For example, consider the macro below:
In the macro, “
a = b” is an assignment, and reads “set the value of variable a equal to that of variable b”. OpticStudio sets the value of a equal to b and returns a logical TRUE because the operation succeeded. Therefore, the macro reads:
PRINT "a equals b"
PRINT "a does not equal b"
In this case, OpticStudio traps the error and returns:
Syntax error: Illegal assignment (a = b).
The correct behavior can be obtained instead using logical operation “
a == b”.
IF and FOR conditional loops are both supported in ZPL.
Syntax for IF loops is the following:
IF (expression) THEN (single command)
IF loops are typically used with logical operators as tests in expression statements. For example:
IF (X < Y)
PRINT "X is less than Y"
PRINT "X is greater than Y"
Note that it is left to the user to trap errors, e.g. what happens if x = y ?
Syntax for FOR loops is the following:
FOR variable, start_value, stop_value, increment
FOR i, 1, 10, 1
The following syntax, defining variable and start value at the same time, is also valid:
FOR i = 1, 10, 1
Note that start_value and stop_value can be explicit numbers as well as any variable or expression that evaluates to a number. The increment should be an integer, or any variable or expression that evaluates to an integer
CALLMACRO allows one macro (parent) to call another one (child). Numeric and string buffers allow data to be shared between parent and child macros. Sample macros “Parent.ZPL” and “Child.ZPL” are included in OpticStudio installation files and are described in help file section The Programming Tab > About the ZPL > Calling a Macro from within a Macro
GOSUB allows execution to jump to a subroutine indicated by
SUB. Control returns to the line following
GOSUB keyword once the subroutine has finished executing. Subroutines can be called multiple times throughout the macro. Note that they are defined in the same macro, they are not separate macros as in the parent/child case previously described. More information on subroutines is provided in OpticStudio help file section The Programming Tab > About the ZPL > KEYWORDS (about the zpl) > GOSUB, SUB, RETURN, and END
For macros to be used in sequential mode, one of the most important keywords is
RAYTRACE, which traces a single ray (identified by normalized field and pupil coordinates) of a given wavelength; the keyword does nothing else. The syntax is:
RAYTRACE hx, hy, px, py, wave
RAYTRACE 0, 1, 0, 0, PWAV(), traces the chief ray at the primary wavelength.
You can then use subsequent functions to extract any relevant ray trace data, such as:
RAYZ(x), returning the ray’s X- Y- and Z-coordinate on surface x
RAYN(x), returning the ray’s direction cosines on surface x
RANZ(x), returning the surface normal direction cosines where the ray hit surface x
RAYT(x), returning the ray’s optical path length up to surface x
OPDC(), returning the ray’s optical path difference with respect to the chief ray
For example, the following code traces the marginal ray at wavelength 2. Then extracts and prints to screen the Y-coordinate intercept of the ray on the image surface:
RAYTRACE 0, 0, 0, 1, 2
PRINT "The chief ray height is ", RAYY(NSUR())
RAYE() reports on any errors that might have occurred when tracing the ray:
- Zero is returned if no errors have occurred during the ray trace
- A negative value indicates that Total Internal Reflection has occurred at the surface whose number is the absolute value of the value returned
- A positive value indicates the ray missed the surface number returned
RAYE() is optional, however the data extracting functions described above may return invalid data if
RAYE() doesn’t return zero.
RAYV() can be used to checked if the ray was vignetted i.e. blocked by an aperture. The function returns the surface number at which the ray was vignetted, or zero if the ray was not vignetted. Remember that rays are still traced if they hit apertures, and can be excluded from calculations by testing with
RAYV() if vignetting has occurred.
RAYTRACEX keyword is a very useful extension to
RAYTRACE. It calls OpticStudio ray tracing routines to trace a particular ray from any starting surface through the current system. The syntax is:
RAYTRACEX x, y, z, l, m, n, surf, wave
Data about the ray trace can then be extracted using the same subsequent functions as
RAYTRACE, as described above.
Finally, comments allow you to document how your ZPL program works. Any line that starts with “ ! ”, and any text that follows “ # ”, is treated as a comment and ignored. OpticStudio will only execute the script that wraps these components together.
For example, imagine that you need to optimize a lens, compute the merit function value and multiply this value by the log of 5. To do this, the ZPL macro is simply:
The execution of a macro in OpticStudio is typically very fast, and ZPL is a simple language to learn and use. To help get you started, there are several articles within the Knowledgebase that demonstrate some of the capabilities of the Zemax Programming Language, and many samples are supplied with your OpticStudio installation.
Next article: How to write a ZPL macro