ZOS-API using Python.NET

This article will show how to use Python for .NET (pythonnet) to communicate with ZOS-API. Since ZOS-API is written using the .NET Framework, using languages that can directly communicate with .NET gives the most flexibility and the best performance.

Authored By Michael Humphreys, Sandrine Auriol

Introduction

This article will explain how to use Python for .NET and will outline the differences between using .NET via the pythonnet module and using COM via the pywin32 module to connect Python to the ZOS-API.

Why Use Python for .NET

Up until OptiStudio 20.1, the method for connecting Python to the ZOS-API uses COM. This is an older technology which requires interfaces, classes, objects and methods to be registered in Windows Registry. All communication between Python and ZOS-API are translated through an intermediate layer by a 3rd party module called pywin32. This technology cannot take advantage of all the benefits of the ZOS-API.NET such as:

  • Inheritance: To get around this shortcoming, pywin32 uses an inheritance method called CastTo().
  • Enumerated variables: ZOS-API uses a single enum dictionary called constants.
  • Interfaces: Interfaces, classes, objects and methods can only be registered one time with Windows, so it is impossible to test and run the ZOS-API between 2 versions of OpticStudio without registering the objects with the Windows Registery.

Since the ZOS-API is written using the .NET Framework, using languages that can directly communicate with .NET will give the most flexibility and the best performance. Rather than using the pywin32 module to connect Python to the ZOS-API, this new template uses the pythonnet module to make the connection.

There are several benefits to this approach:

  1. Object inheritance is automatic (no more using the CastTo() method).
  2. Enums are properly handled (no more using the constants dictionary).
  3. It is possible to test and run the ZOS-API between 2 versions of OpticStudio (just like in C# or Matlab).
  4. External DLL for batch processing large data sets can be used, like such as for example the “RayTrace.dll” when performing a sequential ray trace or parsing a ZRD file.

For example, in “PythonStandalone_01_new_file_and_quickfocus.py”, here is the code using COM:


#! [e01s08_py]
# QuickFocus
quickFocus = TheSystem.Tools.OpenQuickFocus()
quickFocus.Criterion = constants. QuickFocusCriterion_SpotSizeRadial
quickFocus.UseCentroid = True
quickFocusCast = CastTo(quickFocus,'ISystemTool')
quickFocusCast.RunAndWaitForCompletion()
quickFocusCast.Close()
#! [e01s08_py]

Here is the same code using .NET:

#! [e01s08_py]
# QuickFocus
quickFocus = TheSystem.Tools.OpenQuickFocus()
quickFocus.Criterion = ZOSAPI.Tools.General.QuickFocusCriterion.SpotSizeRadial
quickFocus.UseCentroid = True   
quickFocus.RunAndWaitForCompletion()
quickFocus.Close()
#! [e01s08_py]

Installation

If you do not have Python already installed, please first read the "Getting started with Python" article.

The following instructions are for installing the pythonnet module via PIP.

  1. Ensure you are running Python 3.4, 3.5, 3.6, 3.7, or 3.8. PythonNET is continually updated to work with more recent editions of Python, but Zemax staff has not tested compatibility with any versions later than 3.8.
  2. Pythonnet is a package that gives Python programmers an integration with the .NET Common Language Runtime (CLR). The ZOS-API is a .NET 4.8-based library, so the PythonNET version should support .NET 4.8 to be used with ZOS-API. From our latest tests, PythonNET 3.0 doesn't work with ZOS-API so for now we recommend using PythonNET 2.5.2.

  3. Install pythonnet installed from pip or check if it is installed.

Open a Command Prompt window and type:

python -m pip install pythonnet

The message below means that pythonnet is installed:

 

Command

Importing modules

The only module that is needed is the clr module. It allows Common Language Runtime (CLR) namespaces to be treated essentially as Python packages. CLR is a runtime environment for .NET program codes.

Other modules that can be useful are numpy and matplotlib.

  • Numpy helps when passing array data for example from a DLL to Python.
  • Matplotlib is the easiest way to plot the data. 

Both are extra modules that can be downloaded from pip manually.  Note that numpy is a dependency for matplotlib, so if you install matplotlib you’ll have both modules:

python -m pip install matplotlib

In order to efficiently pass the array data to numpy, you will need to read the data directly from memory so you will need to include ctypes, sys, GCHandle, and GCHandleType (these are already in the template for Python for .NET generated from OpticStudio). The import section of the Python file will look like this:

# THESE ARE REQUIRED FOR PYTHONNET
# common language runtime for PythonNET
import clr
# used to read memory address from .NET arrays and pass into NUMPY arrays
import ctypes, sys
from System.Runtime.InteropServices import GCHandle, GCHandleType

# THIS IS ALL-BUT-REQUIRED FOR PYTHON VISUALIZATION
# Python visualization and matrix manipulation
import matplotlib.pyplot as plt, numpy as np

# THESE ARE ONLY NEEDED FOR THIS SCRIPT
import os, winreg

Then the following line will add a DLL like ZOS-API libraries as a reference.

clr.AddReference()

Using Python Restricted Words in Enums

There are a few cases in the ZOS-API where we have enums which use a Python restricted word. A Python restricted word is a word that is defined In Python and has predefined meaning and syntax in the language. If you are using an IDE with syntax highlighting, these are typically words which change color (often to blue) when you complete the word. In that case we can’t use the Python restricted word.

One Python restricted word is “None”, but some enums in ZOS-API have an option of “None”. To get around this issue, you can import the Enum class from the System namespace and then use Enum.Parse().

from System import Enum
win_settings.Polarization = Enum.Parse(ZOSAPI.Analysis.Settings.Polarization, "None");

Passing Multiple “Out” Variables

One thing to note when using Python for .NET is that it does not know during runtime if a method’s arguments are inputs or outputs from the method. Therefore, when using a method with “out” variables, these need to have a dummy placeholder when calling the function in Python.

For example, if you have the following method in C#:

public bool TestFunction(double x, double y,
    out double x_times_y, out double x_dividedBy_y)
{
    x_times_y = x * y;
    if (y != 0)
    { x_dividedBy_y = x / y; }
    else
    { x_dividedBy_y = double.NaN; }
    return true;
}

You would need to call that function like this in Python:

success, x_times_y, x_dividedBy_y = reader.TestFunction(1.2, 2.3, 0, 0)

where the last 2 arguments (the 0’s) are just dummy variables.

Python for .NET can often automatically detect the type of the output argument, so you will simply need to pass a Python variable to the API method. However, there are some methods where Python for .NET doesn't detect the type of the output argument, especially when methods under the same namespace have the same name but belongs to different interfaces. The methods can have different output variables. In this case, you will need to explicitly define and pass the variable as a NET variable.  You can explicitly call a NET variable by importing the variable type and initializing the variable to a default value:

from System import Int32, Double
# Python NET requires all arguments to be passed in as reference, so need to have placeholders
sysInt = Int32(1)
sysDbl = Double(1.0)

Examples 22 and 23 in the {Zemax}\ZOS-API Sample Code\Python directory show with the ReadNextResult() method.

Using an external C# DLL

Python.net can use external DLLs for batch processing large data sets. For example, it can use directly the “RayTrace.dll” (for more information see the "Batch Processing of Ray Trace Data using ZOS-API" article) when performing a sequential ray trace or parsing a ZRD file.

When creating a new external C# DLL to interact with Python.NET, here are a few points to check:

  1. The C# class has to be declared public because undeclared classes are marked as internal by default and not accessible to PythonNET CLR.
  2. In the Solution Explorer, access the Project’s Properties and ensure the Application > Output type is set to Class Library
  3. Compile the DLL in either Debug Mode or Release Mode (if you want to distribute the DLL to other users, make sure you select Release Mode)

Loading the DLL

To load the C# DLL, you can simply use the clr.AddReference method and pass in the file location for the DLL:

# load the DLL
clr.AddReference(os.path.abspath(os.path.join(os.path.abspath(''), '..', r'ZBFReader\bin\Release\ZOSReader.dll')));
Then you can import the namespace(s) from the DLL:
# import the namespace
import Reader

Calling a Class

To call a class, simply use the namespace.class_init function.

# load the class
reader = Reader.ZBF(os.path.join(ZemaxDataDir, r'POP\BEAMFILES\F.ZBF'))

Converting from double[] to Numpy

Python is inefficient at looping through arrays so using built in commands like map or filter and passing arrays off to a compiled library (such as numpy) is much more efficient.

  • For arrays that only have a few indices, you can read these values directly.
  • However, for arrays that have hundreds or thousands of indices, you should use numpy to iterate and manipulate the data.  All the arrays that are being passed back from the C# DLL will be in the form of int[] or double[], so we need to convert this to a format that numpy understands.  Since iterating over each index and passing this to a numpy array will be inefficient, we need to have a new function (def) which will read the int[] or double[] from its memory address and pass the values from memory directly into numpy.  The following function takes a double[] array and returns a numpy array:

def DoubleToNumpy(data):
    if 'numpy' not in sys.modules:
        print('You have not imported numpy into this file')
        return False
    else:
        src_hndl = GCHandle.Alloc(data, GCHandleType.Pinned)
        try:
            src_ptr = src_hndl.AddrOfPinnedObject().ToInt64()
            cbuf = (ctypes.c_double*len(data)).from_address(src_ptr)
            npData = np.frombuffer(cbuf, dtype=np.float64)
        finally:
            if src_hndl.IsAllocated: src_hndl.Free()
        return npData

 

KA-01951

Was this article helpful?
7 out of 16 found this helpful

Comments

0 comments

Article is closed for comments.