In [1]:
from __future__ import print_function
import os
import sys
import numpy as np
from IPython.display import display, Image, YouTubeVideo
import matplotlib.pyplot as plt
# Imports for using ZOS API in Python directly with pywin32
# (not required if using PyZOS)
from win32com.client.gencache import EnsureDispatch, EnsureModule
from win32com.client import CastTo, constants
# Import for using ZOS API in Python using PyZOS
import pyzos.zos as zos
We will use the following two Zemax knowledgebase articles as base material for this discussion. Especially, the code from the first article is used to compare and illustrate the enhanced features of PyZOS library.
"How to build and optimize a singlet using ZOS-API with Python," Zemax KB, 12/16/2015, Thomas Aumeyr.
"Interfacing to OpticStudio from Mathematica," Zemax KB, 05/03/2015, David.
In [2]:
# Set this variable to True or False to use ZOS API with
# the PyZOS library or without it respectively.
USE_PYZOS = True
The PyZOS library provides three functions (instance methods of OpticalSystem
) for the sync-with-ui mechanism.
The sync-with-ui mechanism may be turned on during the instantiation of the OpticalSystem
object using the parameter sync_ui
set to True
(see the screen-shot video below) or using the method zSyncWithUI()
of the OpticalSystem
instance at other times.
The other two functions
zPushLens()
-- for copying a lens from the headless ZOS COM server (invisible) to a running user-interface application (visible) zGetRefresh()
-- for copying a lens from the running user-interface application (visible) to the headless ZOS COM server (invisible).enable the user to interact with a running OpticStudio user-interface and observe the changes made through the API instantly in the user-interface.
(If you cannot see the video in the frame below, or if the resolution is not good enough within this notebook, here is a direct link to the Youtube video: https://www.youtube.com/watch?v=ot5CrjMXc_w&feature=youtu.be)
In [3]:
# a screenshot video of the feature:
display(YouTubeVideo("ot5CrjMXc_w?t", width=900, height=600))
The first enhancement is the complete elimination of boiler-plate code to get create and initialize an optical system object and get started. We just need to create an instance of a PyZOS OpticalSystem
to get started. The optical system can be either a sequential or a non-sequential system.
In [4]:
if USE_PYZOS:
osys = zos.OpticalSystem() # Directly get the Primary Optical system
else:
# using ZOS API directly with pywin32
EnsureModule('ZOSAPI_Interfaces', 0, 1, 0)
connect = EnsureDispatch('ZOSAPI.ZOSAPI_Connection')
app = connect.CreateNewApplication() # The Application
osys = app.PrimarySystem # Optical system (primary)
# common
osys.New(False)
It may seem that if we are using PyZOS, then the application is not available. In fact, it is available through a property of the PyZOS OpticalSystem
object:
In [5]:
if USE_PYZOS:
print(osys.pTheApplication)
Because of the way property attributes are mapped by the PyWin32 library, the properties are not introspectable (visible) on Tab press in smart editors such as IPython. Only the methods are shown upon pressing the Tab button (see figure below). Note that although the properties are not "visible" they are still accessible.
PyZOS enhances the user experience by showing both the method and the properties of the ZOS object (by creating a wrapped object and delegating the attribute access to the underlying ZOS API COM objects). In addition, the properties are easily identified by the prefix p in front of the property attribute names.
In [6]:
Image('./images/00_01_property_attribute.png')
Out[6]:
In [7]:
if USE_PYZOS:
sdir = osys.pTheApplication.pSamplesDir
else:
sdir = osys.TheApplication.SamplesDir
sdir
Out[7]:
Note that the above enhancement doesn't come at the cost of code-breaks. If you have already written applications that interfaced directly using pywin32, i.e. the code accessed properties as CoatingDir
, SamplesDir
, etc., the application should run even with PyZOS library, as shown below:
In [8]:
osys.TheApplication.SamplesDir # note that the properties doesn't have 'p' prefix in the names
Out[8]:
There are some reasons why we may want to override some of the methods provided by ZOS. As an example, consider the SaveAs
method of OpticalSystem
that accepts a filename
. If the filename
is invalid the SaveAs
method doesn't raise any exception/error, instead the lens data is saved to the default file "Lens.zmx".
This function in PyZOS has been overridden to raise an exception if the directory path is not valid, as shown below:
In [9]:
file_out = os.path.join(sdir, 'invalid_directory',
'Single Lens Example wizard+EFFL.zmx')
try:
osys.SaveAs(file_out)
except Exception as err:
print(repr(err))
In [10]:
file_out = os.path.join(sdir, 'Sequential', 'Objectives',
'Single Lens Example wizard+EFFL.zmx')
osys.SaveAs(file_out)
In [11]:
# Aperture
if USE_PYZOS:
sdata = osys.pSystemData
sdata.pAperture.pApertureValue = 40
else:
sdata = osys.SystemData
sdata.Aperture.ApertureValue = 40
In [12]:
# Set Field data
if USE_PYZOS:
field = sdata.pFields.AddField(0, 5.0, 1.0)
print('Number of fields =', sdata.pFields.pNumberOfFields)
else:
field = sdata.Fields.AddField(0, 5.0, 1.0)
print('Number of fields =', sdata.Fields.NumberOfFields)
The ZOS API provides a large set of enumerations that are accessible as constants through the constants
object of PyWin32. However, they are not introspectable using the Tab key. PyZOS automatically retrieves the constants and makes them introspectable as shown below:
In [13]:
Image('./images/00_02_constants.png')
Out[13]:
In [14]:
# Setting wavelength using wavelength preset
if USE_PYZOS:
sdata.pWavelengths.SelectWavelengthPreset(zos.Const.WavelengthPreset_d_0p587);
else:
sdata.Wavelengths.SelectWavelengthPreset(constants.WavelengthPreset_d_0p587);
PyZOS allows us to easily extend the functionality of any ZOS API object by adding custom methods and properties, supporting the idea of developing a useful library over time. In the following block of codes we have added custom methods zInsertNewSurfaceAt()
and zSetSurfaceData()
to the OpticalSystem
ojbect.
(Please note there is no implication that one cannot build a common set of functions without using PyZOS. Here, we only show that PyZOS allows us to add methods to the ZOS objects. How to add new methods PyZOS objects will be explained later.)
In [15]:
# Set Lens data Editor
if USE_PYZOS:
osys.zInsertNewSurfaceAt(1)
osys.zInsertNewSurfaceAt(1)
osys.zSetSurfaceData(1, thick=10, material='N-BK7', comment='front of lens')
osys.zSetSurfaceData(2, thick=50, comment='rear of lens')
osys.zSetSurfaceData(3, thick=350, comment='Stop is free to move')
else:
lde = osys.LDE
lde.InsertNewSurfaceAt(1)
lde.InsertNewSurfaceAt(1)
surf1 = lde.GetSurfaceAt(1)
surf2 = lde.GetSurfaceAt(2)
surf3 = lde.GetSurfaceAt(3)
surf1.Thickness = 10.0
surf1.Comment = 'front of lens'
surf1.Material = 'N-BK7'
surf2.Thickness = 50.0
surf2.Comment = 'rear of lens'
surf3.Thickness = 350.0
surf3.Comment = 'Stop is free to move'
The custom functions are introspectable, and they are identified by the prefix z to their names as shown in the following figure.
In [16]:
Image('./images/00_03_extendiblity_custom_functions.png')
Out[16]:
In [17]:
# Setting solves - Make thickness and radii variable
# nothing to demonstrate in particular in this block of code
if USE_PYZOS:
osys.pLDE.GetSurfaceAt(1).pRadiusCell.MakeSolveVariable()
osys.pLDE.GetSurfaceAt(1).pThicknessCell.MakeSolveVariable()
osys.pLDE.GetSurfaceAt(2).pRadiusCell.MakeSolveVariable()
osys.pLDE.GetSurfaceAt(2).pThicknessCell.MakeSolveVariable()
osys.pLDE.GetSurfaceAt(3).pThicknessCell.MakeSolveVariable()
else:
surf1.RadiusCell.MakeSolveVariable()
surf1.ThicknessCell.MakeSolveVariable()
surf2.RadiusCell.MakeSolveVariable()
surf2.ThicknessCell.MakeSolveVariable()
surf3.ThicknessCell.MakeSolveVariable()
In [18]:
# Setting up the default merit function
# this code block again shows that we can create add custom methods
# based on our requirements
if USE_PYZOS:
osys.zSetDefaultMeritFunctionSEQ(ofType=0, ofData=1, ofRef=0, rings=2, arms=0, grid=0,
useGlass=True, glassMin=3, glassMax=15, glassEdge=3,
useAir=True, airMin=0.5, airMax=1000, airEdge=0.5)
else:
mfe = osys.MFE
wizard = mfe.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
wizard.CommonSettings.OK()
Here we can demonstrate another strong reason why we may require to add methods to ZOS objects when using the ZOS API with pywin32
library. The problem is illustrated in the figure below. According to the ZOS API manual, the MFE object (IMeritFunctionEditor
) should have the methods AddRow()
, DeleteAllRows()
, DeleteRowAt()
, DeleteRowsAt()
, InsertRowAt()
and GetRowAt()
that it inherits from IEditor
object. However, due to the way pywin32
handles inheritence, these methods (defined in the base class) are apparently not available to the derived class object [1].
In [19]:
Image('./images/00_04_extendiblity_required_methods.png')
Out[19]:
In order to solve the above problem in PyZOS, currently we "add" these methods to the derived (and wrapped) objects and delegate the calls to the base class. (Probably there is a more intelligent method of solving this ... which will not require so much code re-writing!)
In [20]:
# Add operand
if USE_PYZOS:
mfe = osys.pMFE
operand1 = mfe.InsertNewOperandAt(1)
operand1.ChangeType(zos.Const.MeritOperandType_EFFL)
operand1.pTarget = 400.0
operand1.pWeight = 1.0
else:
operand1 = mfe.InsertNewOperandAt(1)
operand1.ChangeType(constants.MeritOperandType_EFFL)
operand1.Target = 400.0
operand1.Weight = 1.0
In [21]:
# Local optimization
if USE_PYZOS:
local_opt = osys.pTools.OpenLocalOptimization()
local_opt.pAlgorithm = zos.Const.OptimizationAlgorithm_DampedLeastSquares
local_opt.pCycles = zos.Const.OptimizationCycles_Automatic
local_opt.pNumberOfCores = 8
local_opt.RunAndWaitForCompletion()
local_opt.Close()
else:
local_opt = osys.Tools.OpenLocalOptimization()
local_opt.Algorithm = constants.OptimizationAlgorithm_DampedLeastSquares
local_opt.Cycles = constants.OptimizationCycles_Automatic
local_opt.NumberOfCores = 8
base_tool = CastTo(local_opt, 'ISystemTool')
base_tool.RunAndWaitForCompletion()
base_tool.Close()
In [22]:
# save the latest changes to the file
osys.Save()
In [23]:
%matplotlib inline
In [24]:
osys2 = zos.OpticalSystem()
In [25]:
# load a lens into the Optical System
lens = 'Cooke 40 degree field.zmx'
zfile = os.path.join(sdir, 'Sequential', 'Objectives', lens)
osys2.LoadFile(zfile, False)
Out[25]:
In [26]:
osys2.pSystemName
Out[26]:
In [27]:
# check the aperture
osys2.pSystemData.pAperture.pApertureValue
Out[27]:
In [28]:
# a more detailed information about the pupil
osys2.pLDE.GetPupil()
Out[28]:
In [29]:
# Thickness of a surface
surf6 = osys2.pLDE.GetSurfaceAt(6)
surf6.pThickness
Out[29]:
In [30]:
# Thickness of surface through custom added method
osys2.zGetSurfaceData(6).thick
Out[30]:
In [31]:
# Open Analysis windows in the system currently
num_analyses = osys2.pAnalyses.pNumberOfAnalyses
for i in range(num_analyses):
print(osys2.pAnalyses.Get_AnalysisAtIndex(i+1).pGetAnalysisName)
In [32]:
#mtf analysis
fftMtf = osys2.pAnalyses.New_FftMtf() # open a new FFT MTF window
fftMtf
Out[32]:
In [33]:
fftMtf_settings = fftMtf.GetSettings()
fftMtf_settings
Out[33]:
In [34]:
# Set the maximum frequency to 160 lp/mm
fftMtf_settings.pMaximumFrequency = 160.0
In [35]:
# run the analysis
fftMtf.ApplyAndWaitForCompletion()
In [36]:
# results
fftMtf_results = fftMtf.GetResults() # returns an <pyzos.zosutils.IAR_ object
In [37]:
# info about the result data
print('Number of data grids:', fftMtf_results.pNumberOfDataGrids)
print('Number of data series:', fftMtf_results.pNumberOfDataSeries)
In [38]:
ds = fftMtf_results.GetDataSeries(1)
In [39]:
ds.pDescription
Out[39]:
In [40]:
ds.pNumSeries
Out[40]:
In [41]:
ds.pSeriesLabels
Out[41]:
Since these objects has not been wrapped (at the time of this writing), we will wrap them first
In [42]:
dsXdata = ds.pXData
dsYdata = ds.pYData
In [43]:
freq = np.array(dsXdata.pData)
mtf = np.array(dsYdata.pData) # shape = (len(freq) , ds.pNumSeries)
In [44]:
# build a function to plot the FFTMTF
def plot_FftMtf(optical_system, max_freq=160.0):
fftMtf = optical_system.pAnalyses.New_FftMtf()
fftMtf.GetSettings().pMaximumFrequency = max_freq
fftMtf.ApplyAndWaitForCompletion()
fftMtf_results = fftMtf.GetResults()
fig, ax = plt.subplots(1,1, figsize=(8,6))
num_dataseries = fftMtf_results.pNumberOfDataSeries
col = ['#0080FF', '#F52080', '#00CC60', '#B96F20', '#1f77b4',
'#ff7f0e', '#2ca02c', '#8c564b', '#00BFFF', '#FF8073']
for i in range(num_dataseries):
ds = fftMtf_results.GetDataSeries(i)
dsXdata = ds.pXData
dsYdata = ds.pYData
freq = np.array(dsXdata.pData)
mtf = np.array(dsYdata.pData) # shape = (len(freq) , ds.pNumSeries)
ax.plot(freq[::5], mtf[::5, 0], color=col[i], lw=1.5, label=ds.pDescription) # tangential
ax.plot(freq[::5], mtf[::5, 1], '--', color=col[i], lw=2) # sagittal
ax.set_xlabel('Spatial Frequency (lp/mm)')
ax.set_ylabel('FFT MTF')
ax.legend()
plt.text(0.85, -0.1,u'\u2014 {}'.format(ds.pSeriesLabels[0]), transform=ax.transAxes)
plt.text(0.85, -0.15,'-- {}'.format(ds.pSeriesLabels[1]), transform=ax.transAxes)
plt.grid()
plt.show()
In [45]:
# FFT MTF of Optical System 2
plot_FftMtf(osys2)
In [46]:
# FFT MTF of Optical System 1
plot_FftMtf(osys, 100)
In [47]:
# Close the application
if USE_PYZOS:
app = osys.pTheApplication
app.CloseApplication()
del app
In [ ]: