How to read a static data file into a user-defined surface

There are times when it is necessary to read fixed data into OpticStudio for use during ray trace calculations. This article explains how to take data from a text file and use it to create a user-defined source. This technique can be generalized for arbitrary surface and data types.

Authored By Sanjay Gangadhara


Article Attachments


There are times when it is necessary to read fixed data into OpticStudio for use during ray trace calculations. Using data to define an arbitrary apodization function is one such example. This is useful for cases when this data cannot be fit to any of the existing models in OpticStudio. Such data can be read into OpticStudio using a user-defined surface. More information on how to create a user-defined surface may be found in the Knowledgebase article "How to compile a user-defined DLL".

Reading static data from a file into a user-defined surface

An example (Reading_filter.ZMX) illustrating the use of a user-defined surface for defining an apodization function is available as an article attachment. This design contains two lenses: one to collimate light from an on-axis field point, and one to focus the beam back down to a point on the image plane. Situated between these lenses is a surface which acts as a transmission filter. The system is modeled with multiple configurations – an apodization function is applied to the intermediate surface in configuration 1 (providing finite values for the transmission of this surface) but not in configuration 2 (the transmission is therefore unity over the surface in this case). The apodization function used in configuration 1 is read in from the file "Transmission.txt". This function is applied to the intermediate surface (surface 3 in this case) through the use of a user-defined surface. The source code and DLL for this surface (named "US_FILT_FILE.cpp" and "US_FILT_FILE.DLL", respectively) are provided as an attachment to this article. The transmission data file is also provided in the attached ZIP file, and should be placed in the {Zemax}\Miscellaneous directory.

The DLL is very similar to that used for defining the surface US_FILT (see the Help System file The Setup Tab...Editors Group (Setup Tab)...Lens Data Editor...Sequential Surfaces (lens data editor)...User Defined). However, rather than specifying an exponential variation of the apodization function with the normalized radial coordinate (r), US_FILT_FILE.DLL allows the user to specify the value of the transmission at various values of r. In the data file, you need to specify the number of data elements in the first row, and then values for r and the associated transmission in subsequent rows:


In this DLL, values for the transmission at arbitrary values of r are calculated from linear interpolation of the inputted data.

As with any other DLL in OpticStudio, this DLL is called every time that rays are traced (e.g. in the Layout diagram, an analysis feature, etc.). To maximize computational efficiency, the ray trace calculations in OpticStudio are multi-threaded, meaning that there are generally many copies of the DLL which are being simultaneously opened during calculation. If it was necessary to read the data in to OpticStudio each time a copy of the DLL was made (e.g. each time a new analysis feature is opened or a current one is updated), this would slow down the computation time immensely. Fortunately, this is not the case. The data can be read in once, when the DLL is first loaded into OpticStudio (e.g. when the surface type is set to User Defined and the Surface DLL is then set to USER_FILT_FILE.DLL), during the DLL_PROCESS_ATTACH step:

BOOL WINAPI DllMain (HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved)


FILE *in;

struct parse_type *pars;



/* Read in the data when the DLL first loads */


/* Allocate memory for the transmission data */

xtrans = (double*)malloc(MAX_PATH_LENGTH * (MAX_TEXT_PARS+1));

trans = (double*)malloc(MAX_PATH_LENGTH * (MAX_TEXT_PARS+1));

pars = (parse_type*)malloc(sizeof(struct parse_type) * (MAX_TEXT_PARS+1));

/* Open the text file containing the

transmission data */

in = NULL;

if ((in = fopen("C:\\Program Files\\Zemax\\DLL\\Transmission.txt", "rt")) == NULL) return(0);

/* Read in the number of data points */

fgets(disp, MAX_PATH_LENGTH, in);

n_trans = atoi(disp);

/* Read in the transmission data */

for (i=1; i<=n_trans; i++)


fgets(disp, MAX_PATH_LENGTH, in);

Parser(disp, pars, MAX_TEXT_PARS);

xtrans[i] = strtod(pars[1].n,NULL);

trans[i] = strtod(pars[2].n,NULL);


/* Close the file, and free the memory associated

with the text parser */

if (in) fclose(in);

in = NULL;

if (pars) free(pars);

pars = NULL;



/* Free the memory associated with the transmission

data once the DLL is unloaded */

if (xtrans) free(xtrans);

xtrans = NULL;

if (trans) free(trans);

trans = NULL;



return TRUE;


During this step (which only occurs when the DLL is first loaded into OpticStudio), memory is allocated to those variables which will contain the coordinate and transmission data (in this case XTRANS and TRANS, respectively) as well as to a variable which will be used to read data in from the file (PARS). The file is then opened and data read from it. Once all of the data has been read and placed into the appropriate variables, the file is closed and the memory associated with the file-read variable is released. By defining XTRANS and TRANS to be global variables (i.e. by initializing them at the beginning of the program, before the DllMain call), this data is then available to the DLL for all subsequent ray traces:

/* define the global variables, which will be available

during all calls to the DLL */

struct parse_type




int n_trans, i;

char disp[MAX_PATH_LENGTH];

double *xtrans, *trans;

Data could also have been read into this DLL using the case 8 flag under the switch(FD->type) within the DLL itself. However, case 8 – which corresponds to the DLL initialization flag – is called every time the DLL is called (e.g. if a new analysis feature is opened or a current one is updated). This method would therefore require that the data be read in multiple times. It is simply much more efficient to read the data in once during the DLL_PROCESS_ATTACH step. Since the data is read in only once, ray-tracing is not slowed by use of the external data. A sample illustrating this method is provided in the attachment as well ("US_FILT_FILE2.cpp" and "US_FILT_FILE2.DLL"). Note that because the DLL is being called by multiple threads, the use of critical sections is required. Another modification to this source code is the ability to specify the filter file name as the comment in the LDE.

The data is only unloaded from memory when the DLL is unloaded from OpticStudio, e.g. when the program is closed or the surface type is changed. At this point the DLL_PROCESS_DETACH step is initiated, during which the memory allocated for the global variables is released. If the DLL is then re-loaded into OpticStudio, data from the file will again need to be read in, but only once. If data in the file has changed and needs to be read back into OpticStudio, you can therefore unload the DLL (by changing the surface type) and then reload it, and the new data will be read in.

This example illustrates a useful way in which external data may be read into OpticStudio. This technique can be generalized for arbitrary surface and data types.


Was this article helpful?
1 out of 2 found this helpful



Article is closed for comments.